diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index a51b9757b41..dac3fd30db7 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -136,6 +136,8 @@ module.exports = { rules: { 'no-console': 'off', 'no-empty-pattern': 'off', + // Disable React Testing Library rules for Playwright tests + 'testing-library/prefer-screen-queries': 'off', }, }, ], diff --git a/frontend/packages/integration-tests/mocks/cluster-version.ts b/frontend/e2e/mocks/cluster-version.ts similarity index 63% rename from frontend/packages/integration-tests/mocks/cluster-version.ts rename to frontend/e2e/mocks/cluster-version.ts index b1efbd0e828..a671441d7d2 100644 --- a/frontend/packages/integration-tests/mocks/cluster-version.ts +++ b/frontend/e2e/mocks/cluster-version.ts @@ -1,3 +1,134 @@ +/** + * Mock cluster version data for testing channel modal behavior + * Simplified from packages/integration-tests/mocks/cluster-version.ts + */ + +const baseClusterVersion = { + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterVersion', + metadata: { + creationTimestamp: '2024-01-04T05:14:57Z', + generation: 4, + name: 'version', + resourceVersion: '370626', + uid: '40b1ad1b-13d2-4c7c-932a-ce78c4447ed8', + }, + spec: { + channel: 'stable-4.16', + clusterID: '4976480a-15e1-4c94-bafe-aafb96bc0248', + upstream: 'https://openshift-release.apps.ci.l2s4.p1.openshiftapps.com/graph', + }, + status: { + availableUpdates: [], + conditions: [ + { + lastTransitionTime: '2024-01-04T05:37:10Z', + status: 'True', + type: 'RetrievedUpdates', + }, + { + lastTransitionTime: '2024-01-04T05:15:00Z', + message: 'Capabilities match configured spec', + reason: 'AsExpected', + status: 'False', + type: 'ImplicitlyEnabledCapabilities', + }, + { + lastTransitionTime: '2024-01-04T05:15:00Z', + message: + 'Payload loaded version="4.16.0" image="registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721" architecture="amd64"', + reason: 'PayloadLoaded', + status: 'True', + type: 'ReleaseAccepted', + }, + { + lastTransitionTime: '2024-01-04T05:35:14Z', + message: 'Done applying 4.16.0', + status: 'True', + type: 'Available', + }, + { + lastTransitionTime: '2024-01-04T05:35:14Z', + status: 'False', + type: 'Failing', + }, + { + lastTransitionTime: '2024-01-04T05:35:14Z', + message: 'Cluster version is 4.16.0', + status: 'False', + type: 'Progressing', + }, + ], + desired: { + image: + 'registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721', + version: '4.16.0', + }, + history: [ + { + completionTime: '2024-01-04T05:35:14Z', + image: + 'registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721', + startedTime: '2024-01-04T05:15:00Z', + state: 'Completed', + verified: false, + version: '4.16.0', + }, + ], + observedGeneration: 3, + versionHash: 'rqBs61ZyVwQ=', + }, +}; + +const conditionUpgradeNoChannel = { + lastTransitionTime: '2024-01-10T18:10:53Z', + message: 'The update channel has not been configured.', + reason: 'NoChannel', + status: 'False', + type: 'RetrievedUpdates', +}; + +const conditionProgressing = { + lastTransitionTime: '2024-01-11T13:45:34Z', + message: 'Cluster version is 4.16.0', + status: 'True', + type: 'Progressing', +}; + +const desired = { + image: + 'registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721', + version: '4.16.2', +}; + +/** + * Cluster version with no channel configured + */ +export const clusterVersionWithoutChannel = JSON.parse(JSON.stringify(baseClusterVersion)); +clusterVersionWithoutChannel.spec.channel = ''; +clusterVersionWithoutChannel.status.conditions.push(conditionUpgradeNoChannel); + +/** + * Cluster version with channel and available channels in desired.channels + */ +export const clusterVersionWithDesiredChannels = JSON.parse(JSON.stringify(baseClusterVersion)); +clusterVersionWithDesiredChannels.status.desired.channels = [ + 'stable-4.16', + 'candidate-4.16', + 'fast-4.16', + 'stable-4.17', + 'candidate-4.17', + 'fast-4.17', +]; + +/** + * Cluster version with update in progress + */ +export const clusterVersionWithProgressing = JSON.parse(JSON.stringify(baseClusterVersion)); +clusterVersionWithProgressing.status.conditions.push(conditionProgressing); +clusterVersionWithProgressing.spec.desired = desired; +clusterVersionWithProgressing.status.desired = desired; + const availableUpdates = [ { image: @@ -34,7 +165,7 @@ const conditionalUpdates = [ url: 'https://bugzilla.redhat.com/show_bug.cgi?id=2047190', name: 'AlibabaStorageDriverDemo', message: - 'The Alibaba storage driver was updated from a patched 1.1.4 to a patched 1.1.6 in 4.10.0-rc.0. That is unlikely to fix anything that regressed in this provider from fc.3 to fc.4, but this conditional update is pretending it does, as a demonstration of the conditional update system.', + 'The Alibaba storage driver was updated from a patched 1.1.4 to a patched 1.1.6 in 4.10.0-rc.0.', matchingRules: [ { type: 'PromQL', @@ -53,7 +184,7 @@ const conditionalUpdates = [ lastTransitionTime: '2024-02-03T22:53:33Z', reason: 'AlibabaStorageDriverDemo', message: - 'The Alibaba storage driver was updated from a patched 1.1.4 to a patched 1.1.6 in 4.10.0-rc.0. That is unlikely to fix anything that regressed in this provider from fc.3 to fc.4, but this conditional update is pretending it does, as a demonstration of the conditional update system. https://bugzilla.redhat.com/show_bug.cgi?id=2047190', + 'The Alibaba storage driver was updated from a patched 1.1.4 to a patched 1.1.6 in 4.10.0-rc.0.', }, ], }, @@ -69,7 +200,7 @@ const conditionalUpdates = [ url: 'https://bugzilla.redhat.com/show_bug.cgi?id=2047190', name: 'AlibabaStorageDriverDemo', message: - 'The Alibaba storage driver was updated from a patched 1.1.4 to a patched 1.1.6 in 4.10.0-rc.0. That is unlikely to fix anything that regressed in this provider from fc.3 to fc.4, but this conditional update is pretending it does, as a demonstration of the conditional update system.', + 'The Alibaba storage driver was updated from a patched 1.1.4 to a patched 1.1.6 in 4.10.0-rc.0.', matchingRules: [ { type: 'PromQL', @@ -94,45 +225,25 @@ const conditionalUpdates = [ }, ]; -const conditionFailing = { - lastTransitionTime: '2024-01-11T13:45:34Z', - message: 'Cluster operator kube-storage-version-migrator has not yet reported success', - status: 'True', - type: 'Failing', -}; - -const conditionInvalid = { - lastTransitionTime: '2024-01-11T13:15:41Z', - message: - 'The cluster version is invalid: spec.desiredUpdate.version: Invalid value: "4.16.0-foo": when image is empty the update must be a previous version or an available update', - status: 'True', - type: 'Invalid', -}; - -const conditionProgressing = { - lastTransitionTime: '2024-01-11T13:45:34Z', - message: 'Cluster version is 4.16.0', - status: 'True', - type: 'Progressing', -}; +/** + * Cluster version with available updates + */ +export const clusterVersionWithAvailableUpdates = JSON.parse(JSON.stringify(baseClusterVersion)); +clusterVersionWithAvailableUpdates.status.availableUpdates = availableUpdates; -const conditionReleaseAcceptedFalse = { - lastTransitionTime: '2024-01-11T19:09:43Z', - message: - 'Retrieving payload failed version="4.16.0-bar" image="registry.ci.openshift.org/ocp/release@sha256:7d7a1696145043c9d4653f27cc001da283ff4b39c915e226104d82cc56e0eeb9" failure=The update cannot be verified: unable to verify sha256:7d7a1696145043c9d4653f27cc001da283ff4b39c915e226104d82cc56e0eeb9 against keyrings: verifier-public-key-redhat', - reason: 'RetrievePayload', - status: 'False', - type: 'ReleaseAccepted', -}; +/** + * Cluster version with available and conditional updates + */ +export const clusterVersionWithAvailableAndConditionalUpdates = JSON.parse( + JSON.stringify(clusterVersionWithAvailableUpdates), +); +clusterVersionWithAvailableAndConditionalUpdates.status.conditionalUpdates = conditionalUpdates; -const conditionRetrievedUpdatesNotFound = { - lastTransitionTime: '2024-01-11T13:15:41Z', - message: - 'Unable to retrieve available updates: currently reconciling cluster version 4.16.0-foo not found in the "foo" channel', - reason: 'VersionNotFound', - status: 'False', - type: 'RetrievedUpdates', -}; +/** + * Cluster version with conditional updates only (no available updates) + */ +export const clusterVersionWithConditionalUpdates = JSON.parse(JSON.stringify(baseClusterVersion)); +clusterVersionWithConditionalUpdates.status.conditionalUpdates = conditionalUpdates; const conditionUpgradableFalse = { lastTransitionTime: '2024-01-04T05:45:29Z', @@ -143,183 +254,10 @@ const conditionUpgradableFalse = { type: 'Upgradeable', }; -const conditionUpgradeNoChannel = { - lastTransitionTime: '2024-01-10T18:10:53Z', - message: 'The update channel has not been configured.', - reason: 'NoChannel', - status: 'False', - type: 'RetrievedUpdates', -}; - -const desired = { - image: - 'registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721', - version: '4.16.2', -}; - -const desirecChannels = [ - 'stable-4.16', - 'candidate-4.16', - 'fast-4.16', - 'stable-4.17', - 'candidate-4.17', - 'fast-4.17', -]; - -export const clusterVersion = { - apiVersion: 'config.openshift.io/v1', - kind: 'ClusterVersion', - metadata: { - creationTimestamp: '2024-01-04T05:14:57Z', - generation: 4, - name: 'version', - resourceVersion: '370626', - uid: '40b1ad1b-13d2-4c7c-932a-ce78c4447ed8', - }, - spec: { - channel: 'stable-4.16', - clusterID: '4976480a-15e1-4c94-bafe-aafb96bc0248', - upstream: 'https://openshift-release.apps.ci.l2s4.p1.openshiftapps.com/graph', - }, - status: { - availableUpdates: [], - capabilities: { - enabledCapabilities: [ - 'Build', - 'CSISnapshot', - 'CloudCredential', - 'Console', - 'DeploymentConfig', - 'ImageRegistry', - 'Insights', - 'MachineAPI', - 'NodeTuning', - 'OperatorLifecycleManager', - 'Storage', - 'baremetal', - 'marketplace', - 'openshift-samples', - ], - knownCapabilities: [ - 'Build', - 'CSISnapshot', - 'CloudCredential', - 'Console', - 'DeploymentConfig', - 'ImageRegistry', - 'Insights', - 'MachineAPI', - 'NodeTuning', - 'OperatorLifecycleManager', - 'Storage', - 'baremetal', - 'marketplace', - 'openshift-samples', - ], - }, - conditions: [ - { - lastTransitionTime: '2024-01-04T05:37:10Z', - status: 'True', - type: 'RetrievedUpdates', - }, - { - lastTransitionTime: '2024-01-04T05:15:00Z', - message: 'Capabilities match configured spec', - reason: 'AsExpected', - status: 'False', - type: 'ImplicitlyEnabledCapabilities', - }, - { - lastTransitionTime: '2024-01-04T05:15:00Z', - message: - 'Payload loaded version="4.16.0" image="registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721" architecture="amd64"', - reason: 'PayloadLoaded', - status: 'True', - type: 'ReleaseAccepted', - }, - { - lastTransitionTime: '2024-01-04T05:35:14Z', - message: 'Done applying 4.16.0', - status: 'True', - type: 'Available', - }, - { - lastTransitionTime: '2024-01-04T05:35:14Z', - status: 'False', - type: 'Failing', - }, - { - lastTransitionTime: '2024-01-04T05:35:14Z', - message: 'Cluster version is 4.16.0', - status: 'False', - type: 'Progressing', - }, - ], - desired: { - image: - 'registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721', - version: '4.16.0', - }, - history: [ - { - completionTime: '2024-01-04T05:35:14Z', - image: - 'registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721', - startedTime: '2024-01-04T05:15:00Z', - state: 'Completed', - verified: false, - version: '4.16.0', - }, - ], - observedGeneration: 3, - versionHash: 'rqBs61ZyVwQ=', - }, -}; - -export const clusterVersionWithAvailableUpdates = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithAvailableUpdates.status.availableUpdates = availableUpdates; - -export const clusterVersionWithAvailableAndConditionalUpdates = JSON.parse( - JSON.stringify(clusterVersionWithAvailableUpdates), -); -clusterVersionWithAvailableAndConditionalUpdates.status.conditionalUpdates = conditionalUpdates; - -export const clusterVersionWithConditionalUpdates = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithConditionalUpdates.status.conditionalUpdates = conditionalUpdates; - -export const clusterVersionWithDesiredChannels = JSON.parse( - JSON.stringify(clusterVersionWithAvailableUpdates), -); -clusterVersionWithDesiredChannels.status.desired.channels = desirecChannels; - -export const clusterVersionWithFailing = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithFailing.status.conditions.push(conditionFailing); - -export const clusterVersionWithInvalidChannel = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithInvalidChannel.spec.channel = 'foo'; -clusterVersionWithInvalidChannel.status.conditions.push(conditionRetrievedUpdatesNotFound); - -export const clusterVersionWithInvalidRelease = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithInvalidRelease.status.conditions.push(conditionInvalid); - -export const clusterVersionWithProgressing = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithProgressing.status.conditions.push(conditionProgressing); -clusterVersionWithProgressing.spec.desired = desired; -clusterVersionWithProgressing.status.desired = desired; - -export const clusterVersionWithProgessingAndFailing = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithProgessingAndFailing.status.conditions.push(conditionProgressing); -clusterVersionWithProgessingAndFailing.status.conditions.push(conditionFailing); - -export const clusterVersionWithReleaseAcceptedFalse = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithReleaseAcceptedFalse.status.conditions.push(conditionReleaseAcceptedFalse); - +/** + * Cluster version with Upgradeable=False condition + */ export const clusterVersionWithUpgradeableFalse = JSON.parse( JSON.stringify(clusterVersionWithAvailableUpdates), ); clusterVersionWithUpgradeableFalse.status.conditions.push(conditionUpgradableFalse); - -export const clusterVersionWithoutChannel = JSON.parse(JSON.stringify(clusterVersion)); -clusterVersionWithoutChannel.spec.channel = ''; -clusterVersionWithoutChannel.status.conditions.push(conditionUpgradeNoChannel); diff --git a/frontend/packages/integration-tests/mocks/machine-config-pool.ts b/frontend/e2e/mocks/machine-config-pool.ts similarity index 100% rename from frontend/packages/integration-tests/mocks/machine-config-pool.ts rename to frontend/e2e/mocks/machine-config-pool.ts diff --git a/frontend/e2e/pages/cluster-settings-page.ts b/frontend/e2e/pages/cluster-settings-page.ts new file mode 100644 index 00000000000..a5a5ed4bf1d --- /dev/null +++ b/frontend/e2e/pages/cluster-settings-page.ts @@ -0,0 +1,208 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class ClusterSettingsPage extends BasePage { + // Navigation elements + private readonly pageHeading = this.page.getByTestId('cluster-settings-page-heading'); + private readonly detailsTab = this.page.getByTestId('horizontal-link-Details'); + private readonly clusterOperatorsTab = this.page.getByTestId('horizontal-link-ClusterOperators'); + private readonly configurationTab = this.page.getByTestId('horizontal-link-Configuration'); + + // Channel elements + private readonly currentChannelUpdateLink = this.page.getByTestId('current-channel-update-link'); + + // Managed cluster elements + private readonly hostedAlert = this.page.getByTestId('cluster-settings-alerts-hosted'); + private readonly updateButton = this.page.getByTestId('cv-update-button'); + private readonly upstreamServerUrl = this.page.getByTestId('cv-upstream-server-url'); + private readonly autoscalerLink = this.page.getByTestId('cv-autoscaler'); + + // Modal elements + private readonly modalTitle = this.page.getByTestId('modal-title'); + private readonly channelModalInput = this.page.getByTestId('channel-modal-input'); + private readonly channelModal = this.page.getByTestId('channel-modal'); + private readonly confirmActionButton = this.page.getByTestId('confirm-action'); + + /** + * Navigate to cluster settings details page + */ + async navigateToDetails(): Promise { + await this.goTo('/settings/cluster'); + await this.detailsTab.waitFor({ state: 'visible' }); + } + + /** + * Get the current channel update link locator + */ + getCurrentChannelLink(): Locator { + return this.currentChannelUpdateLink; + } + + /** + * Click the current channel update link to open the modal + */ + async openChannelModal(): Promise { + await this.currentChannelUpdateLink.waitFor({ state: 'visible' }); + await this.currentChannelUpdateLink.click(); + // Wait a bit for modal animation/React state update + await this.page.waitForTimeout(1000); + // Now wait for modal + await this.modalTitle.waitFor({ state: 'visible', timeout: 30_000 }); + } + + /** + * Get the modal title locator + */ + getModalTitle(): Locator { + return this.modalTitle; + } + + /** + * Type a channel name into the input field (for "Input channel" modal) + */ + async inputChannelName(channelName: string): Promise { + await this.channelModalInput.waitFor({ state: 'visible' }); + await this.channelModalInput.clear(); + await this.channelModalInput.fill(channelName); + } + + /** + * Open the channel dropdown and select a channel (for "Select channel" modal) + */ + async selectChannelFromDropdown(channelName: string): Promise { + const dropdownToggle = this.channelModal.locator('[data-test="console-select-menu-toggle"]'); + await this.robustClick(dropdownToggle); + + const channelOption = this.page.locator(`[data-test-dropdown-menu="${channelName}"]`); + await this.robustClick(channelOption); + } + + /** + * Click the confirm action button in the modal + */ + async confirmAction(): Promise { + await this.robustClick(this.confirmActionButton); + // Wait for modal to close + await this.modalTitle.waitFor({ state: 'hidden', timeout: 10_000 }).catch(() => { + // Modal may close quickly, ignore timeout + }); + } + + /** + * Navigate to the Configuration tab + */ + async navigateToConfigurationTab(): Promise { + await this.navigateToTab(this.configurationTab); + // Wait for loading to complete + await this.page + .locator('.loading-box__loaded') + .first() + .waitFor({ state: 'visible', timeout: 30_000 }); + } + + /** + * Navigate to Details tab, then navigate to Configuration tab + * Common pattern for Configuration tab tests + */ + async navigateToConfiguration(): Promise { + await this.navigateToDetails(); + await this.navigateToConfigurationTab(); + } + + /** + * Get the channel modal input field locator + */ + getChannelModalInput(): Locator { + return this.channelModalInput; + } + + /** + * Get the channel modal locator + */ + getChannelModal(): Locator { + return this.channelModal; + } + + /** + * Navigate to the ClusterOperators tab + */ + async navigateToClusterOperatorsTab(): Promise { + await this.navigateToTab(this.clusterOperatorsTab); + await this.waitForLoadingComplete(); + } + + /** + * Get the page heading locator + */ + getPageHeading(): Locator { + return this.pageHeading; + } + + /** + * Get locators for managed cluster UI elements that should be hidden + */ + getHostedAlert(): Locator { + return this.hostedAlert; + } + + getUpdateButton(): Locator { + return this.updateButton; + } + + getUpstreamServerUrl(): Locator { + return this.upstreamServerUrl; + } + + getAutoscalerLink(): Locator { + return this.autoscalerLink; + } + + /** + * Check if a configuration resource link exists + */ + getConfigurationResourceLink(resourceName: string): Locator { + return this.page.locator( + `[href="/k8s/cluster/config.openshift.io~v1~${resourceName}/cluster"]`, + ); + } + + /** + * Open the update cluster modal + */ + async openUpdateModal(): Promise { + const updateButton = this.page.getByTestId('cv-update-button'); + await updateButton.waitFor({ state: 'visible' }); + await this.robustClick(updateButton); + + // Wait for modal to appear + await this.modalTitle.waitFor({ state: 'visible', timeout: 10_000 }); + await this.page.waitForFunction( + () => { + const title = document.querySelector('[data-test="modal-title"]'); + return title?.textContent?.includes('Update cluster'); + }, + { timeout: 10_000 }, + ); + + const modal = this.page.getByTestId('update-cluster-modal'); + await modal.waitFor({ state: 'visible' }); + } + + /** + * Open the update version dropdown in the update modal + */ + async openUpdateDropdown(): Promise { + const modal = this.page.getByTestId('update-cluster-modal'); + const dropdownToggle = modal.locator('[data-test="dropdown-with-switch-toggle"]'); + + // Wait for toggle to be visible and enabled + await dropdownToggle.waitFor({ state: 'visible' }); + await dropdownToggle.waitFor({ state: 'attached' }); + + // Wait for any loading states to clear + await this.page.waitForLoadState('networkidle'); + + await this.robustClick(dropdownToggle); + } +} diff --git a/frontend/e2e/pages/details-page.ts b/frontend/e2e/pages/details-page.ts new file mode 100644 index 00000000000..b884c16e8fa --- /dev/null +++ b/frontend/e2e/pages/details-page.ts @@ -0,0 +1,93 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class DetailsPage extends BasePage { + private readonly pageHeading = this.page.getByTestId('page-heading'); + private readonly resourceTitle = this.page.getByTestId('resource-title'); + private readonly skeletonLoader = this.page.getByTestId('skeleton-detail-view'); + + /** + * Wait for the details page to load + */ + async waitForPageLoad(): Promise { + await this.skeletonLoader.waitFor({ state: 'detached', timeout: 30_000 }).catch(() => { + // Skeleton may not appear for fast loads + }); + // Wait for either resource title or page heading to be visible + await Promise.race([ + this.resourceTitle.waitFor({ state: 'visible', timeout: 30_000 }), + this.pageHeading.waitFor({ state: 'visible', timeout: 30_000 }), + ]); + } + + /** + * Get the page heading locator + */ + getPageHeading(): Locator { + return this.pageHeading; + } + + /** + * Select a specific tab by name + */ + async selectTab(tabName: string): Promise { + const tab = this.page.getByTestId(`horizontal-link-${tabName}`); + await this.robustClick(tab); + await this.waitForLoadingComplete(); + } + + /** + * Click a kebab menu action (assumes menu is already open) + */ + async clickKebabAction(actionId: string): Promise { + const action = this.page.locator(`[data-test-action="${actionId}"]`); + await action.waitFor({ state: 'visible', timeout: 10_000 }); + await this.robustClick(action); + } + + /** + * Get a resource row link by test ID (e.g., for ClusterOperators or Configuration resources) + * Uses data-test attribute (modern selector convention) + */ + getResourceRow(resourceId: string): Locator { + // Prefer the link with data-test (for Configuration resources) + // Fall back to any element with data-test or data-test-action + const link = this.page.locator(`a[data-test="${resourceId}"]`); + const fallback = this.page.locator( + `[data-test="${resourceId}"], [data-test-action="${resourceId}"]`, + ); + + // Return link if it exists, otherwise fallback + return link.or(fallback).first(); + } + + /** + * Click a resource row to navigate to its details + * Set waitForLoad=false to skip waiting for page load (useful for in-page navigation) + */ + async clickResourceRow(resourceId: string, waitForLoad = true): Promise { + const row = this.getResourceRow(resourceId); + await row.waitFor({ state: 'visible', timeout: 30_000 }); + await this.robustClick(row); + if (waitForLoad) { + await this.waitForPageLoad(); + } + } + + /** + * Get a resource row by test-action attribute (for Configuration resources) + */ + getResourceByAction(actionName: string): Locator { + return this.page.locator(`[data-test-action="${actionName}"]`); + } + + /** + * Click a resource in Configuration tab and open its kebab menu + */ + async openResourceKebabMenu(actionName: string): Promise { + const resourceRow = this.getResourceByAction(actionName); + const kebabButton = resourceRow.getByTestId('kebab-button'); + await this.robustClick(kebabButton); + } +} diff --git a/frontend/e2e/pages/oauth-page.ts b/frontend/e2e/pages/oauth-page.ts new file mode 100644 index 00000000000..27d19b5382f --- /dev/null +++ b/frontend/e2e/pages/oauth-page.ts @@ -0,0 +1,107 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class OAuthPage extends BasePage { + private readonly oauthSettingsURL = '/k8s/cluster/config.openshift.io~v1~OAuth/cluster'; + private readonly addIDPDropdown = this.page.getByTestId('dropdown-button'); + private readonly idpNameInput = this.page.locator('#idp-name'); + private readonly addIDPButton = this.page.getByTestId('add-idp'); + private readonly errorAlert = this.page.getByTestId('alert-error'); + + /** + * Navigate to OAuth settings page + */ + async navigateToOAuthSettings(): Promise { + await this.goTo(this.oauthSettingsURL); + } + + /** + * Start IDP setup: open dropdown, select IDP type, enter name + */ + async startIDPSetup(idpName: string, idpType: string): Promise { + await this.robustClick(this.addIDPDropdown); + const idpOption = this.page.getByTestId(idpType); + await this.robustClick(idpOption); + await this.idpNameInput.clear(); + await this.idpNameInput.fill(idpName); + } + + /** + * Click Add button and verify IDP was created successfully + */ + async saveAndVerifyIDP(idpName: string, idpType: string): Promise { + await this.robustClick(this.addIDPButton); + + // Wait for navigation back to OAuth settings page + await this.waitForLoadingComplete(); + + // Verify no error alert + await this.errorAlert.waitFor({ state: 'detached', timeout: 5_000 }).catch(() => { + // Alert may not appear at all + }); + + // Verify the IDP appears in the list + const idpNameCell = this.page.getByTestId(`idp-name-${idpName}`); + await idpNameCell.waitFor({ state: 'visible', timeout: 30_000 }); + + // Verify content matches expected values + await this.page.waitForFunction( + ({ name, type }) => { + const nameEl = document.querySelector(`[data-test="idp-name-${name}"]`); + const typeEl = document.querySelector(`[data-test="idp-type-${name}"]`); + return nameEl?.textContent === name && typeEl?.textContent === type; + }, + { name: idpName, type: idpType }, + { timeout: 10_000 }, + ); + } + + /** + * Get an IDP row kebab menu + */ + getIDPKebabMenu(idpName: string): Locator { + return this.page.getByTestId(`idp-kebab-${idpName}`); + } + + /** + * Remove an IDP using the kebab menu + */ + async removeIDP(idpName: string): Promise { + // First verify the IDP exists + const kebabCell = this.getIDPKebabMenu(idpName); + await kebabCell.waitFor({ state: 'visible', timeout: 5_000 }).catch(() => { + throw new Error(`IDP "${idpName}" not found in the list - cannot remove`); + }); + + // Open the kebab menu - find the button within the kebab cell + const kebabButton = kebabCell.getByTestId('kebab-button'); + await this.robustClick(kebabButton); + + // Click the Remove action + const removeAction = this.page.locator('[data-test-action="Remove identity provider"]'); + await removeAction.waitFor({ state: 'visible', timeout: 10_000 }); + await this.robustClick(removeAction); + + // Confirm the removal + const confirmButton = this.page.getByTestId('confirm-action'); + await confirmButton.waitFor({ state: 'visible', timeout: 5_000 }); + await this.robustClick(confirmButton); + + // Wait for loading to complete after removal + await this.waitForLoadingComplete(); + + // Verify the IDP was removed (wait up to 30 seconds for OAuth operator to process) + await kebabCell.waitFor({ state: 'detached', timeout: 30_000 }); + } + + /** + * Verify an IDP does not exist in the list + */ + async verifyIDPNotExists(idpName: string): Promise { + const kebab = this.getIDPKebabMenu(idpName); + await kebab.waitFor({ state: 'detached', timeout: 5_000 }).catch(() => { + // Already doesn't exist + }); + } +} diff --git a/frontend/e2e/pages/overview-page.ts b/frontend/e2e/pages/overview-page.ts new file mode 100644 index 00000000000..115bf52200d --- /dev/null +++ b/frontend/e2e/pages/overview-page.ts @@ -0,0 +1,43 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class OverviewPage extends BasePage { + private readonly controlPlaneSection = this.page.getByTestId('Control Plane'); + + /** + * Navigate to the Overview page + */ + async navigateToOverview(): Promise { + await this.goTo('/overview'); + await this.waitForLoadingComplete(); + } + + /** + * Get the Control Plane section locator + */ + getControlPlaneSection(): Locator { + return this.controlPlaneSection; + } + + /** + * Navigate to Overview via sidebar navigation + */ + async navigateViaNav(): Promise { + // Wait for any ongoing navigation or loading to complete + await this.page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { + // Page might already be idle + }); + await this.waitForLoadingComplete(); + + // Click Home in sidebar + const homeButton = this.page.getByRole('button', { name: 'Home' }); + await this.robustClick(homeButton); + + // Click Overview in the expanded section + const overviewLink = this.page.getByRole('link', { name: 'Overview' }); + await this.robustClick(overviewLink); + + await this.waitForLoadingComplete(); + } +} diff --git a/frontend/e2e/tests/console/cluster-settings/channel-modal.spec.ts b/frontend/e2e/tests/console/cluster-settings/channel-modal.spec.ts new file mode 100644 index 00000000000..d49d98576fd --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/channel-modal.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; +import { + clusterVersionWithoutChannel, + clusterVersionWithDesiredChannels, +} from '../../../mocks/cluster-version'; + +const CLUSTER_VERSION_URL = '**/apis/config.openshift.io/v1/clusterversions/version'; + +test.describe('Cluster Settings channel modal', { tag: ['@admin', '@smoke'] }, () => { + test('changes based on cluster version', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + + await test.step('Handle no channel configured scenario', async () => { + // Mock the API response to return cluster version without channel + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithoutChannel), + }); + }); + + // Navigate to cluster settings (dismisses tour and triggers the mocked response) + await clusterSettings.navigateToDetails(); + + // Verify current channel shows "Not configured" + await expect(clusterSettings.getCurrentChannelLink()).toContainText('Not configured'); + + // Open the modal + await clusterSettings.openChannelModal(); + + // Verify modal title is "Input channel" (the key test - modal adapts to state) + await expect(clusterSettings.getModalTitle()).toContainText('Input channel'); + + // Verify the input field is present (not a dropdown) + await expect(clusterSettings.getChannelModalInput()).toBeVisible(); + + // Close modal without submitting (this is a UI state test, not an integration test) + await clusterSettings.page.keyboard.press('Escape'); + }); + + await test.step('Handle channel configured with available channels scenario', async () => { + // Clear previous route and set new mock + await page.unroute(CLUSTER_VERSION_URL); + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithDesiredChannels), + }); + }); + + // Navigate to cluster settings to trigger the mocked response + await clusterSettings.navigateToDetails(); + + // Verify current channel shows "stable-4.16" + await expect(clusterSettings.getCurrentChannelLink()).toContainText('stable-4.16'); + + // Open the modal + await clusterSettings.openChannelModal(); + + // Verify modal title is "Select channel" (the key test - modal adapts to state) + await expect(clusterSettings.getModalTitle()).toContainText('Select channel'); + + // Verify the dropdown is present (not an input field) + const dropdown = clusterSettings + .getChannelModal() + .locator('[data-test="console-select-menu-toggle"]'); + await expect(dropdown).toBeVisible(); + + // Close modal without submitting (this is a UI state test, not an integration test) + await clusterSettings.page.keyboard.press('Escape'); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/cluster-settings.spec.ts b/frontend/e2e/tests/console/cluster-settings/cluster-settings.spec.ts new file mode 100644 index 00000000000..3bb6d8f6011 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/cluster-settings.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; +import { DetailsPage } from '../../../pages/details-page'; + +test.describe('Cluster Settings', { tag: ['@admin'] }, () => { + test('displays page title, horizontal navigation tab headings and pages', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + + await test.step('Navigate to Cluster Settings and verify page title', async () => { + await clusterSettings.navigateToDetails(); + await expect(clusterSettings.getPageHeading()).toContainText('Cluster Settings'); + }); + + await test.step('Verify Details tab is accessible', async () => { + await clusterSettings.navigateToDetails(); + }); + + await test.step('Verify ClusterOperators tab is accessible', async () => { + await clusterSettings.navigateToClusterOperatorsTab(); + }); + + await test.step('Verify Configuration tab is accessible', async () => { + await clusterSettings.navigateToConfigurationTab(); + }); + }); + + test('displays Cluster Operators page and console Operator details page', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + const details = new DetailsPage(page); + + await test.step('Navigate to ClusterOperators tab', async () => { + await clusterSettings.navigateToDetails(); + await clusterSettings.navigateToClusterOperatorsTab(); + }); + + await test.step('Click console operator row', async () => { + await details.clickResourceRow('console'); + }); + + await test.step('Verify console operator details page and YAML tab', async () => { + await expect(details.getPageHeading()).toContainText('console'); + await details.selectTab('YAML'); + }); + }); + + test('displays Configuration page and ClusterVersion configuration details page', async ({ + page, + }) => { + const clusterSettings = new ClusterSettingsPage(page); + const details = new DetailsPage(page); + + await test.step('Navigate to Configuration tab', async () => { + await clusterSettings.navigateToConfiguration(); + // Wait for configuration resources to load + await page.getByTestId('ClusterVersion').waitFor({ state: 'visible', timeout: 30_000 }); + }); + + await test.step('Click ClusterVersion and select YAML tab', async () => { + // Click the ClusterVersion link directly + const clusterVersionLink = page.getByTestId('ClusterVersion'); + await clusterVersionLink.click(); + + // Wait for navigation and YAML tab to appear + await page.waitForLoadState('networkidle'); + await details.selectTab('YAML'); + }); + }); + + test('displays Configuration page and ClusterVersion Edit ClusterVersion resource details page', async ({ + page, + }) => { + const clusterSettings = new ClusterSettingsPage(page); + const details = new DetailsPage(page); + + await test.step('Navigate to Configuration tab', async () => { + await clusterSettings.navigateToConfiguration(); + }); + + await test.step('Open ClusterVersion kebab menu and click Edit', async () => { + await details.openResourceKebabMenu('ClusterVersion'); + await details.clickKebabAction('Edit ClusterVersion resource'); + }); + + await test.step('Verify Edit page loads with version title', async () => { + await expect(details.getPageHeading()).toContainText('version'); + }); + }); + + test('displays Configuration page and ClusterVersion Explore Console API details page', async ({ + page, + }) => { + const clusterSettings = new ClusterSettingsPage(page); + const details = new DetailsPage(page); + + await test.step('Navigate to Configuration tab', async () => { + await clusterSettings.navigateToConfiguration(); + }); + + await test.step('Open ClusterVersion kebab menu and click Explore API', async () => { + await details.openResourceKebabMenu('ClusterVersion'); + await details.clickKebabAction('Explore ClusterVersion API'); + }); + + await test.step('Verify API Explorer page loads', async () => { + await expect(details.getPageHeading()).toContainText('ClusterVersion'); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/managed-control-plane.spec.ts b/frontend/e2e/tests/console/cluster-settings/managed-control-plane.spec.ts new file mode 100644 index 00000000000..e5f082943a5 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/managed-control-plane.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; +import { OverviewPage } from '../../../pages/overview-page'; + +/** + * Tests UI behavior for managed clusters (ROSA, ARO, etc.) where control plane is external. + * This test only runs on clusters with SERVER_FLAGS.controlPlaneTopology = 'External'. + * On regular clusters, the test will skip with an explanatory message. + */ +test.describe('Cluster Settings when control plane is managed', { tag: ['@admin'] }, () => { + test('displays an alert and hides elements', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + const overviewPage = new OverviewPage(page); + + // Intercept and modify SERVER_FLAGS before React components mount + // Use Object.defineProperty to ensure the override persists + await page.addInitScript(() => { + // Store the original descriptor + let originalFlags: any = null; + + // Intercept SERVER_FLAGS assignment + Object.defineProperty(window, 'SERVER_FLAGS', { + get() { + return originalFlags; + }, + set(value) { + // Override controlPlaneTopology whenever SERVER_FLAGS is set + originalFlags = { ...value, controlPlaneTopology: 'External' }; + }, + configurable: true, + }); + }); + + await test.step( + 'Verify Details tab shows hosted alert and hides/disables update controls', + async () => { + await clusterSettings.navigateToDetails(); + + // Hosted cluster alert should be visible + await expect(clusterSettings.getHostedAlert()).toBeVisible(); + + // Update-related controls should be hidden or disabled + // Some are removed from DOM, others are disabled - check both states + await expect(clusterSettings.getCurrentChannelLink()).not.toBeAttached(); + await expect(clusterSettings.getUpdateButton()).not.toBeAttached(); + // Upstream server URL button is disabled but still in DOM + await expect(clusterSettings.getUpstreamServerUrl()).toBeDisabled(); + await expect(clusterSettings.getAutoscalerLink()).not.toBeAttached(); + + // Temporary admin user messages should not exist + // eslint-disable-next-line testing-library/prefer-screen-queries + const tempAdminMessage = page.getByText(/logged in as a temporary administrative user/i); + await expect(tempAdminMessage).not.toBeVisible(); + // eslint-disable-next-line testing-library/prefer-screen-queries + const allowOthersMessage = page.getByText(/allow others to log in/i); + await expect(allowOthersMessage).not.toBeVisible(); + }, + ); + + await test.step('Verify Configuration tab hides cluster-level config resources', async () => { + await clusterSettings.navigateToConfigurationTab(); + + // These configuration resources should not be editable in managed clusters + const hiddenConfigs = [ + 'APIServer', + 'Authentication', + 'DNS', + 'FeatureGate', + 'Networking', + 'OAuth', + 'Proxy', + 'Scheduler', + ]; + + for (const configName of hiddenConfigs) { + await expect(clusterSettings.getConfigurationResourceLink(configName)).not.toBeAttached(); + } + }); + + await test.step('Verify Overview page hides Control Plane section', async () => { + await overviewPage.navigateToOverview(); + + // Control Plane section should not exist for managed clusters + await expect(overviewPage.getControlPlaneSection()).not.toBeAttached(); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/oauth.spec.ts b/frontend/e2e/tests/console/cluster-settings/oauth.spec.ts new file mode 100644 index 00000000000..6f9ac5d380d --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/oauth.spec.ts @@ -0,0 +1,266 @@ +import { test } from '../../../fixtures'; +import { OAuthPage } from '../../../pages/oauth-page'; +import type KubernetesClient from '../../../clients/kubernetes-client'; + +test.describe('OAuth', { tag: ['@admin'] }, () => { + let client: KubernetesClient; + let originalOAuthConfig: any; + const testPrefix = `e2e-${Date.now()}`; + const createdIDPs: string[] = []; + + test.beforeAll(async ({ k8sClient }) => { + client = k8sClient; + + // Save original OAuth configuration + const response = await client.customObjectsApi.getClusterCustomObject({ + group: 'config.openshift.io', + version: 'v1', + plural: 'oauths', + name: 'cluster', + }); + originalOAuthConfig = response.body; + }); + + test.afterEach(async ({ page }) => { + // Clean up any IDPs created in this test that weren't removed + if (createdIDPs.length > 0) { + const oauth = new OAuthPage(page); + for (const idpName of createdIDPs) { + try { + await oauth.navigateToOAuthSettings(); + await oauth.removeIDP(idpName); + } catch { + // IDP may already be removed, continue + } + } + createdIDPs.length = 0; // Clear the array + } + }); + + test.afterAll(async () => { + if (!originalOAuthConfig || !client) { + return; + } + + // Restore original identity providers + const idps = originalOAuthConfig?.spec?.identityProviders ?? []; + try { + await client.customObjectsApi.patchClusterCustomObject({ + group: 'config.openshift.io', + version: 'v1', + plural: 'oauths', + name: 'cluster', + body: [ + { + op: 'replace', + path: '/spec/identityProviders', + value: idps, + }, + ], + }); + } catch (err) { + console.error('Failed to restore OAuth config:', err); + } + }); + + test('creates a Basic Authentication IDP and shows it on the OAuth settings page', async ({ + page, + }) => { + const oauth = new OAuthPage(page); + const idpName = `basic-auth-${testPrefix}`; + createdIDPs.push(idpName); + + await test.step('Navigate to OAuth settings and start IDP setup', async () => { + await oauth.navigateToOAuthSettings(); + await oauth.startIDPSetup(idpName, 'basicauth'); + }); + + await test.step('Fill Basic Authentication form', async () => { + await page.locator('#url').fill('https://example.com'); + }); + + await test.step('Save and verify IDP', async () => { + await oauth.saveAndVerifyIDP(idpName, 'BasicAuth'); + }); + + await test.step('Clean up: remove IDP', async () => { + await oauth.removeIDP(idpName); + createdIDPs.pop(); // Remove from tracking since we successfully deleted it + }); + }); + + test('creates a GitHub IDP and displays it on the OAuth settings page', async ({ page }) => { + const oauth = new OAuthPage(page); + const idpName = `github-${testPrefix}`; + createdIDPs.push(idpName); + + await test.step('Navigate to OAuth settings and start IDP setup', async () => { + await oauth.navigateToOAuthSettings(); + await oauth.startIDPSetup(idpName, 'github'); + }); + + await test.step('Fill GitHub form', async () => { + await page.locator('#client-id').fill('my-client-id'); + await page.locator('#client-secret').fill('my-client-secret'); + await page.getByTestId('list-input-Organization').fill('my-organization'); + }); + + await test.step('Save and verify IDP', async () => { + await oauth.saveAndVerifyIDP(idpName, 'GitHub'); + }); + + await test.step('Clean up: remove IDP', async () => { + await oauth.removeIDP(idpName); + createdIDPs.pop(); + }); + }); + + test('creates a GitLab IDP and displays on the OAuth settings page', async ({ page }) => { + const oauth = new OAuthPage(page); + const idpName = `gitlab-${testPrefix}`; + createdIDPs.push(idpName); + + await test.step('Navigate to OAuth settings and start IDP setup', async () => { + await oauth.navigateToOAuthSettings(); + await oauth.startIDPSetup(idpName, 'gitlab'); + }); + + await test.step('Fill GitLab form', async () => { + await page.locator('#url').fill('https://example.com'); + await page.locator('#client-id').fill('my-client-id'); + await page.locator('#client-secret').fill('my-client-secret'); + }); + + await test.step('Save and verify IDP', async () => { + await oauth.saveAndVerifyIDP(idpName, 'GitLab'); + }); + + await test.step('Clean up: remove IDP', async () => { + await oauth.removeIDP(idpName); + createdIDPs.pop(); + }); + }); + + test('creates a Google IDP and displays it on the OAuth settings page', async ({ page }) => { + const oauth = new OAuthPage(page); + const idpName = `google-${testPrefix}`; + createdIDPs.push(idpName); + + await test.step('Navigate to OAuth settings and start IDP setup', async () => { + await oauth.navigateToOAuthSettings(); + await oauth.startIDPSetup(idpName, 'google'); + }); + + await test.step('Fill Google form', async () => { + await page.locator('#client-id').fill('my-client-id'); + await page.locator('#client-secret').fill('my-client-secret'); + await page.locator('#hosted-domain').fill('example.com'); + }); + + await test.step('Save and verify IDP', async () => { + await oauth.saveAndVerifyIDP(idpName, 'Google'); + }); + + await test.step('Clean up: remove IDP', async () => { + await oauth.removeIDP(idpName); + createdIDPs.pop(); + }); + }); + + test('creates a Keystone IDP and displays it on the OAuth settings page', async ({ page }) => { + const oauth = new OAuthPage(page); + const idpName = `keystone-${testPrefix}`; + createdIDPs.push(idpName); + + await test.step('Navigate to OAuth settings and start IDP setup', async () => { + await oauth.navigateToOAuthSettings(); + await oauth.startIDPSetup(idpName, 'keystone'); + }); + + await test.step('Fill Keystone form', async () => { + await page.locator('#domain-name').fill('example.com'); + await page.locator('#url').fill('https://example.com'); + }); + + await test.step('Save and verify IDP', async () => { + await oauth.saveAndVerifyIDP(idpName, 'Keystone'); + }); + + await test.step('Clean up: remove IDP', async () => { + await oauth.removeIDP(idpName); + createdIDPs.pop(); + }); + }); + + test('creates a LDAP IDP and displays it on the OAuth settings page', async ({ page }) => { + const oauth = new OAuthPage(page); + const idpName = `ldap-${testPrefix}`; + createdIDPs.push(idpName); + + await test.step('Navigate to OAuth settings and start IDP setup', async () => { + await oauth.navigateToOAuthSettings(); + await oauth.startIDPSetup(idpName, 'ldap'); + }); + + await test.step('Fill LDAP form', async () => { + await page.locator('#url').fill('ldap://ldap.example.com/o=Acme?cn?sub?(enabled=true)'); + }); + + await test.step('Save and verify IDP', async () => { + await oauth.saveAndVerifyIDP(idpName, 'LDAP'); + }); + + await test.step('Clean up: remove IDP', async () => { + await oauth.removeIDP(idpName); + createdIDPs.pop(); + }); + }); + + test('creates a OpenID IDP and displays it on the OAuth settings page', async ({ page }) => { + const oauth = new OAuthPage(page); + const idpName = `oidc-${testPrefix}`; + createdIDPs.push(idpName); + + await test.step('Navigate to OAuth settings and start IDP setup', async () => { + await oauth.navigateToOAuthSettings(); + await oauth.startIDPSetup(idpName, 'oidconnect'); + }); + + await test.step('Fill OpenID form', async () => { + await page.locator('#client-id').fill('my-client-id'); + await page.locator('#client-secret').fill('my-client-secret'); + await page.locator('#issuer').fill('https://example.com'); + }); + + await test.step('Save and verify IDP', async () => { + await oauth.saveAndVerifyIDP(idpName, 'OpenID'); + }); + + await test.step('Clean up: remove IDP', async () => { + await oauth.removeIDP(idpName); + createdIDPs.pop(); + }); + }); + + test('creates and removes a Basic Authentication IDP', async ({ page }) => { + const oauth = new OAuthPage(page); + const idpName = `basic-auth-delete-${testPrefix}`; + createdIDPs.push(idpName); + + await test.step('Navigate to OAuth settings and create IDP', async () => { + await oauth.navigateToOAuthSettings(); + await oauth.startIDPSetup(idpName, 'basicauth'); + await page.locator('#url').fill('https://example.com'); + await oauth.saveAndVerifyIDP(idpName, 'BasicAuth'); + }); + + await test.step('Remove IDP using kebab menu', async () => { + await oauth.removeIDP(idpName); + createdIDPs.pop(); + }); + + await test.step('Verify IDP was removed', async () => { + await oauth.verifyIDPNotExists(idpName); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/update-in-progress.spec.ts b/frontend/e2e/tests/console/cluster-settings/update-in-progress.spec.ts new file mode 100644 index 00000000000..455e1ca75e6 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/update-in-progress.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; +import { clusterVersionWithProgressing } from '../../../mocks/cluster-version'; + +const CLUSTER_VERSION_URL = '**/apis/config.openshift.io/v1/clusterversions/version'; + +test.describe('Cluster Settings while an update is in progress', { tag: ['@admin'] }, () => { + test('displays information about the update', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + + await test.step('Setup: Mock cluster version with update in progress', async () => { + // Mock the API response to return progressing cluster version + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithProgressing), + }); + }); + }); + + await test.step('Navigate to Cluster Settings', async () => { + await clusterSettings.navigateToDetails(); + }); + + await test.step('Verify update progress information is displayed', async () => { + // Verify "Last completed version" header + const currentVersionHeader = page.getByTestId('cv-current-version-header'); + await expect(currentVersionHeader).toContainText('Last completed version'); + + // Verify current version + const currentVersion = page.getByTestId('cv-current-version'); + await expect(currentVersion).toContainText('4.16.0'); + + // Verify update status showing progress + const updateStatus = page.getByTestId('cv-update-status-updating'); + await expect(updateStatus).toBeVisible(); + await expect(updateStatus).toContainText('Update to 4.16.2 in progress'); + + // Verify progress indicator exists + const updatesProgress = page.getByTestId('cv-updates-progress'); + await expect(updatesProgress).toBeVisible(); + + // Verify updates group (3 items expected) + const updatesGroup = page.getByTestId('cv-updates-group'); + await expect(updatesGroup).toHaveCount(3); + + // Verify Update button is hidden during update + const updateButton = page.getByTestId('cv-update-button'); + await expect(updateButton).not.toBeAttached(); + }); + + await test.step('Verify pause/resume update functionality', async () => { + const pauseButton = page.getByTestId('mcp-paused-button'); + + // Initially should show "Pause update" + await expect(pauseButton).toBeVisible(); + await expect(pauseButton).toContainText('Pause update'); + + // Click to pause + await pauseButton.click(); + + // Should now show "Resume update" + await expect(pauseButton).toContainText('Resume update'); + + // Click to resume + await pauseButton.click(); + + // Should be back to "Pause update" + await expect(pauseButton).toContainText('Pause update'); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/update-modal.spec.ts b/frontend/e2e/tests/console/cluster-settings/update-modal.spec.ts new file mode 100644 index 00000000000..6944ea27763 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/update-modal.spec.ts @@ -0,0 +1,226 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; +import { + clusterVersionWithAvailableUpdates, + clusterVersionWithAvailableAndConditionalUpdates, + clusterVersionWithConditionalUpdates, +} from '../../../mocks/cluster-version'; +import { + machineConfigPoolListWithPausedWorker, + machineConfigPoolListWithUnpausedWorker, +} from '../../../mocks/machine-config-pool'; + +const CLUSTER_VERSION_URL = '**/apis/config.openshift.io/v1/clusterversions/version'; +const MCP_LIST_URL = '**/apis/machineconfiguration.openshift.io/v1/machineconfigpools?*'; + +/** + * Stub MCP WebSocket to prevent watch from overwriting mocked GET responses + * This only stubs MCP watch WebSockets - all other WebSockets work normally + */ +async function stubMachineConfigPoolWebSocket(page) { + await page.addInitScript(() => { + const OriginalWebSocket = window.WebSocket; + + // Override WebSocket constructor + (window as any).WebSocket = function (url: string | URL, protocols?: string | string[]) { + const urlString = typeof url === 'string' ? url : url.toString(); + + // Only stub MCP list watch WebSocket - let all others through + if (urlString.includes('machineconfiguration.openshift.io/v1/machineconfigpools')) { + // Return a fake closed WebSocket that does nothing + const stub = { + close: () => {}, + send: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => true, + readyState: 3, // CLOSED + url: urlString, + protocol: '', + extensions: '', + bufferedAmount: 0, + binaryType: 'blob' as BinaryType, + onopen: null, + onerror: null, + onclose: null, + onmessage: null, + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3, + }; + return stub; + } + + // All other WebSockets use the original implementation + return new OriginalWebSocket(url, protocols); + }; + + // Copy static properties + (window as any).WebSocket.CONNECTING = OriginalWebSocket.CONNECTING; + (window as any).WebSocket.OPEN = OriginalWebSocket.OPEN; + (window as any).WebSocket.CLOSING = OriginalWebSocket.CLOSING; + (window as any).WebSocket.CLOSED = OriginalWebSocket.CLOSED; + }); +} + +test.describe('Cluster Settings cluster update modal', { tag: ['@admin'] }, () => { + test('changes based on the cluster', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + + await test.step('Scenario 1: With a paused Worker MCP', async () => { + // Setup mocks + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithAvailableUpdates), + }); + }); + await page.route(MCP_LIST_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(machineConfigPoolListWithPausedWorker), + }); + }); + + await stubMachineConfigPoolWebSocket(page); + await clusterSettings.navigateToDetails(); + + // Open update modal and dropdown + await clusterSettings.openUpdateModal(); + await clusterSettings.openUpdateDropdown(); + + // Verify switch is disabled when worker is paused + const updateSwitch = page + .getByTestId('update-cluster-modal') + .locator('[data-test="dropdown-with-switch-switch"]'); + await expect(updateSwitch).toBeVisible(); + await expect(updateSwitch).toBeDisabled(); + + // Select version 4.17.1 + const version4171 = page + .getByTestId('update-cluster-modal') + .locator('[data-test="dropdown-with-switch-menu-item-4.17.1"]'); + await expect(version4171).toBeVisible(); + await version4171.click(); + + // Verify paused nodes warning appears + const pausedWarning = page.getByTestId('update-cluster-modal-paused-nodes-warning'); + await expect(pausedWarning).toBeVisible(); + + // Verify partial update option appears + const partialUpdateRadio = page.getByTestId('update-cluster-modal-partial-update-radio'); + await expect(partialUpdateRadio).toBeVisible(); + await partialUpdateRadio.click(); + + // Verify worker checkbox is checked + const workerCheckbox = page.getByTestId('pause-mcp-checkbox-worker'); + await expect(workerCheckbox).toBeVisible(); + await expect(workerCheckbox).toBeChecked(); + + // Cancel the modal + const cancelButton = page.getByTestId('modal-cancel-action'); + await expect(cancelButton).toBeVisible(); + await cancelButton.click(); + }); + + await test.step('Scenario 2: With available and conditional updates', async () => { + // Update mocks + await page.unroute(CLUSTER_VERSION_URL); + await page.unroute(MCP_LIST_URL); + + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithAvailableAndConditionalUpdates), + }); + }); + await page.route(MCP_LIST_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(machineConfigPoolListWithUnpausedWorker), + }); + }); + + await stubMachineConfigPoolWebSocket(page); + await page.reload(); + await page.getByTestId('horizontal-link-Details').waitFor({ state: 'visible' }); + + // Open update modal and dropdown + await clusterSettings.openUpdateModal(); + await clusterSettings.openUpdateDropdown(); + + // Select version 4.17.1 (should be recommended) + const version4171 = page + .getByTestId('update-cluster-modal') + .locator('[data-test="dropdown-with-switch-menu-item-4.17.1"]'); + await expect(version4171).toBeVisible(); + await version4171.click(); + + // Should not show not-recommended alert for 4.17.1 + const notRecommendedAlert = page.getByTestId('update-cluster-modal-not-recommended-alert'); + await expect(notRecommendedAlert).not.toBeVisible(); + + // Cancel the modal + const cancelButton = page.getByTestId('modal-cancel-action'); + await cancelButton.click(); + }); + + await test.step('Scenario 3: With conditional updates only', async () => { + // Update mocks + await page.unroute(CLUSTER_VERSION_URL); + + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithConditionalUpdates), + }); + }); + + await stubMachineConfigPoolWebSocket(page); + await page.reload(); + await page.getByTestId('horizontal-link-Details').waitFor({ state: 'visible' }); + + // Verify not-recommended alert on main page + const mainPageAlert = page.getByTestId('cv-not-recommended-alert'); + await expect(mainPageAlert).toBeVisible(); + + // Open update modal and dropdown + await clusterSettings.openUpdateModal(); + await clusterSettings.openUpdateDropdown(); + + // Enable conditional updates switch + const updateSwitch = page + .getByTestId('update-cluster-modal') + .locator('[data-test="dropdown-with-switch-switch"]'); + await expect(updateSwitch).toBeVisible(); + await updateSwitch.click(); + + // Verify 4.17.1 is not available (only conditional updates) + const version4171 = page + .getByTestId('update-cluster-modal') + .locator('[data-test="dropdown-with-switch-menu-item-4.17.1"]'); + await expect(version4171).not.toBeAttached(); + + // Verify 4.16.4 (conditional) is available + const version4164 = page + .getByTestId('update-cluster-modal') + .locator('[data-test="dropdown-with-switch-menu-item-4.16.4"]'); + await expect(version4164).toBeVisible(); + await version4164.click(); + + // Should show not-recommended alert for conditional update + const notRecommendedAlert = page.getByTestId('update-cluster-modal-not-recommended-alert'); + await expect(notRecommendedAlert).toBeVisible(); + + // Cancel the modal + const cancelButton = page.getByTestId('modal-cancel-action'); + await cancelButton.click(); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/updates-graph.spec.ts b/frontend/e2e/tests/console/cluster-settings/updates-graph.spec.ts new file mode 100644 index 00000000000..11dbd68a243 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/updates-graph.spec.ts @@ -0,0 +1,163 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; + +const CLUSTER_VERSION_URL = '**/apis/config.openshift.io/v1/clusterversions/version'; + +// Create a mock with both desired channels and available updates +const clusterVersionWithGraphData = { + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterVersion', + metadata: { + creationTimestamp: '2024-01-04T05:14:57Z', + generation: 4, + name: 'version', + resourceVersion: '370626', + uid: '40b1ad1b-13d2-4c7c-932a-ce78c4447ed8', + }, + spec: { + channel: 'stable-4.16', + clusterID: '4976480a-15e1-4c94-bafe-aafb96bc0248', + upstream: 'https://openshift-release.apps.ci.l2s4.p1.openshiftapps.com/graph', + }, + status: { + availableUpdates: [ + { + image: + 'registry.ci.openshift.org/ocp/release@sha256:f31d5b0e23c8f978b57f5ef74c8811e7f87103187aa1895880f67eac4eb76f6d', + version: '4.16.1', + }, + { + image: + 'registry.ci.openshift.org/ocp/release@sha256:f31d5b0e23c8f978b57f5ef74c8811e7f87103187aa1895880f67eac4eb76f6e', + version: '4.16.2', + }, + { + image: + 'registry.ci.openshift.org/ocp/release@sha256:06cd433ae0036e3b79e2ecd08512abca540fbefe9805b225a6a9c33ca9456ad3', + version: '4.17.0', + }, + ], + conditions: [ + { + lastTransitionTime: '2024-01-04T05:37:10Z', + status: 'True', + type: 'RetrievedUpdates', + }, + { + lastTransitionTime: '2024-01-04T05:35:14Z', + message: 'Done applying 4.16.0', + status: 'True', + type: 'Available', + }, + { + lastTransitionTime: '2024-01-04T05:35:14Z', + status: 'False', + type: 'Failing', + }, + { + lastTransitionTime: '2024-01-04T05:35:14Z', + message: 'Cluster version is 4.16.0', + status: 'False', + type: 'Progressing', + }, + ], + desired: { + channels: [ + 'stable-4.16', + 'candidate-4.16', + 'fast-4.16', + 'stable-4.17', + 'candidate-4.17', + 'fast-4.17', + ], + image: + 'registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721', + version: '4.16.0', + }, + history: [ + { + completionTime: '2024-01-04T05:35:14Z', + image: + 'registry.ci.openshift.org/ocp/release@sha256:ff486203f5b065836105fcd56f29467229bfc6258e5d8ba7f479ac575d81c721', + startedTime: '2024-01-04T05:15:00Z', + state: 'Completed', + verified: false, + version: '4.16.0', + }, + ], + observedGeneration: 3, + versionHash: 'rqBs61ZyVwQ=', + }, +}; + +test.describe('Cluster Settings updates graph', { tag: ['@admin'] }, () => { + test('displays when an update is in progress', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + + await test.step('Setup: Mock cluster version with graph data', async () => { + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithGraphData), + }); + }); + }); + + await test.step('Navigate to Cluster Settings', async () => { + await clusterSettings.navigateToDetails(); + }); + + await test.step('Verify updates graph is displayed', async () => { + const updatesGraph = page.getByTestId('cv-updates-graph'); + await expect(updatesGraph).toBeVisible(); + }); + + await test.step('Verify channel sections are displayed', async () => { + const channels = page.getByTestId('cv-channel'); + await expect(channels).toHaveCount(2); + }); + + await test.step('Verify first channel (stable-4.16) details', async () => { + const firstChannel = page.getByTestId('cv-channel').first(); + const firstChannelName = page.getByTestId('cv-channel-name').first(); + + // Verify channel name + await expect(firstChannelName).toContainText('stable-4.16 channel'); + + // Verify version dots and versions + const versionDots = firstChannel.locator('[data-test="cv-channel-version-dot"]'); + await expect(versionDots).toHaveCount(2); + + const versions = firstChannel.locator('[data-test="cv-channel-version"]'); + await expect(versions).toHaveCount(2); + }); + + await test.step('Open and close "Other available paths" modal', async () => { + const firstChannel = page.getByTestId('cv-channel').first(); + const moreUpdatesButton = firstChannel.locator('[data-test="cv-more-updates-button"]'); + + // Click "More updates" button + await expect(moreUpdatesButton).toBeVisible(); + await moreUpdatesButton.click(); + + // Verify modal opens + const modalTitle = page.getByTestId('modal-title'); + await expect(modalTitle).toBeVisible(); + await expect(modalTitle).toContainText('Other available paths'); + + // Close modal + const closeButton = page.getByTestId('more-updates-modal-close-button'); + await expect(closeButton).toBeVisible(); + await closeButton.click(); + + // Verify modal is closed + await expect(modalTitle).not.toBeVisible(); + }); + + await test.step('Verify second channel (stable-4.17) name', async () => { + const secondChannelName = page.getByTestId('cv-channel-name').nth(1); + await expect(secondChannelName).toContainText('stable-4.17 channel'); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/upgradeable-false.spec.ts b/frontend/e2e/tests/console/cluster-settings/upgradeable-false.spec.ts new file mode 100644 index 00000000000..11b30534078 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/upgradeable-false.spec.ts @@ -0,0 +1,107 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; +import { clusterVersionWithUpgradeableFalse } from '../../../mocks/cluster-version'; + +const CLUSTER_VERSION_URL = '**/apis/config.openshift.io/v1/clusterversions/version'; + +test.describe('Cluster Settings when ClusterVersion Upgradeable=False', { tag: ['@admin'] }, () => { + test('displays alerts and badges for blocked updates', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + + await test.step('Setup: Mock cluster version with Upgradeable=False', async () => { + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithUpgradeableFalse), + }); + }); + }); + + await test.step('Navigate to Cluster Settings', async () => { + await clusterSettings.navigateToDetails(); + }); + + await test.step('Verify not-upgradeable alert is displayed', async () => { + const notUpgradeableAlert = page.getByTestId('cluster-settings-alerts-not-upgradeable'); + await expect(notUpgradeableAlert).toBeVisible(); + }); + + await test.step('Open and verify "Other available paths" modal', async () => { + const firstChannel = page.getByTestId('cv-channel').first(); + const moreUpdatesButton = firstChannel.locator('[data-test="cv-more-updates-button"]'); + + // Click "More updates" button + await expect(moreUpdatesButton).toBeVisible(); + await moreUpdatesButton.click(); + + // Verify modal contains not-upgradeable alert + const modal = page.getByTestId('more-updates-modal'); + await expect(modal).toBeVisible(); + + const modalNotUpgradeableAlert = modal.locator( + '[data-test="cluster-settings-alerts-not-upgradeable"]', + ); + await expect(modalNotUpgradeableAlert).toBeVisible(); + + // Verify modal contains blocked update badge + const blockedBadge = modal.locator('[data-test="cv-update-blocked"]'); + await expect(blockedBadge).toBeVisible(); + + // Close modal + const closeButton = page.getByTestId('more-updates-modal-close-button'); + await expect(closeButton).toBeVisible(); + await closeButton.click(); + + // Verify modal is closed + await expect(modal).not.toBeVisible(); + }); + + await test.step('Verify blocked version badge in channel', async () => { + const blockedVersionBadge = page.getByTestId('cv-channel-version-blocked'); + await expect(blockedVersionBadge).toBeVisible(); + }); + + await test.step('Click blocked version dot and verify blocked info', async () => { + const firstChannel = page.getByTestId('cv-channel').first(); + const blockedDot = firstChannel.locator('[data-test="cv-channel-version-dot-blocked"]'); + + await expect(blockedDot).toBeVisible(); + await blockedDot.click(); + + // Verify blocked info popover appears + const blockedInfo = page.getByTestId('cv-update-blocked'); + await expect(blockedInfo).toBeVisible(); + + const blockedDotInfo = page.getByTestId('cv-channel-version-dot-blocked-info'); + await expect(blockedDotInfo).toBeVisible(); + + // Close the popover by pressing Escape + await page.keyboard.press('Escape'); + await expect(blockedInfo).not.toBeVisible(); + }); + + await test.step('Open update modal and verify blocked updates in dropdown', async () => { + // Open update modal using page object method + await clusterSettings.openUpdateModal(); + + // Verify modal contains not-upgradeable alert + const modal = page.getByTestId('update-cluster-modal'); + await expect(modal).toBeVisible(); + + const modalNotUpgradeableAlert = modal.locator( + '[data-test="cluster-settings-alerts-not-upgradeable"]', + ); + await expect(modalNotUpgradeableAlert).toBeVisible(); + + // Open dropdown + const dropdownToggle = modal.locator('[data-test="dropdown-with-switch-toggle"]'); + await expect(dropdownToggle).toBeVisible(); + await dropdownToggle.click(); + + // Verify 2 blocked update badges in dropdown + const blockedBadges = page.getByTestId('cv-update-blocked'); + await expect(blockedBadges).toHaveCount(2); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/upstream-modal.spec.ts b/frontend/e2e/tests/console/cluster-settings/upstream-modal.spec.ts new file mode 100644 index 00000000000..a24bae167f5 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/upstream-modal.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; + +test.describe('Cluster Settings upstream configuration modal', { tag: ['@admin'] }, () => { + test('can be opened and closed', async ({ page }) => { + const clusterSettings = new ClusterSettingsPage(page); + + await test.step('Navigate to Cluster Settings', async () => { + await clusterSettings.navigateToDetails(); + }); + + await test.step('Click upstream URL to open modal', async () => { + const upstreamServerUrl = page.getByTestId('cv-upstream-server-url'); + + // Scroll to element and click + await upstreamServerUrl.scrollIntoViewIfNeeded(); + await expect(upstreamServerUrl).toBeVisible(); + await upstreamServerUrl.click(); + + // Verify modal opens + const modalTitle = clusterSettings.getModalTitle(); + await expect(modalTitle).toBeVisible(); + await expect(modalTitle).toContainText('Edit upstream configuration'); + }); + + await test.step('Close modal', async () => { + const cancelButton = page.getByTestId('modal-cancel-action'); + await expect(cancelButton).toBeVisible(); + await cancelButton.click(); + + // Verify modal is closed + const modalTitle = clusterSettings.getModalTitle(); + await expect(modalTitle).not.toBeVisible(); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/worker-mcp-paused.spec.ts b/frontend/e2e/tests/console/cluster-settings/worker-mcp-paused.spec.ts new file mode 100644 index 00000000000..e3318cce83b --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/worker-mcp-paused.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '../../../fixtures'; +import { ClusterSettingsPage } from '../../../pages/cluster-settings-page'; +import { clusterVersionWithAvailableUpdates } from '../../../mocks/cluster-version'; + +const CLUSTER_VERSION_URL = '**/apis/config.openshift.io/v1/clusterversions/version'; + +test.describe( + 'Cluster Settings when worker MachineConfigPool is paused', + { tag: ['@admin'] }, + () => { + test('displays an alert', async ({ page, k8sClient }) => { + const clusterSettings = new ClusterSettingsPage(page); + + await test.step('Setup: Pause worker MCP', async () => { + // Pause the worker MCP using JSON Patch format + try { + await k8sClient.customObjectsApi.patchClusterCustomObject({ + group: 'machineconfiguration.openshift.io', + version: 'v1', + plural: 'machineconfigpools', + name: 'worker', + body: [ + { + op: 'replace', + path: '/spec/paused', + value: true, + }, + ], + }); + } catch (error) { + // MCP may not exist on all clusters (e.g., non-OCP clusters) + console.warn('Failed to pause worker MCP:', error); + } + }); + + await test.step('Setup: Mock cluster version with available updates', async () => { + await page.route(CLUSTER_VERSION_URL, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(clusterVersionWithAvailableUpdates), + }); + }); + }); + + await test.step('Navigate to Cluster Settings', async () => { + await clusterSettings.navigateToDetails(); + }); + + await test.step('Verify paused nodes alert is displayed', async () => { + const pausedNodesAlert = page.getByTestId('cluster-settings-alerts-paused-nodes'); + await expect(pausedNodesAlert).toBeVisible(); + }); + + await test.step('Verify pause button is displayed', async () => { + const pauseButton = page.getByTestId('mcp-paused-button'); + await expect(pauseButton).toBeVisible(); + }); + + await test.step('Cleanup: Unpause worker MCP', async () => { + // Unpause the worker MCP using JSON Patch format + try { + await k8sClient.customObjectsApi.patchClusterCustomObject({ + group: 'machineconfiguration.openshift.io', + version: 'v1', + plural: 'machineconfigpools', + name: 'worker', + body: [ + { + op: 'replace', + path: '/spec/paused', + value: false, + }, + ], + }); + } catch (error) { + // MCP may not exist on all clusters + console.warn('Failed to unpause worker MCP:', error); + } + }); + }); + }, +); diff --git a/frontend/packages/console-app/src/components/oauth-config/IdentityProviders.tsx b/frontend/packages/console-app/src/components/oauth-config/IdentityProviders.tsx index 9ae86232d2e..9d3d6cadb13 100644 --- a/frontend/packages/console-app/src/components/oauth-config/IdentityProviders.tsx +++ b/frontend/packages/console-app/src/components/oauth-config/IdentityProviders.tsx @@ -46,16 +46,32 @@ export const IdentityProviders: FC = ({ identityProvider {_.map(identityProviders, (idp, index) => ( - + {idp.name} - + {idp.type} - + {idp.mappingMethod || 'claim'} - + = ({ obj }: { obj: OAuthK component="button" id={key} data-test-id={key} + data-test={key} onClick={(e) => navigate(`/settings/idp/${e.currentTarget.id}`)} > {getAddIDPItemLabels(value)} @@ -130,6 +131,7 @@ export const OAuthConfigDetails: FC = ({ obj }: { obj: OAuthK setIDPOpen(!isIDPOpen)} isExpanded={isIDPOpen} diff --git a/frontend/packages/console-app/src/components/tour/tour-context.ts b/frontend/packages/console-app/src/components/tour/tour-context.ts index 62ca051888b..6f298c7cdae 100644 --- a/frontend/packages/console-app/src/components/tour/tour-context.ts +++ b/frontend/packages/console-app/src/components/tour/tour-context.ts @@ -136,10 +136,13 @@ export const useTourValuesForContext = (): TourContextType => { (state) => getRequiredFlagsByTour(state, selectorSteps), isEqual, ); + const isIntegrationTest = useConsoleSelector( + (state: RootState) => getFlagsObject(state).INTEGRATION_TEST, + ); const [tourCompletionState, setTourCompletionState, loaded] = useTourStateForPerspective( activePerspective, ); - const completed = tourCompletionState?.completed; + const completed = tourCompletionState?.completed || isIntegrationTest; const onComplete = () => { if (completed === false) { setTourCompletionState(true); diff --git a/frontend/packages/console-shared/src/components/actions/menu/ActionMenuToggle.tsx b/frontend/packages/console-shared/src/components/actions/menu/ActionMenuToggle.tsx index f2892ebfbed..d139c075554 100644 --- a/frontend/packages/console-shared/src/components/actions/menu/ActionMenuToggle.tsx +++ b/frontend/packages/console-shared/src/components/actions/menu/ActionMenuToggle.tsx @@ -86,6 +86,7 @@ const ActionMenuToggle: FC = ({ aria-label={toggleLabel} aria-haspopup="true" data-test-id={isKebabVariant ? 'kebab-button' : 'actions-menu-button'} + data-test={isKebabVariant ? 'kebab-button' : 'actions-menu-button'} onClick={handleToggleClick} onFocus={onToggleHover} onMouseOver={onToggleHover} diff --git a/frontend/packages/integration-tests/tests/cluster-settings/channel-modal.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/channel-modal.cy.ts deleted file mode 100644 index 58da769551a..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/channel-modal.cy.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - clusterVersionWithDesiredChannels, - clusterVersionWithoutChannel, -} from '../../mocks/cluster-version'; -import { checkErrors } from '../../support'; -import { clusterSettings } from '../../views/cluster-settings'; - -const CHANNEL_CANDIDATE = 'candidate-4.16'; -const CHANNEL_STABLE = 'stable-4.16'; -const CLUSTER_VERSION_ALIAS = 'clusterVersion'; -const WAIT_OPTIONS = { timeout: 300000 }; - -describe('Cluster Settings channel modal', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - beforeEach(() => { - clusterSettings.detailsIsLoaded(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('changes based on cluster version', () => { - cy.log('when no channel is set'); - cy.intercept( - { - url: '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - times: 1, - }, - clusterVersionWithoutChannel, - ).as(CLUSTER_VERSION_ALIAS); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - cy.byLegacyTestID('current-channel-update-link') - .should('exist') - .and('contain.text', 'Not configured') - .click(); - cy.byLegacyTestID('modal-title').should('contain.text', 'Input channel'); - cy.byTestID('channel-modal-input').should('exist').clear().type(CHANNEL_CANDIDATE); - cy.byTestID('confirm-action').should('exist').click(); - cy.log('Reload to test changes'); - clusterSettings.detailsIsLoaded(); - cy.byLegacyTestID('current-channel-update-link') - .should('exist') - .and('contain.text', CHANNEL_CANDIDATE); - - cy.log('when and channel is set and channels are present'); - cy.intercept( - { - url: '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - times: 1, - }, - clusterVersionWithDesiredChannels, - ).as(CLUSTER_VERSION_ALIAS); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - cy.byLegacyTestID('current-channel-update-link') - .should('exist') - .and('contain.text', 'stable-4.16') - .click(); - cy.byLegacyTestID('modal-title').should('contain.text', 'Select channel'); - cy.byTestID('channel-modal') - .find('[data-test="console-select-menu-toggle"]') - .should('exist') - .click(); - cy.get(`[data-test-dropdown-menu="${CHANNEL_STABLE}"]`).should('exist').click(); - cy.byTestID('confirm-action').should('exist').click(); - cy.log('Reload to test changes'); - clusterSettings.detailsIsLoaded(); - cy.byLegacyTestID('current-channel-update-link') - .should('exist') - .and('contain.text', CHANNEL_STABLE); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/cluster-settings.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/cluster-settings.cy.ts deleted file mode 100644 index 16ff5469389..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/cluster-settings.cy.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { checkErrors } from '../../support'; -import { clusterSettings } from '../../views/cluster-settings'; -import { detailsPage } from '../../views/details-page'; - -describe('Cluster Settings', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - beforeEach(() => { - clusterSettings.detailsIsLoaded(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('displays page title, horizontal navigation tab headings and pages', () => { - cy.byLegacyTestID('cluster-settings-page-heading').should('contain.text', 'Cluster Settings'); - detailsPage.selectTab('Details'); - detailsPage.isLoaded(); - detailsPage.selectTab('ClusterOperators'); - detailsPage.isLoaded(); - detailsPage.selectTab('Configuration'); - detailsPage.isLoaded(); - }); - - it('displays Cluster Operators page and console Operator details page', () => { - detailsPage.selectTab('ClusterOperators'); - cy.byLegacyTestID('console').should('exist').click(); - detailsPage.titleShouldContain('console'); - detailsPage.selectTab('YAML'); - detailsPage.isLoaded(); - }); - - it('displays Configuration page and ClusterVersion configuration details page', () => { - detailsPage.selectTab('Configuration'); - cy.byLegacyTestID('ClusterVersion').should('exist').click(); - detailsPage.selectTab('YAML'); - detailsPage.isLoaded(); - }); - - it('displays Configuration page and ClusterVersion Edit ClusterVersion resource details page', () => { - detailsPage.selectTab('Configuration'); - detailsPage.isLoaded(); - cy.byTestActionID('ClusterVersion').within(() => { - cy.get('[data-test-id="kebab-button"]').click(); - }); - cy.byTestActionID('Edit ClusterVersion resource').click(); - detailsPage.titleShouldContain('version'); - }); - - it('displays Configuration page and ClusterVersion Explore Console API details page', () => { - detailsPage.selectTab('Configuration'); - detailsPage.isLoaded(); - cy.byTestActionID('ClusterVersion').within(() => { - cy.get('[data-test-id="kebab-button"]').click(); - }); - cy.byTestActionID('Explore ClusterVersion API').click(); - detailsPage.titleShouldContain('ClusterVersion'); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/managed-control-plane.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/managed-control-plane.cy.ts deleted file mode 100644 index cbb071ef724..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/managed-control-plane.cy.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { checkErrors } from '../../support'; -import { detailsPage } from '../../views/details-page'; - -describe('Cluster Settings when control plane is managed', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('displays an alert and hides elements', () => { - cy.visit('/settings/cluster', { - onLoad: (contentWindow) => { - contentWindow.SERVER_FLAGS.controlPlaneTopology = 'External'; - }, - }); - cy.byTestID('cluster-settings-alerts-hosted').should('exist'); - cy.byTestID('current-channel-update-link').should('not.exist'); - cy.byTestID('cv-update-button').should('not.exist'); - cy.byTestID('cv-upstream-server-url').should('not.exist'); - cy.byTestID('cv-autoscaler').should('not.exist'); - cy.contains('logged in as a temporary administrative user').should('not.exist'); - cy.contains('allow others to log in').should('not.exist'); - // check on Configuration page - detailsPage.selectTab('Configuration'); - cy.get('.loading-box__loaded').should('exist'); - const configName = [ - 'APIServer', - 'Authentication', - 'DNS', - 'FeatureGate', - 'Networking', - 'OAuth', - 'Proxy', - 'Scheduler', - ]; - configName.forEach(function (name) { - cy.get(`[href="/k8s/cluster/config.openshift.io~v1~${name}/cluster"]`).should('not.exist'); - }); - // check on Overview page - cy.clickNavLink(['Home', 'Overview']); - cy.byTestID('Control Plane').should('not.exist'); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/oauth.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/oauth.cy.ts deleted file mode 100644 index 39f730de448..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/oauth.cy.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { checkErrors, testName } from '../../support'; -import { oauth } from '../../views/oauth'; - -describe('OAuth', () => { - let originalOAuthConfig: any; - - before(() => { - cy.login(); - cy.exec('oc get oauths cluster -o json').then((result) => { - originalOAuthConfig = JSON.parse(result.stdout); - }); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - const idpJSON = JSON.stringify(originalOAuthConfig?.spec?.identityProviders) ?? '[]'; - cy.exec( - `oc patch oauths cluster --type json -p='[{ op: 'replace', path: '/spec/identityProviders', value: ${idpJSON}}]'`, - ); - }); - - // TODO: Add tests for HTPasswd and Request Header identity providers. - // These IDPs require file upload. - - it(`creates a 'basic-auth-${testName}' Basic Authentication IDP and shows the BasicAuth IDP on the OAuth settings page`, () => { - const idpName = `basic-auth-${testName}`; - oauth.idpSetup(idpName, 'basicauth'); - cy.get('#url').type('https://example.com'); - oauth.idpSaveAndVerify(idpName, 'BasicAuth'); - }); - - it('creates a GitHub IDP and displays it on the OAuth settings page', () => { - const idpName = `github-${testName}`; - oauth.idpSetup(idpName, 'github'); - cy.get('#client-id').type('my-client-id'); - cy.get('#client-secret').type('my-client-secret'); - cy.get('[data-test-list-input-for="Organization"]').type('my-organization'); - oauth.idpSaveAndVerify(idpName, 'GitHub'); - }); - - it('creates a GitLab IDP and displays on the OAuth settings page', () => { - const idpName = `gitlab-${testName}`; - oauth.idpSetup(idpName, 'gitlab'); - cy.get('#url').type('https://example.com'); - cy.get('#client-id').type('my-client-id'); - cy.get('#client-secret').type('my-client-secret'); - oauth.idpSaveAndVerify(idpName, 'GitLab'); - }); - - it('creates a Google IDP and displays it on the OAuth settings page', () => { - const idpName = `google-${testName}`; - oauth.idpSetup(idpName, 'google'); - cy.get('#client-id').type('my-client-id'); - cy.get('#client-secret').type('my-client-secret'); - cy.get('#hosted-domain').type('example.com'); - oauth.idpSaveAndVerify(idpName, 'Google'); - }); - - it('creates a Keystone IDP and displays it on the OAuth settings page', () => { - const idpName = `keystone-${testName}`; - oauth.idpSetup(idpName, 'keystone'); - cy.get('#domain-name').type('example.com'); - cy.get('#url').type('https://example.com'); - oauth.idpSaveAndVerify(idpName, 'Keystone'); - }); - - it('creates a LDAP IDP and displays it on the OAuth settings page', () => { - const idpName = `ldap-${testName}`; - oauth.idpSetup(idpName, 'ldap'); - cy.get('#url').type('ldap://ldap.example.com/o=Acme?cn?sub?(enabled=true)'); - oauth.idpSaveAndVerify(idpName, 'LDAP'); - }); - - it('creates a OpenID IDP and displays it on the OAuth settings page', () => { - const idpName = `oidc-${testName}`; - oauth.idpSetup(idpName, 'oidconnect'); - cy.get('#client-id').type('my-client-id'); - cy.get('#client-secret').type('my-client-secret'); - cy.get('#issuer').type('https://example.com'); - oauth.idpSaveAndVerify(idpName, 'OpenID'); - }); - - it(`removes the Basic Authentication IDP 'basic-auth-${testName}' in the list on the OAuth settings page`, () => { - const idpName = `basic-auth-${testName}`; - - // Open the kebab menu and remove the IDP - cy.get(`[data-test-idp-kebab-for="${idpName}"]`).find('[data-test-id="kebab-button"]').click(); - cy.get('[data-test-action="Remove identity provider"]').should('be.visible').click(); - cy.get('[data-test="confirm-action"]').click(); - - // Verify the IDP was successfully removed - cy.get(`[data-test-idp-kebab-for="${idpName}"]`).should('not.exist'); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/update-in-progress.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/update-in-progress.cy.ts deleted file mode 100644 index b2759df0782..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/update-in-progress.cy.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { clusterVersionWithProgressing } from '../../mocks/cluster-version'; -import { checkErrors } from '../../support'; -import { clusterSettings } from '../../views/cluster-settings'; - -const CLUSTER_VERSION_ALIAS = 'clusterVersion'; -const WAIT_OPTIONS = { timeout: 300000 }; - -describe('Cluster Settings while an update is in progress', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - beforeEach(() => { - clusterSettings.detailsIsLoaded(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('displays information about the update', () => { - cy.intercept( - '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - clusterVersionWithProgressing, - ).as(CLUSTER_VERSION_ALIAS); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - cy.byTestID('cv-current-version-header').should('contain.text', 'Last completed version'); - cy.byTestID('cv-current-version').should('contain.text', '4.16.0'); - cy.byTestID('cv-update-status-updating') - .should('exist') - .and('contain.text', 'Update to 4.16.2 in progress'); - cy.byTestID('cv-updates-progress').should('exist'); - cy.byTestID('cv-updates-group').should('have.length', 3); - cy.byTestID('mcp-paused-button').should('exist').and('contain.text', 'Pause update').click(); - cy.byTestID('mcp-paused-button').should('exist').and('contain.text', 'Resume update').click(); - cy.byTestID('mcp-paused-button').should('exist').and('contain.text', 'Pause update'); - clusterSettings.pauseMCP('false'); // ensure MCP is not paused - cy.byLegacyTestID('cv-update-button').should('not.exist'); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/update-modal.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/update-modal.cy.ts deleted file mode 100644 index d2954b01ecc..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/update-modal.cy.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { - clusterVersionWithAvailableUpdates, - clusterVersionWithAvailableAndConditionalUpdates, - clusterVersionWithConditionalUpdates, -} from '../../mocks/cluster-version'; -import { - machineConfigPoolListWithPausedWorker, - machineConfigPoolListWithUnpausedWorker, -} from '../../mocks/machine-config-pool'; -import { checkErrors } from '../../support'; -import { stubMachineConfigPoolWatchWebSocket } from '../../support/stub-machine-config-pool-watch-ws'; -import { clusterSettings } from '../../views/cluster-settings'; -import { isLocalDevEnvironment } from '../../views/common'; - -const CLUSTER_VERSION_ALIAS = 'clusterVersion'; -const WAIT_OPTIONS = { requestTimeout: 300000 }; -// List requests only (not .../machineconfigpools/). -const MCP_LIST_PATTERN = '**/machineconfiguration.openshift.io/v1/machineconfigpools?*'; -const visitClusterSettingsWithMcpStub = () => { - clusterSettings.detailsIsLoaded({ - onBeforeLoad: (win) => stubMachineConfigPoolWatchWebSocket(win), - }); -}; -const reloadClusterSettingsWithMcpStub = () => { - cy.reload({ - onBeforeLoad: (win) => stubMachineConfigPoolWatchWebSocket(win), - }); - cy.byLegacyTestID('horizontal-link-Details').should('exist'); -}; - -describe('Cluster Settings cluster update modal', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - beforeEach(() => { - cy.intercept( - '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - clusterVersionWithAvailableUpdates, - ).as(CLUSTER_VERSION_ALIAS); - cy.intercept('GET', MCP_LIST_PATTERN, { - body: machineConfigPoolListWithPausedWorker, - }); - visitClusterSettingsWithMcpStub(); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - }); - - afterEach(() => { - checkErrors(); - }); - - it('changes based on the cluster', () => { - cy.log('with a paused Worker MCP'); - clusterSettings.pauseMCP(); - clusterSettings.openUpdateModalAndOpenDropdown(); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-switch"]') - .should('exist') - .and('have.attr', 'disabled'); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.17.1"]') - .should('exist'); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.17.1"]') - .click(); - cy.byTestID('update-cluster-modal-paused-nodes-warning').should('exist'); - cy.byTestID('update-cluster-modal-partial-update-radio').should('exist'); - cy.byTestID('update-cluster-modal-partial-update-radio').click(); - cy.byTestID('pause-mcp-checkbox-worker').should('exist').and('have.attr', 'checked'); - cy.byLegacyTestID('modal-cancel-action').should('exist'); - cy.byLegacyTestID('modal-cancel-action').click(); - clusterSettings.pauseMCP('false'); - - cy.log('with available and conditional updates'); - cy.intercept( - '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - clusterVersionWithAvailableAndConditionalUpdates, - ).as(CLUSTER_VERSION_ALIAS); - cy.intercept('GET', MCP_LIST_PATTERN, { - body: machineConfigPoolListWithUnpausedWorker, - }); - reloadClusterSettingsWithMcpStub(); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - clusterSettings.openUpdateModalAndOpenDropdown(); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.17.1"]') - .should('exist'); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.17.1"]') - .click(); - cy.byTestID('update-cluster-modal-not-recommended-alert').should('not.exist'); - // Only run locally as this is flaking in CI - if (isLocalDevEnvironment) { - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-toggle"]') - .should('exist'); - cy.byTestID('update-cluster-modal').find('[data-test="dropdown-with-switch-toggle"]').click(); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-switch"]') - .should('exist'); - cy.byTestID('update-cluster-modal').find('[data-test="dropdown-with-switch-switch"]').click(); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.16.4"]') - .should('exist'); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.16.4"]') - .click(); - cy.byTestID('update-cluster-modal-not-recommended-alert').should('exist'); - } - cy.byLegacyTestID('modal-cancel-action').should('exist'); - cy.byLegacyTestID('modal-cancel-action').click(); - - cy.log('with conditional updates'); - cy.intercept( - '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - clusterVersionWithConditionalUpdates, - ).as(CLUSTER_VERSION_ALIAS); - cy.intercept('GET', MCP_LIST_PATTERN, { - body: machineConfigPoolListWithUnpausedWorker, - }); - reloadClusterSettingsWithMcpStub(); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - cy.byTestID('cv-not-recommended-alert').should('exist'); - clusterSettings.openUpdateModalAndOpenDropdown(); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-switch"]') - .should('exist'); - cy.byTestID('update-cluster-modal').find('[data-test="dropdown-with-switch-switch"]').click(); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.17.1"]') - .should('not.exist'); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.16.4"]') - .should('exist'); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-menu-item-4.16.4"]') - .click(); - cy.byTestID('update-cluster-modal-not-recommended-alert').should('exist'); - cy.byLegacyTestID('modal-cancel-action').should('exist'); - cy.byLegacyTestID('modal-cancel-action').click(); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/updates-graph.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/updates-graph.cy.ts deleted file mode 100644 index 128f9637a2d..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/updates-graph.cy.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { clusterVersionWithDesiredChannels } from '../../mocks/cluster-version'; -import { checkErrors } from '../../support'; -import { clusterSettings } from '../../views/cluster-settings'; - -const CLUSTER_VERSION_ALIAS = 'clusterVersion'; -const WAIT_OPTIONS = { timeout: 300000 }; - -describe('Cluster Settings updates graph', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - beforeEach(() => { - clusterSettings.detailsIsLoaded(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('displays when an update is in progress', () => { - cy.intercept( - '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - clusterVersionWithDesiredChannels, - ).as(CLUSTER_VERSION_ALIAS); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - cy.byTestID('cv-updates-graph').should('exist'); - cy.byTestID('cv-channel').should('have.length', 2); - cy.byTestID('cv-channel-name').first().should('contain.text', 'stable-4.16 channel'); - cy.byTestID('cv-channel') - .first() - .find('[data-test="cv-channel-version-dot"]') - .should('have.length', 2); - cy.byTestID('cv-channel') - .first() - .find('[data-test="cv-channel-version"]') - .should('have.length', 2); - cy.byTestID('cv-channel') - .first() - .find('[data-test="cv-more-updates-button"]') - .should('exist') - .click(); - cy.byLegacyTestID('modal-title').should('contain.text', 'Other available paths'); - cy.byTestID('more-updates-modal-close-button').should('exist').click(); - cy.byTestID('cv-channel-name').eq(1).should('contain.text', 'stable-4.17 channel'); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/upgradeable-false.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/upgradeable-false.cy.ts deleted file mode 100644 index a426120d7c7..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/upgradeable-false.cy.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { clusterVersionWithUpgradeableFalse } from '../../mocks/cluster-version'; -import { checkErrors } from '../../support'; -import { clusterSettings } from '../../views/cluster-settings'; - -const CLUSTER_VERSION_ALIAS = 'clusterVersion'; -const WAIT_OPTIONS = { timeout: 300000 }; - -describe('Cluster Settings when ClusterVersion Upgradeable=False', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - beforeEach(() => { - clusterSettings.detailsIsLoaded(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('displays alerts and badges', () => { - cy.intercept( - '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - clusterVersionWithUpgradeableFalse, - ).as(CLUSTER_VERSION_ALIAS); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - cy.byTestID('cluster-settings-alerts-not-upgradeable').should('exist'); - cy.byTestID('cv-more-updates-button').should('exist').click(); - cy.byTestID('more-updates-modal') - .find('[data-test="cluster-settings-alerts-not-upgradeable"]') - .should('exist'); - cy.byTestID('more-updates-modal').find('[data-test="cv-update-blocked"]').should('exist'); - cy.byTestID('more-updates-modal-close-button').should('exist').click(); - cy.byTestID('cv-channel-version-blocked').should('exist'); - cy.byTestID('cv-channel') - .first() - .find('[data-test="cv-channel-version-dot-blocked"]') - .should('exist') - .click(); - cy.byTestID('cv-update-blocked').should('exist'); - cy.byTestID('cv-channel-version-dot-blocked-info').should('exist'); - cy.byLegacyTestID('cv-update-button').should('exist').click({ force: true }); // force since popover is covering button - cy.byTestID('update-cluster-modal') - .find('[data-test="cluster-settings-alerts-not-upgradeable"]') - .should('exist'); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-toggle"]') - .should('exist') - .click(); - cy.byTestID('cv-update-blocked').should('have.length', 2); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/upstream-modal.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/upstream-modal.cy.ts deleted file mode 100644 index 31018a88f85..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/upstream-modal.cy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { checkErrors } from '../../support'; -import { clusterSettings } from '../../views/cluster-settings'; - -describe('Cluster Settings upstream configuration modal', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - beforeEach(() => { - clusterSettings.detailsIsLoaded(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('can be opened and closed', () => { - cy.byLegacyTestID('cv-upstream-server-url').should('be.visible').click(); - cy.byLegacyTestID('modal-title').should('contain.text', 'Edit upstream configuration'); - cy.byLegacyTestID('modal-cancel-action').should('be.visible').click(); - }); -}); diff --git a/frontend/packages/integration-tests/tests/cluster-settings/worker-mcp-paused.cy.ts b/frontend/packages/integration-tests/tests/cluster-settings/worker-mcp-paused.cy.ts deleted file mode 100644 index b3567cf68ed..00000000000 --- a/frontend/packages/integration-tests/tests/cluster-settings/worker-mcp-paused.cy.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { clusterVersionWithAvailableUpdates } from '../../mocks/cluster-version'; -import { checkErrors } from '../../support'; -import { clusterSettings } from '../../views/cluster-settings'; - -const CLUSTER_VERSION_ALIAS = 'clusterVersion'; -const WAIT_OPTIONS = { timeout: 300000 }; - -describe('Cluster Settings when worker MachineConfigPool is paused', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - beforeEach(() => { - clusterSettings.detailsIsLoaded(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('displays an alert', () => { - clusterSettings.pauseMCP(); - cy.intercept( - '/api/kubernetes/apis/config.openshift.io/v1/clusterversions/version', - clusterVersionWithAvailableUpdates, - ).as(CLUSTER_VERSION_ALIAS); - cy.wait(`@${CLUSTER_VERSION_ALIAS}`, WAIT_OPTIONS); - cy.byTestID('cluster-settings-alerts-paused-nodes').should('exist'); - cy.byTestID('mcp-paused-button').should('exist'); - clusterSettings.pauseMCP('false'); - }); -}); diff --git a/frontend/packages/integration-tests/views/cluster-settings.ts b/frontend/packages/integration-tests/views/cluster-settings.ts deleted file mode 100644 index 3fd59e104c3..00000000000 --- a/frontend/packages/integration-tests/views/cluster-settings.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const clusterSettings = { - detailsIsLoaded: (visitOptions?: Partial) => { - cy.visit('/settings/cluster', visitOptions); - cy.byLegacyTestID('horizontal-link-Details').should('exist'); // wait for page to load - }, - pauseMCP: (value: 'true' | 'false' = 'true') => { - cy.exec( - `oc patch mcp/worker --patch '{ "spec": { "paused": ${value} } }' --type=merge`, - // MCO API is absent on non-OCP clusters and some CI profiles; tests still assert UI when MCP exists. - { failOnNonZeroExit: false }, - ); - }, - openUpdateModalAndOpenDropdown: () => { - cy.byLegacyTestID('cv-update-button').should('exist').click(); - cy.byLegacyTestID('modal-title').should('contain.text', 'Update cluster'); - cy.byTestID('update-cluster-modal').should('exist').and('be.visible'); - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-toggle"]') - .should('exist') - .and('be.visible') - .and('not.be.disabled') - .and('not.have.attr', 'aria-busy', 'true'); - // Query element again before clicking to ensure it's still in the DOM - cy.byTestID('update-cluster-modal') - .find('[data-test="dropdown-with-switch-toggle"]') - .should('be.visible') - .and('not.be.disabled'); - cy.byTestID('update-cluster-modal').find('[data-test="dropdown-with-switch-toggle"]').click(); - }, -}; diff --git a/frontend/packages/integration-tests/views/oauth.ts b/frontend/packages/integration-tests/views/oauth.ts deleted file mode 100644 index 3874e6b6667..00000000000 --- a/frontend/packages/integration-tests/views/oauth.ts +++ /dev/null @@ -1,20 +0,0 @@ -const idpNameInput = '#idp-name'; -const oauthSettingsURL = '/k8s/cluster/config.openshift.io~v1~OAuth/cluster'; - -export const oauth = { - idpSetup: (idpName: string, idpID: string) => { - cy.visit(oauthSettingsURL); - cy.byLegacyTestID('dropdown-button').click(); - cy.byLegacyTestID(idpID).click(); - cy.get(idpNameInput).clear(); - cy.get(idpNameInput).type(idpName); - }, - idpSaveAndVerify: (idpName: string, idpType: string) => { - cy.byLegacyTestID('add-idp').click(); - cy.byTestID('alert-error').should('not.exist'); - cy.url().should('include', oauthSettingsURL); - cy.get(`[data-test-idp-name="${idpName}"]`).should('have.text', idpName); - cy.get(`[data-test-idp-type-for="${idpName}"]`).should('have.text', idpType); - cy.get(`[data-test-idp-mapping-for="${idpName}"]`).should('have.text', 'claim'); - }, -}; diff --git a/frontend/public/components/cluster-settings/basicauth-idp-form.tsx b/frontend/public/components/cluster-settings/basicauth-idp-form.tsx index fab0c23b846..4a1e17a64db 100644 --- a/frontend/public/components/cluster-settings/basicauth-idp-form.tsx +++ b/frontend/public/components/cluster-settings/basicauth-idp-form.tsx @@ -229,7 +229,7 @@ export const AddBasicAuthPage: FC = () => { - @@ -1241,7 +1243,16 @@ export const ClusterSettingsPage: FC = () => { return ( - {title}} /> + + {title} + + } + /> ); diff --git a/frontend/public/components/cluster-settings/github-idp-form.tsx b/frontend/public/components/cluster-settings/github-idp-form.tsx index 9c6bd2abca4..3adbd3f667e 100644 --- a/frontend/public/components/cluster-settings/github-idp-form.tsx +++ b/frontend/public/components/cluster-settings/github-idp-form.tsx @@ -261,7 +261,7 @@ export const AddGitHubPage = () => { /> - - diff --git a/frontend/public/components/modals/configure-cluster-upstream-modal.tsx b/frontend/public/components/modals/configure-cluster-upstream-modal.tsx index 45c85b06eca..fd1bee5a373 100644 --- a/frontend/public/components/modals/configure-cluster-upstream-modal.tsx +++ b/frontend/public/components/modals/configure-cluster-upstream-modal.tsx @@ -76,6 +76,7 @@ export const ConfigureClusterUpstreamModal = (props: ConfigureClusterUpstreamMod @@ -183,7 +184,12 @@ export const ConfigureClusterUpstreamModal = (props: ConfigureClusterUpstreamMod > {t('public~Save')} - diff --git a/frontend/public/components/utils/details-page.tsx b/frontend/public/components/utils/details-page.tsx index f5729071f0c..4dcd7573f3a 100644 --- a/frontend/public/components/utils/details-page.tsx +++ b/frontend/public/components/utils/details-page.tsx @@ -215,6 +215,7 @@ export const UpstreamConfigDetailsItem: FC = ({ type="button" isInline data-test-id="cv-upstream-server-url" + data-test="cv-upstream-server-url" onClick={(e) => { e.preventDefault(); e.stopPropagation(); diff --git a/frontend/public/components/utils/headings.tsx b/frontend/public/components/utils/headings.tsx index f51f5d404e2..12609209342 100644 --- a/frontend/public/components/utils/headings.tsx +++ b/frontend/public/components/utils/headings.tsx @@ -172,7 +172,11 @@ export const ConnectedPageHeading = connectToModel( (kind || resourceTitle || resourceStatus) && (
{kind && }{' '} - + {resourceTitle} {data?.metadata?.namespace && data?.metadata?.ownerReferences?.length && ( diff --git a/frontend/public/components/utils/horizontal-nav.tsx b/frontend/public/components/utils/horizontal-nav.tsx index 772a591b3df..8f2dd0ae076 100644 --- a/frontend/public/components/utils/horizontal-nav.tsx +++ b/frontend/public/components/utils/horizontal-nav.tsx @@ -205,6 +205,7 @@ export const NavBar: FC = ({ pages }) => { navigate(to); }} data-test-id={`horizontal-link-${nameKey ? nameKey.split('~')[1] : name}`} + data-test={`horizontal-link-${nameKey ? nameKey.split('~')[1] : name}`} title={{nameKey ? t(nameKey) : name}} aria-controls={undefined} // there is no corresponding tab content to control, so this ID is invalid {...(badge ? { actions: badge } : {})} diff --git a/frontend/public/components/utils/kebab.tsx b/frontend/public/components/utils/kebab.tsx index deff1bbd6a5..8fcad338f5b 100644 --- a/frontend/public/components/utils/kebab.tsx +++ b/frontend/public/components/utils/kebab.tsx @@ -214,6 +214,7 @@ export const Kebab: KebabComponent = (props) => { { required={missingValues && i === 0} aria-describedby={helpText ? this.helpID : undefined} data-test-list-input-for={label} + data-test={`list-input-${label}`} aria-label={label} />