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/e2e/clients/kubernetes-client.ts b/frontend/e2e/clients/kubernetes-client.ts index a9255d43cf4..cd15c1643b5 100644 --- a/frontend/e2e/clients/kubernetes-client.ts +++ b/frontend/e2e/clients/kubernetes-client.ts @@ -388,6 +388,15 @@ export default class KubernetesClient { } } + async patchSecret(name: string, namespace: string, patch: object[]): Promise { + await this.k8sApi.patchNamespacedSecret({ + name, + namespace, + body: patch, + contentType: k8s.PatchStrategy.JsonPatch, + } as any); + } + async deleteSecret(name: string, namespace: string): Promise { try { await this.k8sApi.deleteNamespacedSecret({ name, namespace }); @@ -471,4 +480,29 @@ export default class KubernetesClient { const response = await this.k8sApi.listNamespacedPod({ namespace }); return response.items || []; } + + async createProject(name: string, labels?: Record): Promise { + await this.createNamespace(name, labels); + } + + async deleteProject(name: string): Promise { + await this.deleteNamespace(name); + } + + async createPod(pod: k8s.V1Pod): Promise { + await this.k8sApi.createNamespacedPod({ + namespace: pod.metadata!.namespace!, + body: pod, + }); + } + + async deletePod(name: string, namespace: string): Promise { + try { + await this.k8sApi.deleteNamespacedPod({ name, namespace }); + } catch (err) { + if (!isNotFound(err)) { + throw err; + } + } + } } 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/alertmanager-page.ts b/frontend/e2e/pages/alertmanager-page.ts new file mode 100644 index 00000000000..1cd92e4a725 --- /dev/null +++ b/frontend/e2e/pages/alertmanager-page.ts @@ -0,0 +1,156 @@ +import { expect } from '@playwright/test'; +import yaml from 'js-yaml'; + +import BasePage from './base-page'; + +type AlertmanagerConfig = { + global?: Record; + receivers?: AlertmanagerReceiver[]; + route?: any; + inhibit_rules?: any[]; +}; + +type AlertmanagerReceiver = { + name: string; + [key: string]: any; +}; + +export class AlertmanagerPage extends BasePage { + private readonly createReceiverButton = this.page.getByTestId('create-receiver'); + private readonly receiverNameInput = this.page.getByTestId('receiver-name'); + private readonly receiverTypeDropdown = this.page.getByTestId('receiver-type'); + private readonly saveChangesButton = this.page.getByTestId('save-changes'); + private readonly advancedConfigButton = this.page.getByTestId('advanced-configuration'); + + /** + * Navigate to the Alertmanager configuration page + */ + async navigateToAlertmanager(): Promise { + await this.goTo('/settings/cluster/alertmanagerconfig'); + await this.createReceiverButton.waitFor({ state: 'visible' }); + } + + /** + * Navigate to the Alertmanager YAML editor page + */ + async navigateToYAMLPage(): Promise { + await this.goTo('/settings/cluster/alertmanageryaml'); + // Wait for editor toolbar to load (indicates editor is ready) + await this.page.getByRole('button', { name: 'Copy code to clipboard' }).waitFor(); + } + + /** + * Navigate to the edit page for a specific receiver + */ + async navigateToEditReceiver(receiverName: string): Promise { + await this.goTo(`/settings/cluster/alertmanagerconfig/receivers/${receiverName}/edit`); + await this.saveChangesButton.waitFor({ state: 'visible' }); + } + + /** + * Start creating a new receiver + * @param receiverName - Name for the receiver + * @param receiverTypeConfig - The receiver type config name (e.g. 'email_configs') + */ + async createReceiver(receiverName: string, receiverTypeConfig: string): Promise { + await this.robustClick(this.createReceiverButton); + await this.receiverNameInput.fill(receiverName); + + // Open receiver type dropdown and select + await this.robustClick(this.receiverTypeDropdown); + const typeOption = this.page.getByTestId(`receiver-type-${receiverTypeConfig}`); + await this.robustClick(typeOption); + } + + /** + * Click the save changes button and wait for changes to rollout + */ + async save(): Promise { + await expect(this.saveChangesButton).toBeEnabled(); + await this.robustClick(this.saveChangesButton); + // Wait for changes to propagate to alertmanager pods + await this.page.waitForTimeout(10000); + } + + /** + * Expand the advanced configuration section + */ + async showAdvancedConfiguration(): Promise { + const button = this.advancedConfigButton.locator('button'); + await this.robustClick(button); + } + + /** + * Get the YAML editor content + * @returns The YAML content as a string + */ + async getYAMLContent(): Promise { + // Get content from Monaco editor + const content = await this.page.evaluate(() => { + const monacoEditor = (window as any).monaco?.editor?.getModels()?.[0]; + return monacoEditor?.getValue() || ''; + }); + + return content; + } + + /** + * Set the YAML editor content + */ + async setYAMLContent(content: string): Promise { + await this.page.evaluate((text) => { + const monacoEditor = (window as any).monaco?.editor?.getModels()?.[0]; + monacoEditor?.setValue(text); + }, content); + } + + /** + * Validate that a receiver appears in the list with expected cells + */ + async validateReceiverInList(receiverName: string): Promise { + // Navigate to list page first + await this.navigateToAlertmanager(); + + // Check receiver row exists + const receiverRow = this.page.getByRole('row', { name: new RegExp(receiverName) }); + await expect(receiverRow).toBeVisible(); + + // Check that integration type cell is visible + const integrationTypeCell = this.page.getByTestId( + `data-view-cell-${receiverName}-integration-types`, + ); + await expect(integrationTypeCell).toBeVisible(); + + // Check that routing labels cell is visible + const routingLabelsCell = this.page.getByTestId( + `data-view-cell-${receiverName}-routing-labels`, + ); + await expect(routingLabelsCell).toBeVisible(); + } +} + +/** + * Parse YAML content and extract global config and receiver config + * @param receiverName - Name of the receiver to find + * @param configName - Config type name (e.g. 'email_configs') + * @param yamlContent - The YAML content string + * @returns Object with globals and receiverConfig + */ +export function getGlobalsAndReceiverConfig( + receiverName: string, + configName: string, + yamlContent: string, +): { + globals: any; + receiverConfig: any; +} { + const config: AlertmanagerConfig = yaml.load(yamlContent) as AlertmanagerConfig; + const receiver: AlertmanagerReceiver | undefined = config.receivers?.find( + (r) => r.name === receiverName, + ); + + return { + globals: config.global || {}, + receiverConfig: receiver?.[configName]?.[0] || {}, + }; +} 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/navigation.ts b/frontend/e2e/pages/navigation.ts new file mode 100644 index 00000000000..2f7689aa257 --- /dev/null +++ b/frontend/e2e/pages/navigation.ts @@ -0,0 +1,65 @@ +import { Page } from '@playwright/test'; + +/** + * Helper class for navigating using the primary navigation menu + */ +export class Navigation { + constructor(private page: Page) {} + + /** + * Navigate using the primary nav by expanding a nav section and clicking a link + * @param section - The nav section to expand (e.g., "Administration", "Workloads") + * @param link - The link to click within that section (e.g., "CustomResourceDefinitions", "Pods") + */ + async navigateViaNav(section: string, link: string): Promise { + // Navigate to home first to ensure app is loaded + await this.page.goto('/'); + await this.page.waitForLoadState('networkidle'); + + await this.page.getByRole('button', { name: section }).click(); + await this.page.getByRole('link', { name: link }).click(); + await this.page.waitForLoadState('networkidle'); + } + + /** + * Navigate to CustomResourceDefinitions via Administration nav + */ + async navigateToCRDs(): Promise { + await this.navigateViaNav('Administration', 'CustomResourceDefinitions'); + } + + /** + * Navigate to a specific page via Administration nav + */ + async navigateToAdministration(link: string): Promise { + await this.navigateViaNav('Administration', link); + } + + /** + * Navigate to a specific page via Workloads nav + */ + async navigateToWorkloads(link: string): Promise { + await this.navigateViaNav('Workloads', link); + } + + /** + * Navigate to a specific page via Compute nav + */ + async navigateToCompute(link: string): Promise { + await this.navigateViaNav('Compute', link); + } + + /** + * Navigate to a specific page via Storage nav + */ + async navigateToStorage(link: string): Promise { + await this.navigateViaNav('Storage', link); + } + + /** + * Navigate to a specific page via User Management nav + */ + async navigateToUserManagement(link: string): Promise { + await this.navigateViaNav('User Management', link); + } +} 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/alertmanager/alertmanager-test-utils.ts b/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager-test-utils.ts new file mode 100644 index 00000000000..79a37576913 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager-test-utils.ts @@ -0,0 +1,47 @@ +import KubernetesClient from '../../../../clients/kubernetes-client'; + +export const DEFAULT_ALERTMANAGER_YAML = `global: + resolve_timeout: 5m +inhibit_rules: +- equal: + - namespace + - alertname + source_match: + severity: critical + target_match_re: + severity: warning|info +- equal: + - namespace + - alertname + source_match: + severity: warning + target_match_re: + severity: info +receivers: +- name: Default +- name: Watchdog +- name: Critical +route: + group_by: + - namespace + group_interval: 5m + group_wait: 30s + receiver: Default + repeat_interval: 12h + routes: + - matchers: + - alertname = Watchdog + receiver: Watchdog + - matchers: + - severity = critical + receiver: Critical`; + +export async function resetAlertmanagerConfig(k8sClient: KubernetesClient): Promise { + await k8sClient.patchSecret('alertmanager-main', 'openshift-monitoring', [ + { + op: 'replace', + path: '/data/alertmanager.yaml', + value: Buffer.from(DEFAULT_ALERTMANAGER_YAML).toString('base64'), + }, + ]); +} diff --git a/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager.spec.ts b/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager.spec.ts new file mode 100644 index 00000000000..fa26f363e6b --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager.spec.ts @@ -0,0 +1,255 @@ +import { test, expect } from '../../../../fixtures'; +import jsYaml from 'js-yaml'; +import { AlertmanagerPage } from '../../../../pages/alertmanager-page'; +import KubernetesClient from '../../../../clients/kubernetes-client'; +import { resetAlertmanagerConfig } from './alertmanager-test-utils'; + +type AlertmanagerConfig = { + global?: Record; + receivers?: AlertmanagerReceiver[]; + route: { + routes?: AlertmanagerRoute[]; + [key: string]: any; + }; + inhibit_rules?: any[]; +}; + +type AlertmanagerReceiver = { + name: string; + [key: string]: any; +}; + +type AlertmanagerRoute = { + receiver?: string; + matchers?: string[]; + [key: string]: any; +}; + +test.describe.configure({ mode: 'serial' }); + +test.describe('Alertmanager', { tag: ['@admin'] }, () => { + let alertmanager: AlertmanagerPage; + let k8sClient: KubernetesClient; + + test.beforeEach(async ({ page, k8sClient: client }) => { + alertmanager = new AlertmanagerPage(page); + k8sClient = client; + }); + + test.afterEach(async () => { + await resetAlertmanagerConfig(k8sClient); + }); + + test('displays the Alertmanager Configuration Details page', async ({ page }) => { + await page.goto('/settings/cluster'); + await page.getByRole('tab', { name: 'Configuration' }).click(); + await page.getByTestId('Alertmanager').click(); + await expect(page.getByRole('heading', { name: 'Alert routing' })).toBeVisible(); + }); + + test('launches Alert Routing modal, edits and saves correctly', async ({ page }) => { + await alertmanager.navigateToAlertmanager(); + + await page.getByTestId('edit-alert-routing-btn').click(); + + // Edit routing values (using legacy test IDs) + await page.locator('[data-test-id="input-group-by"]').fill(', cluster'); + await page.locator('[data-test-id="input-group-wait"]').clear(); + await page.locator('[data-test-id="input-group-wait"]').fill('60s'); + await page.locator('[data-test-id="input-group-interval"]').clear(); + await page.locator('[data-test-id="input-group-interval"]').fill('10m'); + await page.locator('[data-test-id="input-repeat-interval"]').clear(); + await page.locator('[data-test-id="input-repeat-interval"]').fill('24h'); + + await page.getByTestId('confirm-action').click(); + + // Verify values updated + await expect(page.getByTestId('group_by_value')).toContainText(', cluster'); + await expect(page.getByTestId('group_wait_value')).toContainText('60s'); + await expect(page.getByTestId('group_interval_value')).toContainText('10m'); + await expect(page.getByTestId('repeat_interval_value')).toContainText('24h'); + }); + + test('displays the Alertmanager YAML page and saves Alertmanager YAML', async ({ page }) => { + await alertmanager.navigateToYAMLPage(); + + // Verify no success alert initially + await expect(page.getByTestId('alert-success')).not.toBeVisible(); + + // Click save + await page.getByTestId('save-changes').click(); + + // Verify success alert appears + await expect(page.getByTestId('alert-success')).toBeVisible(); + }); + + test('creates and deletes a receiver', async ({ page }) => { + const receiverName = `WebhookReceiver-${Date.now()}`; + const receiverType = 'webhook'; + const configName = `${receiverType}_configs`; + const label = 'severity = warning'; + const webhookURL = 'http://mywebhookurl'; + + await test.step('Create Webhook Receiver', async () => { + await alertmanager.navigateToAlertmanager(); + await alertmanager.createReceiver(receiverName, configName); + + await alertmanager.showAdvancedConfiguration(); + await expect(page.getByTestId('send-resolved-alerts')).toBeChecked(); + + await page.getByTestId('webhook-url').fill(webhookURL); + await page.getByTestId('label-0').fill(label); + + await alertmanager.save(); + }); + + await test.step('Verify receiver was created', async () => { + await alertmanager.validateReceiverInList(receiverName); + }); + + await test.step('Delete receiver', async () => { + const receiverRow = page.getByRole('row', { name: new RegExp(receiverName) }); + await receiverRow.getByRole('button', { name: 'Actions' }).click(); + await page.getByRole('menuitem', { name: 'Delete Receiver' }).click(); + + // Confirm deletion in modal + const modal = page.getByRole('dialog', { name: /Delete Receiver/ }); + await expect(modal).toBeVisible(); + await modal.getByRole('button', { name: 'Delete Receiver' }).click(); + + // Verify receiver was deleted + await expect(receiverRow).not.toBeVisible(); + }); + }); + + test('prevents deletion and form edit of a receiver with sub-route', async ({ page }) => { + const yaml = `route: + routes: + - match: + service: database + receiver: team-DB-pager + routes: + - match: + owner: team-X + receiver: team-X-pager +receivers: +- name: 'team-X-pager' +- name: 'team-DB-pager'`; + + await test.step('Set YAML with sub-route', async () => { + await alertmanager.navigateToYAMLPage(); + await alertmanager.setYAMLContent(yaml); + await page.getByTestId('save-changes').click(); + await expect(page.getByTestId('alert-success')).toBeVisible(); + }); + + await test.step('Verify Delete Receiver is disabled for receiver with sub-route', async () => { + await page.getByRole('tab', { name: 'Details' }).click(); + + const receiverRow = page.getByTestId('data-view-cell-team-X-pager-name').locator('..'); + await receiverRow.getByTestId('kebab-button').click(); + + const deleteMenuItem = page.getByRole('menuitem', { name: 'Delete Receiver' }); + await expect(deleteMenuItem).toBeDisabled(); + }); + }); + + test('converts existing match and match_re routing labels to matchers', async ({ page }) => { + const receiverName = `EmailReceiver-${Date.now()}`; + const severity = 'severity'; + const warning = 'warning'; + const service = 'service'; + const regex = '^(foo1|foo2|baz)$'; + const matcher1 = `${severity} = ${warning}`; + const matcher2 = `${service} =~ ${regex}`; + + const yaml = `global: + resolve_timeout: 5m +inhibit_rules: + - equal: + - namespace + - alertname + source_matchers: + - severity = critical + target_matchers: + - severity =~ warning|info + - equal: + - namespace + - alertname + source_matchers: + - severity = warning + target_matchers: + - severity = info + - equal: + - namespace + source_matchers: + - alertname = InfoInhibitor + target_matchers: + - severity = info +receivers: + - name: Default + - name: Watchdog + - name: Critical + - name: "null" + - name: ${receiverName} + email_configs: + - to: you@there.com + from: me@here.com + smarthost: "smarthost:8080" +route: + group_by: + - namespace + group_interval: 5m + group_wait: 30s + receiver: Default + repeat_interval: 12h + routes: + - matchers: + - alertname = Watchdog + receiver: Watchdog + - matchers: + - alertname = InfoInhibitor + receiver: "null" + - matchers: + - severity = critical + receiver: Critical + - receiver: ${receiverName} + match: + ${severity}: ${warning} + match_re: + ${service}: ${regex}`; + + await test.step('Add receiver with match and match_re routing labels', async () => { + await alertmanager.navigateToYAMLPage(); + await alertmanager.setYAMLContent(yaml); + await page.getByTestId('save-changes').click(); + await expect(page.locator('.yaml-editor__buttons .pf-m-success')).toBeVisible(); + }); + + await test.step('Verify receiver appears and edit it', async () => { + await page.getByRole('tab', { name: 'Details' }).click(); + await expect(page.getByTestId(`data-view-cell-${receiverName}-name`)).toBeVisible(); + + await alertmanager.navigateToEditReceiver(receiverName); + + // Verify matchers were converted from match/match_re + await expect(page.getByTestId('label-0')).toHaveValue(matcher1); + await expect(page.getByTestId('label-1')).toHaveValue(matcher2); + + await alertmanager.save(); + }); + + await test.step('Verify match and match_re were converted to matchers in YAML', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + + const config: AlertmanagerConfig = jsYaml.load(yamlContent) as AlertmanagerConfig; + const route: AlertmanagerRoute | undefined = config.route.routes?.find( + (r: AlertmanagerRoute) => r.receiver === receiverName, + ); + + expect(route?.matchers?.[0]).toBe(matcher1); + expect(route?.matchers?.[1]).toBe(matcher2); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/email.spec.ts b/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/email.spec.ts new file mode 100644 index 00000000000..5b6fac9eb2c --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/email.spec.ts @@ -0,0 +1,222 @@ +import { test, expect } from '../../../../../fixtures'; +import { + AlertmanagerPage, + getGlobalsAndReceiverConfig, +} from '../../../../../pages/alertmanager-page'; +import KubernetesClient from '../../../../../clients/kubernetes-client'; + +test.describe.configure({ mode: 'serial' }); + +test.describe('Alertmanager Email Receiver Form', { tag: ['@admin'] }, () => { + let alertmanager: AlertmanagerPage; + let k8sClient: KubernetesClient; + + const receiverName = `EmailReceiver-${Date.now()}`; + const receiverType = 'email'; + const configName = `${receiverType}_configs`; + const localhost = 'localhost'; + const label = 'severity = warning'; + const emailTo = 'you@there.com'; + const emailFrom = 'me@here.com'; + const emailSmarthost = 'smarthost:8080'; + const username = 'username'; + const password = 'password'; + const identity = 'identity'; + const secret = 'secret'; + const html = 'myhtml'; + + // Default Alertmanager YAML for reset + const defaultAlertmanagerYaml = `global: + resolve_timeout: 5m +inhibit_rules: +- equal: + - namespace + - alertname + source_match: + severity: critical + target_match_re: + severity: warning|info +- equal: + - namespace + - alertname + source_match: + severity: warning + target_match_re: + severity: info +receivers: +- name: Default +- name: Watchdog +- name: Critical +route: + group_by: + - namespace + group_interval: 5m + group_wait: 30s + receiver: Default + repeat_interval: 12h + routes: + - matchers: + - alertname = Watchdog + receiver: Watchdog + - matchers: + - severity = critical + receiver: Critical`; + + test.beforeEach(async ({ page, k8sClient: client }) => { + alertmanager = new AlertmanagerPage(page); + k8sClient = client; + }); + + test.afterEach(async () => { + // Reset alertmanager configuration + await k8sClient.patchSecret('alertmanager-main', 'openshift-monitoring', [ + { + op: 'replace', + path: '/data/alertmanager.yaml', + value: Buffer.from(defaultAlertmanagerYaml).toString('base64'), + }, + ]); + }); + + test('creates and edits Email Receiver correctly', async ({ page }) => { + await test.step('Create Email Receiver with basic configuration', async () => { + await alertmanager.navigateToAlertmanager(); + await alertmanager.createReceiver(receiverName, configName); + + // Verify defaults before smtp change + const saveAsDefaultCheckbox = page.getByTestId('save-as-default'); + await expect(saveAsDefaultCheckbox).toBeDisabled(); + + const emailHelloInput = page.getByTestId('email-hello'); + await expect(emailHelloInput).toHaveValue(localhost); + + const requireTlsCheckbox = page.getByTestId('email-require-tls'); + await expect(requireTlsCheckbox).toBeChecked(); + + // Check advanced configuration defaults + await alertmanager.showAdvancedConfiguration(); + const sendResolvedCheckbox = page.getByTestId('send-resolved-alerts'); + await expect(sendResolvedCheckbox).not.toBeChecked(); + + const emailHtmlInput = page.getByTestId('email-html'); + await expect(emailHtmlInput).toHaveValue('{{ template "email.default.html" . }}'); + + // Fill in required fields + await page.getByTestId('email-to').fill(emailTo); + await page.getByTestId('email-from').fill(emailFrom); + + // Save as default should now be enabled + await expect(saveAsDefaultCheckbox).toBeEnabled(); + + await page.getByTestId('email-smarthost').fill(emailSmarthost); + await page.getByTestId('label-0').fill(label); + + await alertmanager.save(); + }); + + await test.step('Verify Email Receiver was created correctly', async () => { + await alertmanager.validateReceiverInList(receiverName); + + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + // Verify values are NOT in globals + expect(configs.globals).not.toHaveProperty('email_to'); + expect(configs.globals).not.toHaveProperty('smtp_from'); + expect(configs.globals).not.toHaveProperty('smtp_smarthost'); + expect(configs.globals).not.toHaveProperty('smtp_require_tls'); + + // Verify values ARE in receiver config + expect(configs.receiverConfig.to).toBe(emailTo); + expect(configs.receiverConfig.from).toBe(emailFrom); + expect(configs.receiverConfig.smarthost).toBe(emailSmarthost); + // require_tls should not be in receiver config (unchanged from global) + expect(configs.receiverConfig).not.toHaveProperty('require_tls'); + }); + + await test.step('Edit receiver with auth and advanced fields', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + + // Verify existing values + await expect(page.getByTestId('email-to')).toHaveValue(emailTo); + + const saveAsDefaultCheckbox = page.getByTestId('save-as-default'); + await expect(saveAsDefaultCheckbox).toBeEnabled(); + await expect(saveAsDefaultCheckbox).not.toBeChecked(); + + await expect(page.getByTestId('email-from')).toHaveValue(emailFrom); + await expect(page.getByTestId('email-hello')).toHaveValue(localhost); + + // Add auth fields + await page.getByTestId('email-auth-username').fill(username); + await page.getByTestId('email-auth-password').fill(password); + await page.getByTestId('email-auth-identity').fill(identity); + await page.getByTestId('email-auth-secret').fill(secret); + + // Uncheck require TLS + await page.getByTestId('email-require-tls').uncheck(); + + // Update advanced fields + await alertmanager.showAdvancedConfiguration(); + await page.getByTestId('send-resolved-alerts').check(); + + const htmlInput = page.getByTestId('email-html'); + await htmlInput.clear(); + await htmlInput.fill(html); + + await alertmanager.save(); + }); + + await test.step('Verify auth and advanced fields were saved correctly', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + // Auth username should NOT be in globals + expect(configs.globals).not.toHaveProperty('smtp_auth_username'); + + // Auth fields should be in receiver config + expect(configs.receiverConfig.auth_username).toBe(username); + expect(configs.receiverConfig.auth_password).toBe(password); + expect(configs.receiverConfig.auth_identity).toBe(identity); + expect(configs.receiverConfig.auth_secret).toBe(secret); + + // require_tls should now be explicitly false in receiver config + expect(configs.receiverConfig.require_tls).toBe(false); + + // Advanced fields + expect(configs.receiverConfig.send_resolved).toBe(true); + expect(configs.receiverConfig.html).toBe(html); + }); + + await test.step('Save fields as global defaults', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + + const saveAsDefaultCheckbox = page.getByTestId('save-as-default'); + await expect(saveAsDefaultCheckbox).not.toBeChecked(); + await saveAsDefaultCheckbox.check(); + + await alertmanager.save(); + }); + + await test.step('Verify fields were saved as globals', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + // Verify values are now in globals + expect(configs.globals.smtp_from).toBe(emailFrom); + expect(configs.globals.smtp_hello).toBe(localhost); + expect(configs.globals.smtp_smarthost).toBe(emailSmarthost); + expect(configs.globals.smtp_auth_username).toBe(username); + expect(configs.globals.smtp_auth_password).toBe(password); + expect(configs.globals.smtp_auth_identity).toBe(identity); + expect(configs.globals.smtp_auth_secret).toBe(secret); + expect(configs.globals.smtp_require_tls).toBe(false); + + // Non-global field (to) should still be in receiver config + expect(configs.receiverConfig.to).toBe(emailTo); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/pagerduty.spec.ts b/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/pagerduty.spec.ts new file mode 100644 index 00000000000..08ea4215afc --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/pagerduty.spec.ts @@ -0,0 +1,206 @@ +import { test, expect } from '../../../../../fixtures'; +import { + AlertmanagerPage, + getGlobalsAndReceiverConfig, +} from '../../../../../pages/alertmanager-page'; +import KubernetesClient from '../../../../../clients/kubernetes-client'; +import { resetAlertmanagerConfig } from '../alertmanager-test-utils'; + +test.describe.configure({ mode: 'serial' }); + +test.describe('Alertmanager PagerDuty Receiver Form', { tag: ['@admin'] }, () => { + let alertmanager: AlertmanagerPage; + let k8sClient: KubernetesClient; + + const receiverName = `PagerDutyReceiver-${Date.now()}`; + const receiverType = 'pagerduty'; + const configName = `${receiverType}_configs`; + const severity = 'severity'; + const label = `${severity} = warning`; + const pagerDutyClient = '{{ template "pagerduty.default.client" . }}'; + const pagerDutyClientURL = '{{ template "pagerduty.default.clientURL" . }}'; + const pagerDutyURL1 = 'http://pagerduty-url-specific-to-receiver'; + const pagerDutyURL2 = 'http://global-pagerduty-url'; + const pagerDutyURL3 = 'http://pagerduty-url-specific-to-receiver'; + const clientURL = 'http://updated-client-url'; + const pagerDutyDescription = 'new description'; + + test.beforeEach(async ({ page, k8sClient: client }) => { + alertmanager = new AlertmanagerPage(page); + k8sClient = client; + }); + + test.afterEach(async () => { + await resetAlertmanagerConfig(k8sClient); + }); + + test('creates and edits PagerDuty Receiver correctly', async ({ page }) => { + await test.step('Create PagerDuty Receiver with basic configuration', async () => { + await alertmanager.navigateToAlertmanager(); + await alertmanager.createReceiver(receiverName, configName); + + await page.getByTestId('integration-key').fill(''); + + // Verify default URL + await expect(page.getByTestId('pagerduty-url')).toHaveValue( + 'https://events.pagerduty.com/v2/enqueue', + ); + + // Check advanced configuration defaults + await alertmanager.showAdvancedConfiguration(); + await expect(page.getByTestId('send-resolved-alerts')).toBeChecked(); + await expect(page.getByTestId('pagerduty-client')).toHaveValue(pagerDutyClient); + await expect(page.getByTestId('pagerduty-client-url')).toHaveValue(pagerDutyClientURL); + await expect(page.getByTestId('pagerduty-description')).toHaveValue( + '{{ template "pagerduty.default.description" .}}', + ); + await expect(page.getByTestId('pagerduty-severity')).toHaveValue('error'); + + await page.getByTestId('label-0').fill(label); + await alertmanager.save(); + }); + + await test.step('Verify PagerDuty Receiver was created correctly', async () => { + await alertmanager.validateReceiverInList(receiverName); + }); + + await test.step('Update pagerduty_url', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + + // Save as default checkbox disabled when url equals global url + await expect(page.getByTestId('save-as-default')).toBeDisabled(); + + // Changing url enables Save as default checkbox + const urlInput = page.getByTestId('pagerduty-url'); + await urlInput.clear(); + await urlInput.fill(pagerDutyURL1); + + await expect(page.getByTestId('save-as-default')).toBeEnabled(); + await alertmanager.save(); + }); + + await test.step('Verify pagerduty_url was saved with Receiver and not global', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.globals).not.toHaveProperty('pagerduty_url'); + expect(configs.receiverConfig.url).toBe(pagerDutyURL1); + }); + + await test.step('Save pagerduty_url as global', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + + const urlInput = page.getByTestId('pagerduty-url'); + await urlInput.clear(); + await urlInput.fill(pagerDutyURL2); + + const saveAsDefaultCheckbox = page.getByTestId('save-as-default'); + await expect(saveAsDefaultCheckbox).toBeEnabled(); + await saveAsDefaultCheckbox.check(); + + await alertmanager.save(); + }); + + await test.step('Verify pagerduty_url was saved as global', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.globals.pagerduty_url).toBe(pagerDutyURL2); + expect(configs.receiverConfig).not.toHaveProperty('url'); + }); + + await test.step('Add pagerduty_url to receiver with existing global', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + + const urlInput = page.getByTestId('pagerduty-url'); + await urlInput.clear(); + await urlInput.fill(pagerDutyURL3); + + const saveAsDefaultCheckbox = page.getByTestId('save-as-default'); + await expect(saveAsDefaultCheckbox).toBeEnabled(); + await expect(saveAsDefaultCheckbox).not.toBeChecked(); + + await alertmanager.save(); + }); + + await test.step( + 'Verify pagerduty_url saved with Receiver and global still exists', + async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.globals.pagerduty_url).toBe(pagerDutyURL2); + expect(configs.receiverConfig.url).toBe(pagerDutyURL3); + }, + ); + + await test.step('Update advanced configuration fields', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + await alertmanager.showAdvancedConfiguration(); + + const sendResolvedCheckbox = page.getByTestId('send-resolved-alerts'); + await expect(sendResolvedCheckbox).toBeChecked(); + await sendResolvedCheckbox.uncheck(); + await expect(sendResolvedCheckbox).not.toBeChecked(); + + await page.getByTestId('pagerduty-client').clear(); + await page.getByTestId('pagerduty-client').fill('updated-client'); + + await page.getByTestId('pagerduty-client-url').clear(); + await page.getByTestId('pagerduty-client-url').fill(clientURL); + + await alertmanager.save(); + }); + + await test.step('Verify changed fields are saved with Receiver', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.receiverConfig.send_resolved).toBe(false); + expect(configs.receiverConfig.client).toBe('updated-client'); + expect(configs.receiverConfig.client_url).toBe('http://updated-client-url'); + expect(configs.receiverConfig.description).toBeUndefined(); + expect(configs.receiverConfig.severity).toBeUndefined(); + }); + + await test.step('Restore defaults, change desc and severity', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + await alertmanager.showAdvancedConfiguration(); + + const sendResolvedCheckbox = page.getByTestId('send-resolved-alerts'); + await expect(sendResolvedCheckbox).not.toBeChecked(); + await sendResolvedCheckbox.check(); + await expect(sendResolvedCheckbox).toBeChecked(); + + await page.getByTestId('pagerduty-client').clear(); + await page.getByTestId('pagerduty-client').fill(pagerDutyClient); + + await page.getByTestId('pagerduty-client-url').clear(); + await page.getByTestId('pagerduty-client-url').fill(pagerDutyClientURL); + + await page.getByTestId('pagerduty-description').clear(); + await page.getByTestId('pagerduty-description').fill(pagerDutyDescription); + + await page.getByTestId('pagerduty-severity').clear(); + await page.getByTestId('pagerduty-severity').fill(severity); + + await alertmanager.save(); + }); + + await test.step('Verify defaults removed from config, desc and severity saved', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.receiverConfig.send_resolved).toBeUndefined(); + expect(configs.receiverConfig.client).toBeUndefined(); + expect(configs.receiverConfig.client_url).toBeUndefined(); + expect(configs.receiverConfig.description).toBe(pagerDutyDescription); + expect(configs.receiverConfig.severity).toBe(severity); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/slack.spec.ts b/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/slack.spec.ts new file mode 100644 index 00000000000..71bee45c3f3 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/slack.spec.ts @@ -0,0 +1,138 @@ +import { test, expect } from '../../../../../fixtures'; +import { + AlertmanagerPage, + getGlobalsAndReceiverConfig, +} from '../../../../../pages/alertmanager-page'; +import KubernetesClient from '../../../../../clients/kubernetes-client'; +import { resetAlertmanagerConfig } from '../alertmanager-test-utils'; + +test.describe.configure({ mode: 'serial' }); + +test.describe('Alertmanager Slack Receiver Form', { tag: ['@admin'] }, () => { + let alertmanager: AlertmanagerPage; + let k8sClient: KubernetesClient; + + const receiverName = `SlackReceiver-${Date.now()}`; + const receiverType = 'slack'; + const configName = `${receiverType}_configs`; + const label = 'severity = warning'; + const slackAPIURL = 'http://myslackapi'; + const slackChannel = 'myslackchannel'; + const slackIconURL = 'http://slackiconurl'; + const slackUsername = 'slackusername'; + + test.beforeEach(async ({ page, k8sClient: client }) => { + alertmanager = new AlertmanagerPage(page); + k8sClient = client; + }); + + test.afterEach(async () => { + await resetAlertmanagerConfig(k8sClient); + }); + + test('creates and edits Slack Receiver correctly', async ({ page }) => { + await test.step('Create Slack Receiver with basic configuration', async () => { + await alertmanager.navigateToAlertmanager(); + await alertmanager.createReceiver(receiverName, configName); + + await expect(page.getByTestId('save-as-default')).toBeDisabled(); + + await alertmanager.showAdvancedConfiguration(); + + // Verify defaults + await expect(page.getByTestId('send-resolved-alerts')).not.toBeChecked(); + await expect(page.getByTestId('slack-icon-url')).toHaveValue( + '{{ template "slack.default.iconurl" .}}', + ); + await expect(page.getByTestId('slack-icon-emoji')).not.toBeVisible(); + + // Switch to Emoji radio and verify + await page.getByTestId('Emoji-radio-input').click(); + await expect(page.getByTestId('slack-icon-url')).not.toBeVisible(); + await expect(page.getByTestId('slack-icon-emoji')).toHaveValue( + '{{ template "slack.default.iconemoji" .}}', + ); + + // Switch back to URL for the test + await page.getByTestId('URL-radio-input').click(); + + await expect(page.getByTestId('slack-username')).toHaveValue( + '{{ template "slack.default.username" . }}', + ); + await expect(page.getByTestId('slack-link-names')).not.toBeChecked(); + + // Fill required fields + await page.getByTestId('slack-api-url').fill(slackAPIURL); + await expect(page.getByTestId('save-as-default')).toBeEnabled(); + + await page.getByTestId('slack-channel').fill(slackChannel); + await page.getByTestId('label-0').fill(label); + + await alertmanager.save(); + }); + + await test.step('Verify Slack Receiver was created correctly', async () => { + await alertmanager.validateReceiverInList(receiverName); + + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.globals).not.toHaveProperty('slack_api_url'); + expect(configs.receiverConfig.channel).toBe(slackChannel); + expect(configs.receiverConfig.api_url).toBe(slackAPIURL); + // Advanced fields are not saved since they equal their global values + expect(configs.receiverConfig).not.toHaveProperty('send_resolved'); + expect(configs.receiverConfig).not.toHaveProperty('username'); + }); + + await test.step('Save globals and advanced fields', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + + await expect(page.getByTestId('slack-channel')).toHaveValue(slackChannel); + await expect(page.getByTestId('save-as-default')).toBeEnabled(); + await expect(page.getByTestId('slack-api-url')).toHaveValue(slackAPIURL); + + await alertmanager.showAdvancedConfiguration(); + + await page.getByTestId('send-resolved-alerts').check(); + + await page.getByTestId('slack-icon-url').clear(); + await page.getByTestId('slack-icon-url').fill(slackIconURL); + + await page.getByTestId('slack-username').clear(); + await page.getByTestId('slack-username').fill(slackUsername); + + await page.getByTestId('slack-link-names').check(); + + await page.getByTestId('save-as-default').check(); + + await alertmanager.save(); + }); + + await test.step('Verify advanced fields were saved correctly', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + await alertmanager.showAdvancedConfiguration(); + + await expect(page.getByTestId('send-resolved-alerts')).toBeChecked(); + await expect(page.getByTestId('slack-icon-url')).toHaveValue(slackIconURL); + await expect(page.getByTestId('slack-icon-emoji')).not.toBeVisible(); + await expect(page.getByTestId('slack-username')).toHaveValue(slackUsername); + await expect(page.getByTestId('slack-link-names')).toBeChecked(); + }); + + await test.step('Verify YAML has correct global and receiver config', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.globals.slack_api_url).toBe(slackAPIURL); + expect(configs.receiverConfig).not.toHaveProperty('api_url'); + expect(configs.receiverConfig.channel).toBe('myslackchannel'); + expect(configs.receiverConfig.send_resolved).toBe(true); + expect(configs.receiverConfig.icon_url).toBe(slackIconURL); + expect(configs.receiverConfig.username).toBe(slackUsername); + expect(configs.receiverConfig.link_names).toBe(true); + }); + }); +}); diff --git a/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/webhook.spec.ts b/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/webhook.spec.ts new file mode 100644 index 00000000000..9d21b932f81 --- /dev/null +++ b/frontend/e2e/tests/console/cluster-settings/alertmanager/receivers/webhook.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from '../../../../../fixtures'; +import { + AlertmanagerPage, + getGlobalsAndReceiverConfig, +} from '../../../../../pages/alertmanager-page'; +import KubernetesClient from '../../../../../clients/kubernetes-client'; +import { resetAlertmanagerConfig } from '../alertmanager-test-utils'; + +test.describe.configure({ mode: 'serial' }); + +test.describe('Alertmanager Webhook Receiver Form', { tag: ['@admin'] }, () => { + let alertmanager: AlertmanagerPage; + let k8sClient: KubernetesClient; + + const receiverName = `WebhookReceiver-${Date.now()}`; + const receiverType = 'webhook'; + const configName = `${receiverType}_configs`; + const label = 'severity = warning'; + const webhookURL = 'http://mywebhookurl'; + const updatedWebhookURL = 'http://myupdatedwebhookurl'; + + test.beforeEach(async ({ page, k8sClient: client }) => { + alertmanager = new AlertmanagerPage(page); + k8sClient = client; + }); + + test.afterEach(async () => { + await resetAlertmanagerConfig(k8sClient); + }); + + test('creates and edits Webhook Receiver correctly', async ({ page }) => { + await test.step('Create Webhook Receiver', async () => { + await alertmanager.navigateToAlertmanager(); + await alertmanager.createReceiver(receiverName, configName); + + await alertmanager.showAdvancedConfiguration(); + await expect(page.getByTestId('send-resolved-alerts')).toBeChecked(); + + await page.getByTestId('webhook-url').fill(webhookURL); + await page.getByTestId('label-0').fill(label); + + await alertmanager.save(); + }); + + await test.step('Verify Webhook Receiver was created correctly', async () => { + await alertmanager.validateReceiverInList(receiverName); + + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.receiverConfig.url).toBe(webhookURL); + expect(configs.receiverConfig).not.toHaveProperty('send_resolved'); + }); + + await test.step('Edit Webhook Receiver and save advanced fields', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + + await expect(page.getByTestId('webhook-url')).toHaveValue(webhookURL); + + await page.getByTestId('webhook-url').clear(); + await page.getByTestId('webhook-url').fill(updatedWebhookURL); + + await alertmanager.showAdvancedConfiguration(); + await page.getByTestId('send-resolved-alerts').uncheck(); + + await alertmanager.save(); + }); + + await test.step('Verify advanced fields were saved correctly', async () => { + await alertmanager.navigateToEditReceiver(receiverName); + await alertmanager.showAdvancedConfiguration(); + + await expect(page.getByTestId('send-resolved-alerts')).not.toBeChecked(); + }); + + await test.step('Verify YAML has correct config', async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + + expect(configs.receiverConfig.url).toBe(updatedWebhookURL); + expect(configs.receiverConfig.send_resolved).toBe(false); + }); + }); +}); 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/e2e/tests/console/crd-extensions/console-cli-download.spec.ts b/frontend/e2e/tests/console/crd-extensions/console-cli-download.spec.ts new file mode 100644 index 00000000000..44f1dafbc3f --- /dev/null +++ b/frontend/e2e/tests/console/crd-extensions/console-cli-download.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from '../../../fixtures'; +import KubernetesClient from '../../../clients/kubernetes-client'; +import { + createCustomResourceViaYaml, + deleteCustomResourceAndVerify, + navigateToCRDInstances, +} from './crd-test-utils'; + +const crd = 'ConsoleCLIDownload'; + +test.describe(`${crd} CRD`, { tag: ['@admin'] }, () => { + let k8sClient: KubernetesClient; + let name: string; + let crdObj: any; + + test.beforeEach(async ({ k8sClient: client }) => { + k8sClient = client; + + // Generate unique name for each test run + name = `console-cli-download-test-${Date.now()}`; + + // Cannot use default YAML template since it contains new lines + // in the description and that breaks with load + crdObj = { + apiVersion: 'console.openshift.io/v1', + kind: crd, + metadata: { + name, + }, + spec: { + displayName: name, + description: + 'This is an example CLI download description that can include markdown such as paragraphs, unordered lists, code, [links](https://www.example.com), etc.', + links: [{ href: 'https://www.example.com', text: 'Example CLI Download' }], + }, + }; + }); + + test.afterEach(async () => { + // Clean up the ConsoleCLIDownload instance + try { + await k8sClient.deleteCustomResource( + 'console.openshift.io', + 'v1', + '', + 'consoleclidownloads', + name, + ); + } catch (error) { + // Ignore if already deleted + } + }); + + test(`creates, displays, and deletes a new ${crd} instance`, async ({ page }) => { + await test.step('Navigate to CRD instances page', async () => { + await navigateToCRDInstances(page, crd); + }); + + await test.step('Create ConsoleCLIDownload instance via YAML editor', async () => { + await createCustomResourceViaYaml(page, crdObj); + }); + + await test.step('Verify instance appears on details page', async () => { + // YAML save redirects to the created resource — no goto needed + await expect(page.getByRole('heading', { name })).toBeVisible(); + }); + + await test.step('Verify instance appears on Command Line Tools page', async () => { + await page.goto('/command-line-tools'); + await expect(page.locator(`[data-test-id="${name}"]`)).toContainText(name); + }); + + await test.step('Delete the ConsoleCLIDownload instance', async () => { + await deleteCustomResourceAndVerify( + page, + k8sClient, + 'console.openshift.io', + 'v1', + '', + 'consoleclidownloads', + name, + crd, + ); + }); + }); +}); diff --git a/frontend/e2e/tests/console/crd-extensions/console-external-log-link.spec.ts b/frontend/e2e/tests/console/crd-extensions/console-external-log-link.spec.ts new file mode 100644 index 00000000000..2df6273c8e7 --- /dev/null +++ b/frontend/e2e/tests/console/crd-extensions/console-external-log-link.spec.ts @@ -0,0 +1,195 @@ +import { test, expect } from '../../../fixtures'; +import KubernetesClient from '../../../clients/kubernetes-client'; +import { + createCustomResourceViaYaml, + deleteCustomResourceAndVerify, + navigateToCRDInstancesViaDetails, + replaceYamlEditorContent, + updateCustomResourceViaYaml, + waitForYamlEditor, +} from './crd-test-utils'; + +const crd = 'ConsoleExternalLogLink'; + +test.describe(`${crd} CRD`, { tag: ['@admin'] }, () => { + let k8sClient: KubernetesClient; + let name: string; + let podName: string; + let projectName: string; + let crdObj: any; + let podObj: any; + + test.beforeEach(async ({ k8sClient: client }) => { + k8sClient = client; + + // Generate unique names for each test run + const timestamp = Date.now(); + name = `console-external-log-link-test-${timestamp}`; + podName = `test-pod-${timestamp}`; + projectName = `test-project-${timestamp}`; + + // Create test project + await k8sClient.createProject(projectName); + + // Prepare ConsoleExternalLogLink object + crdObj = { + apiVersion: 'console.openshift.io/v1', + kind: crd, + metadata: { + name, + }, + spec: { + text: `${name} Logs`, + hrefTemplate: 'https://example.com/logs?pod=${resourceName}&namespace=${resourceNamespace}', + }, + }; + + // Prepare Pod object + podObj = { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + name: podName, + namespace: projectName, + labels: { + app: name, + }, + }, + spec: { + securityContext: { + runAsNonRoot: true, + seccompProfile: { + type: 'RuntimeDefault', + }, + }, + containers: [ + { + name: 'test-container', + image: 'registry.access.redhat.com/ubi8/ubi-minimal:latest', + command: ['sh', '-c', 'echo Hello && sleep 3600'], + securityContext: { + allowPrivilegeEscalation: false, + capabilities: { + drop: ['ALL'], + }, + }, + }, + ], + }, + }; + }); + + test.afterEach(async () => { + // Clean up the Pod + try { + await k8sClient.deletePod(podName, projectName); + } catch (error) { + // Ignore if already deleted + } + + // Clean up the ConsoleExternalLogLink instance + try { + await k8sClient.deleteCustomResource( + 'console.openshift.io', + 'v1', + '', + 'consoleexternalloglinks', + name, + ); + } catch (error) { + // Ignore if already deleted + } + + // Clean up the project + try { + await k8sClient.deleteProject(projectName); + } catch (error) { + // Ignore if already deleted + } + }); + + test(`creates, displays, modifies, and deletes a new ${crd} instance`, async ({ page }) => { + await test.step('Navigate to CRD instances page', async () => { + await navigateToCRDInstancesViaDetails(page, crd); + }); + + await test.step('Create ConsoleExternalLogLink instance via YAML editor', async () => { + await createCustomResourceViaYaml(page, crdObj); + }); + + await test.step('Verify instance appears on details page', async () => { + // YAML save redirects to the created resource — no goto needed + await expect(page.getByRole('heading', { name })).toBeVisible(); + }); + + await test.step('Create Pod with matching label in test namespace', async () => { + await page.goto(`/k8s/ns/${projectName}/pods`); + await page.getByTestId('item-create').click(); + + await waitForYamlEditor(page); + await replaceYamlEditorContent(page, podObj); + await page.getByTestId('save-changes').click(); + + // Verify no YAML errors + await expect(page.getByTestId('yaml-error')).not.toBeVisible(); + + // Verify we're redirected to the pod details page + await expect(page.getByRole('heading', { name: podName })).toBeVisible({ timeout: 30000 }); + }); + + await test.step('Verify external log link appears on Pod logs page', async () => { + await page.goto(`/k8s/ns/${projectName}/pods/${podName}/logs`); + + // Wait for logs page to load + await expect(page.getByTestId('resource-log-toolbar')).toBeVisible(); + + // Give the console time to load and render the ConsoleExternalLogLink + // The link is fetched asynchronously when the logs page mounts + await page.waitForTimeout(2000); + + // Verify the external log link appears + await expect(page.getByTestId(name)).toBeVisible({ timeout: 10000 }); + }); + + await test.step('Add namespaceFilter to ConsoleExternalLogLink', async () => { + await page.goto(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}/yaml`); + + await updateCustomResourceViaYaml(page, (obj) => { + obj.spec.namespaceFilter = '^openshift-'; + return obj; + }); + }); + + await test.step('Verify external log link is filtered out by namespaceFilter', async () => { + await page.goto(`/k8s/ns/${projectName}/pods/${podName}/logs`); + + // Wait for logs page to load + await expect(page.getByTestId('resource-log-toolbar')).toBeVisible(); + + // Verify the external log link does NOT appear (filtered out) + await expect(page.getByTestId(name)).not.toBeVisible(); + }); + + await test.step('Delete the Pod', async () => { + await k8sClient.deletePod(podName, projectName); + + // Verify deletion by checking the pod is gone + await page.goto(`/k8s/ns/${projectName}/pods`); + const podRow = page.getByRole('row', { name: new RegExp(podName) }); + await expect(podRow).not.toBeVisible({ timeout: 10000 }); + }); + + await test.step('Delete the ConsoleExternalLogLink instance', async () => { + await deleteCustomResourceAndVerify( + page, + k8sClient, + 'console.openshift.io', + 'v1', + '', + 'consoleexternalloglinks', + name, + crd, + ); + }); + }); +}); diff --git a/frontend/e2e/tests/console/crd-extensions/console-link.spec.ts b/frontend/e2e/tests/console/crd-extensions/console-link.spec.ts new file mode 100644 index 00000000000..c831e5826cd --- /dev/null +++ b/frontend/e2e/tests/console/crd-extensions/console-link.spec.ts @@ -0,0 +1,124 @@ +import { test, expect } from '../../../fixtures'; +import KubernetesClient from '../../../clients/kubernetes-client'; +import { createCustomResourceViaYaml, navigateToCRDInstances } from './crd-test-utils'; + +const crd = 'ConsoleLink'; + +const testObjs = [ + { + dropdownMenuName: 'help menu', + dropdownTestId: 'help-dropdown', + dropdownToggleTestId: 'help-dropdown-toggle', + menuLinkLocation: 'HelpMenu', + menuLinkText: 'help menu link', + }, + { + dropdownMenuName: 'user menu', + dropdownTestId: 'user-dropdown', + dropdownToggleTestId: 'user-dropdown-toggle', + menuLinkLocation: 'UserMenu', + menuLinkText: 'user menu link', + }, +]; + +test.describe(`${crd} CRD`, { tag: ['@admin'] }, () => { + let k8sClient: KubernetesClient; + + test.beforeEach(async ({ k8sClient: client }) => { + k8sClient = client; + }); + + testObjs.forEach( + ({ + dropdownMenuName, + dropdownTestId, + dropdownToggleTestId, + menuLinkLocation, + menuLinkText, + }) => { + test(`creates, displays, and deletes a new ${crd} ${dropdownMenuName} instance`, async ({ + page, + }) => { + // Generate unique name for each test run + const name = `console-link-test-${Date.now()}`; + const fullMenuLinkText = `${name} ${menuLinkText}`; + + const crdObj = { + apiVersion: 'console.openshift.io/v1', + kind: crd, + metadata: { + name, + }, + spec: { + location: menuLinkLocation, + text: fullMenuLinkText, + href: 'https://www.example.com', + }, + }; + + try { + await test.step('Navigate to CRD instances page', async () => { + await navigateToCRDInstances(page, crd); + }); + + await test.step('Create ConsoleLink instance via YAML editor', async () => { + await createCustomResourceViaYaml(page, crdObj); + }); + + await test.step('Verify instance appears on details page', async () => { + // YAML save redirects to the created resource — no goto needed + await expect(page.getByRole('heading', { name })).toBeVisible(); + }); + + await test.step(`Verify link appears in ${dropdownMenuName}`, async () => { + // Open the dropdown menu + await page.getByTestId(dropdownToggleTestId).click(); + + // Verify the link appears in the menu + const dropdown = page.getByTestId(dropdownTestId); + const menuLink = dropdown + .getByTestId('application-launcher-item') + .getByText(fullMenuLinkText, { exact: true }); + await expect(menuLink).toBeVisible(); + + // Close the dropdown by clicking the toggle again + await page.getByTestId(dropdownToggleTestId).click(); + }); + + await test.step('Delete the ConsoleLink instance', async () => { + await page.goto(`/k8s/cluster/console.openshift.io~v1~${crd}`); + + // Wait for the list to load + const instanceRow = page.getByRole('row', { name: new RegExp(name) }); + await instanceRow.waitFor({ state: 'visible', timeout: 10000 }); + + // Click kebab menu and delete + const kebabButton = instanceRow.getByTestId('kebab-button'); + await kebabButton.click(); + await page.getByRole('menuitem', { name: `Delete ${crd}` }).click(); + + // Confirm deletion in modal + await expect(page.getByRole('heading', { name: `Delete ${crd}?` })).toBeVisible(); + await page.getByTestId('confirm-action').click(); + + // Verify the instance is gone + await expect(instanceRow).not.toBeVisible({ timeout: 10000 }); + }); + } finally { + // Clean up the ConsoleLink instance + try { + await k8sClient.deleteCustomResource( + 'console.openshift.io', + 'v1', + '', + 'consolelinks', + name, + ); + } catch (error) { + // Ignore if already deleted + } + } + }); + }, + ); +}); diff --git a/frontend/e2e/tests/console/crd-extensions/console-notification.spec.ts b/frontend/e2e/tests/console/crd-extensions/console-notification.spec.ts new file mode 100644 index 00000000000..35cc978ea9f --- /dev/null +++ b/frontend/e2e/tests/console/crd-extensions/console-notification.spec.ts @@ -0,0 +1,141 @@ +import { test, expect } from '../../../fixtures'; +import KubernetesClient from '../../../clients/kubernetes-client'; +import { + createCustomResourceViaYaml, + navigateToCRDInstances, + updateCustomResourceViaYaml, +} from './crd-test-utils'; + +const crd = 'ConsoleNotification'; + +test.describe(`${crd} CRD`, { tag: ['@admin'] }, () => { + let k8sClient: KubernetesClient; + + test.beforeEach(async ({ k8sClient: client }) => { + k8sClient = client; + }); + + test(`creates, displays, modifies, and deletes a new ${crd} instance`, async ({ page }) => { + const name = `console-notification-test-${Date.now()}`; + const location = 'BannerTop'; + const altLocation = 'BannerBottom'; + const text = `${name} notification that appears ${location}`; + const altText = `${name} notification that appears ${altLocation}`; + + const crdObj = { + apiVersion: 'console.openshift.io/v1', + kind: crd, + metadata: { + name, + }, + spec: { + location, + text, + }, + }; + + try { + await test.step('Navigate to CRD instances page', async () => { + await navigateToCRDInstances(page, crd); + }); + + await test.step('Create ConsoleNotification instance via YAML editor', async () => { + await createCustomResourceViaYaml(page, crdObj); + }); + + await test.step('Verify additional printer columns on list page', async () => { + await page.goto(`/k8s/cluster/console.openshift.io~v1~${crd}`); + + // Verify additional printer column headers + await expect(page.getByTestId('additional-printer-column-header-Text')).toHaveText('Text'); + await expect(page.getByTestId('additional-printer-column-header-Location')).toHaveText( + 'Location', + ); + await expect(page.getByTestId('additional-printer-column-header-Age')).toHaveText('Age'); + + // Verify additional printer column data for our instance + const instanceRow = page.getByRole('row', { name: new RegExp(name) }); + await expect(instanceRow.getByTestId('additional-printer-column-data-Text')).toHaveText( + text, + ); + await expect(instanceRow.getByTestId('additional-printer-column-data-Location')).toHaveText( + location, + ); + await expect(instanceRow.getByTestId('additional-printer-column-data-Age')).toBeVisible(); + + // Created column should not exist since Age replaces it + await expect(page.getByTestId('column-header-Created')).not.toBeVisible(); + }); + + await test.step('Verify additional printer columns on details page', async () => { + // Navigate to details by clicking the instance link on the list page + // Using goto can cause a full page reload where model discovery races + const instanceRow = page.getByRole('row', { name: new RegExp(name) }); + await instanceRow.getByRole('link', { name }).click(); + await expect(page.getByRole('heading', { name })).toBeVisible(); + + await expect(page.getByTestId('additional-printer-columns')).toBeVisible(); + await expect(page.locator('[data-test-selector="details-item-label__Text"]')).toHaveText( + 'Text', + ); + await expect(page.locator('[data-test-selector="details-item-value__Text"]')).toHaveText( + text, + ); + await expect( + page.locator('[data-test-selector="details-item-label__Location"]'), + ).toHaveText('Location'); + await expect( + page.locator('[data-test-selector="details-item-value__Location"]'), + ).toHaveText(location); + await expect(page.locator('[data-test-selector="details-item-label__Age"]')).toHaveText( + 'Age', + ); + await expect(page.locator('[data-test-selector="details-item-value__Age"]')).toBeVisible(); + }); + + await test.step('Verify notification banner appears', async () => { + const notification = page.locator(`[data-test="${name}-${location}"]`); + await expect(notification).toBeVisible(); + await expect(notification).toContainText(text); + }); + + await test.step('Modify ConsoleNotification to change location and text', async () => { + await page.goto(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}/yaml`); + + await updateCustomResourceViaYaml(page, (obj) => { + obj.spec.location = altLocation; + obj.spec.text = altText; + return obj; + }); + }); + + await test.step('Verify modified notification banner appears', async () => { + const altNotification = page.locator(`[data-test="${name}-${altLocation}"]`); + await expect(altNotification).toBeVisible(); + await expect(altNotification).toContainText(altText); + }); + + await test.step('Delete the ConsoleNotification instance', async () => { + await k8sClient.deleteCustomResource( + 'console.openshift.io', + 'v1', + '', + 'consolenotifications', + name, + ); + }); + } finally { + try { + await k8sClient.deleteCustomResource( + 'console.openshift.io', + 'v1', + '', + 'consolenotifications', + name, + ); + } catch (error) { + // Ignore if already deleted + } + } + }); +}); diff --git a/frontend/e2e/tests/console/crd-extensions/console-yaml-sample.spec.ts b/frontend/e2e/tests/console/crd-extensions/console-yaml-sample.spec.ts new file mode 100644 index 00000000000..aff3b46c683 --- /dev/null +++ b/frontend/e2e/tests/console/crd-extensions/console-yaml-sample.spec.ts @@ -0,0 +1,141 @@ +import { test, expect } from '../../../fixtures'; +import KubernetesClient from '../../../clients/kubernetes-client'; +import { + createCustomResourceViaYaml, + navigateToCRDInstances, + saveYamlChanges, + waitForYamlEditor, +} from './crd-test-utils'; + +const crd = 'ConsoleYAMLSample'; +const testJobName = 'test-job'; + +test.describe(`${crd} CRD`, { tag: ['@admin'] }, () => { + let k8sClient: KubernetesClient; + + test.beforeEach(async ({ k8sClient: client }) => { + k8sClient = client; + }); + + test(`creates, displays, tests, and deletes a new ${crd} instance`, async ({ page }) => { + const name = `console-yaml-sample-test-${Date.now()}`; + const projectName = `test-project-${Date.now()}`; + + const crdObj = { + apiVersion: 'console.openshift.io/v1', + kind: crd, + metadata: { + name, + }, + spec: { + targetResource: { + apiVersion: 'batch/v1', + kind: 'Job', + }, + title: 'Example Job', + description: 'An example Job YAML sample', + yaml: `apiVersion: batch/v1 +kind: Job +metadata: + name: ${testJobName} + namespace: ${projectName} +spec: + template: + metadata: + name: countdown + namespace: ${projectName} + spec: + containers: + - name: counter + image: centos:7 + command: + - "bin/bash" + - "-c" + - "echo Test" + restartPolicy: Never`, + }, + }; + + await k8sClient.createProject(projectName); + + try { + await test.step('Navigate to CRD instances page', async () => { + await navigateToCRDInstances(page, crd); + }); + + await test.step('Create ConsoleYAMLSample instance via YAML editor', async () => { + await createCustomResourceViaYaml(page, crdObj); + }); + + await test.step('Verify no additional printer columns on list page', async () => { + await page.goto(`/k8s/cluster/console.openshift.io~v1~${crd}`); + + // Additional printer columns should not exist for this CRD + await expect( + page.getByTestId(/^additional-printer-column-header-/).first(), + ).not.toBeVisible(); + + // Created column should exist since Age does not + await expect(page.getByTestId('column-header-Created')).toBeVisible(); + }); + + await test.step('Verify instance on details page', async () => { + // Navigate to details by clicking the instance link on the list page + // Using goto can cause a full page reload where model discovery races + const instanceRow = page.getByRole('row', { name: new RegExp(name) }); + await instanceRow.getByRole('link', { name }).click(); + await expect(page.getByRole('heading', { name })).toBeVisible(); + + // Additional printer columns should not exist + await expect(page.getByTestId('additional-printer-columns')).not.toBeVisible(); + }); + + await test.step('Create Job from YAML sample via resource sidebar', async () => { + await page.goto(`/k8s/ns/${projectName}/batch~v1~Job`); + await page.getByTestId('item-create').click(); + + await waitForYamlEditor(page); + + // Switch to Samples tab in the resource sidebar + await page.getByRole('tab', { name: 'Samples' }).click(); + + // Wait for sample to load and click "Try it" to load into editor + await page.getByTestId('load-sample').first().click(); + + await saveYamlChanges(page); + }); + + await test.step('Verify Job was created from sample', async () => { + await page.goto(`/k8s/ns/${projectName}/batch~v1~Job/${testJobName}`); + await expect(page.getByRole('heading', { name: testJobName })).toBeVisible(); + }); + + await test.step('Delete the ConsoleYAMLSample instance', async () => { + await k8sClient.deleteCustomResource( + 'console.openshift.io', + 'v1', + '', + 'consoleyamlsamples', + name, + ); + }); + } finally { + try { + await k8sClient.deleteCustomResource( + 'console.openshift.io', + 'v1', + '', + 'consoleyamlsamples', + name, + ); + } catch (error) { + // Ignore if already deleted + } + try { + await k8sClient.deleteProject(projectName); + } catch (error) { + // Ignore if already deleted + } + } + }); +}); diff --git a/frontend/e2e/tests/console/crd-extensions/crd-test-utils.ts b/frontend/e2e/tests/console/crd-extensions/crd-test-utils.ts new file mode 100644 index 00000000000..a61a7bc7ab2 --- /dev/null +++ b/frontend/e2e/tests/console/crd-extensions/crd-test-utils.ts @@ -0,0 +1,149 @@ +import { Page, expect } from '@playwright/test'; +import jsYaml from 'js-yaml'; +import KubernetesClient from '../../../clients/kubernetes-client'; +import { Navigation } from '../../../pages/navigation'; + +/** + * Navigate to CRD instances page via kebab menu "View instances" + */ +export async function navigateToCRDInstances(page: Page, crd: string): Promise { + const nav = new Navigation(page); + await nav.navigateToCRDs(); + + const searchInput = page.locator('input[placeholder="Filter by name"]'); + await searchInput.waitFor({ state: 'visible', timeout: 30000 }); + await searchInput.fill(crd); + + const crdRow = page.getByRole('row', { name: new RegExp(crd, 'i') }); + await crdRow.waitFor({ state: 'visible', timeout: 30000 }); + + const kebabButton = crdRow.getByTestId('kebab-button'); + await kebabButton.click(); + await page.getByRole('menuitem', { name: 'View instances' }).click(); + + await expect(page.getByRole('heading', { name: crd })).toBeVisible(); +} + +/** + * Navigate to CRD details page via link click, then switch to Instances tab + */ +export async function navigateToCRDInstancesViaDetails(page: Page, crd: string): Promise { + const nav = new Navigation(page); + await nav.navigateToCRDs(); + + const searchInput = page.locator('input[placeholder="Filter by name"]'); + await searchInput.waitFor({ state: 'visible', timeout: 30000 }); + await searchInput.fill(crd); + + const crdRow = page.getByRole('row', { name: new RegExp(crd, 'i') }); + await crdRow.waitFor({ state: 'visible', timeout: 30000 }); + + const crdLink = crdRow.getByRole('link', { name: new RegExp(crd, 'i') }); + await crdLink.click(); + + await page.getByRole('tab', { name: 'Instances' }).click(); + await expect(page.getByRole('heading', { name: crd })).toBeVisible(); +} + +/** + * Wait for Monaco YAML editor to load and be ready + */ +export async function waitForYamlEditor(page: Page): Promise { + await page.getByRole('button', { name: 'Copy code to clipboard' }).waitFor(); +} + +/** + * Get the current content from the Monaco YAML editor + */ +export async function getYamlEditorContent(page: Page): Promise { + return page.evaluate(() => { + const monacoEditor = (window as any).monaco?.editor?.getModels()?.[0]; + return monacoEditor?.getValue() || ''; + }); +} + +/** + * Set content in the Monaco YAML editor + */ +export async function setYamlEditorContent(page: Page, yaml: string): Promise { + await page.evaluate((yamlContent) => { + const monacoEditor = (window as any).monaco?.editor?.getModels()?.[0]; + monacoEditor?.setValue(yamlContent); + }, yaml); +} + +/** + * Merge an object with the existing YAML editor content and set it + */ +export async function mergeYamlEditorContent(page: Page, obj: any): Promise { + const existingContent = await getYamlEditorContent(page); + const existingObj = jsYaml.load(existingContent) as Record; + const mergedObj = { ...existingObj, ...obj }; + const newYaml = jsYaml.dump(mergedObj, { sortKeys: true }); + await setYamlEditorContent(page, newYaml); +} + +/** + * Replace the entire YAML editor content with a new object + */ +export async function replaceYamlEditorContent(page: Page, obj: any): Promise { + const newYaml = jsYaml.dump(obj, { sortKeys: true }); + await setYamlEditorContent(page, newYaml); +} + +/** + * Save YAML changes and verify no errors + */ +export async function saveYamlChanges(page: Page): Promise { + await page.getByTestId('save-changes').click(); + await expect(page.getByTestId('yaml-error')).not.toBeVisible(); +} + +/** + * Create a custom resource via YAML editor by merging with template + */ +export async function createCustomResourceViaYaml(page: Page, obj: any): Promise { + await page.getByTestId('item-create').click(); + await waitForYamlEditor(page); + await mergeYamlEditorContent(page, obj); + await saveYamlChanges(page); +} + +/** + * Update a custom resource via YAML editor by modifying existing content + */ +export async function updateCustomResourceViaYaml( + page: Page, + modifier: (existingObj: any) => any, +): Promise { + await waitForYamlEditor(page); + + const existingContent = await getYamlEditorContent(page); + const existingObj = jsYaml.load(existingContent) as Record; + const modifiedObj = modifier(existingObj); + const newYaml = jsYaml.dump(modifiedObj, { sortKeys: true }); + + await setYamlEditorContent(page, newYaml); + await saveYamlChanges(page); +} + +/** + * Delete a custom resource and verify it's gone from the list + */ +export async function deleteCustomResourceAndVerify( + page: Page, + k8sClient: KubernetesClient, + apiGroup: string, + version: string, + namespace: string, + plural: string, + name: string, + crd: string, +): Promise { + await k8sClient.deleteCustomResource(apiGroup, version, namespace, plural, name); + + // Verify deletion by checking the instance is gone + await page.goto(`/k8s/cluster/${apiGroup}~${version}~${crd}`); + const instanceRow = page.getByRole('row', { name: new RegExp(name) }); + await expect(instanceRow).not.toBeVisible({ timeout: 10000 }); +} diff --git a/frontend/e2e/tests/console/events/events.spec.ts b/frontend/e2e/tests/console/events/events.spec.ts new file mode 100644 index 00000000000..fb2f7dc4636 --- /dev/null +++ b/frontend/e2e/tests/console/events/events.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '../../../fixtures'; +import KubernetesClient from '../../../clients/kubernetes-client'; + +test.describe('Events', { tag: ['@admin'] }, () => { + let k8sClient: KubernetesClient; + let podName: string; + let projectName: string; + + test.beforeEach(async ({ k8sClient: client }) => { + k8sClient = client; + + const timestamp = Date.now(); + podName = `event-test-pod-${timestamp}`; + projectName = `test-events-${timestamp}`; + + await k8sClient.createProject(projectName); + + await k8sClient.createPod({ + apiVersion: 'v1', + kind: 'Pod', + metadata: { + name: podName, + namespace: projectName, + }, + spec: { + securityContext: { + runAsNonRoot: true, + seccompProfile: { type: 'RuntimeDefault' }, + }, + containers: [ + { + name: 'httpd', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest-error', + securityContext: { + allowPrivilegeEscalation: false, + capabilities: { drop: ['ALL'] }, + }, + }, + ], + }, + }); + }); + + test.afterEach(async () => { + try { + await k8sClient.deletePod(podName, projectName); + } catch { + // Ignore if already deleted + } + try { + await k8sClient.deleteProject(projectName); + } catch { + // Ignore if already deleted + } + }); + + test('displays events for a newly created Pod', async ({ page }) => { + await test.step('Navigate to events page and verify pod events appear', async () => { + await page.goto(`/k8s/ns/${projectName}/events`); + + await expect(page.getByTestId(podName).first()).toBeVisible({ timeout: 30000 }); + }); + + await test.step('Filter events by Warning type', async () => { + await page.getByTestId('console-select-menu-toggle').click(); + await page.getByTestId('console-select-item-warning').click(); + + const totals = page.getByTestId('event-totals'); + await expect(totals).toBeVisible(); + + const warnings = page.getByTestId('event-warning'); + await expect(warnings.first()).toBeVisible(); + expect(await warnings.count()).toBeGreaterThan(0); + }); + + await test.step('Filter events by text', async () => { + await page.getByTestId('item-filter').fill('Error: ImagePullBackOff'); + + await expect(page.getByTestId('event-totals')).toContainText('1 event'); + await expect(page.getByTestId('event-warning')).toHaveCount(1); + }); + }); +}); diff --git a/frontend/e2e/tests/console/favorites/favorites.spec.ts b/frontend/e2e/tests/console/favorites/favorites.spec.ts new file mode 100644 index 00000000000..9d9ceefe739 --- /dev/null +++ b/frontend/e2e/tests/console/favorites/favorites.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from '../../../fixtures'; + +test.describe('Favorites', { tag: ['@admin'] }, () => { + test('adds, displays, removes, and limits favorites', async ({ page }) => { + const sidebar = page.locator('#page-sidebar'); + + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + await test.step('Verify no favorites message when none are added', async () => { + await sidebar.getByRole('button', { name: 'Favorites' }).click(); + await expect(page.getByTestId('no-favorites-message')).toBeVisible(); + }); + + await test.step('Open Add to Favorites modal', async () => { + await page.getByTestId('favorite-button').click(); + const dialog = page.getByRole('dialog'); + await expect(dialog).toContainText('Add to favorites'); + }); + + await test.step('Save a favorite with custom name', async () => { + const nameInput = page.getByTestId('input-name'); + await expect(nameInput).toHaveValue('Overview'); + await nameInput.clear(); + await nameInput.fill('test-favorite'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(sidebar).toContainText('test-favorite'); + }); + + await test.step('Remove a favorite by clicking the favorite button again', async () => { + await page.getByTestId('favorite-button').click(); + + await sidebar.getByRole('button', { name: 'Favorites' }).click(); + await expect(page.getByTestId('no-favorites-message')).toBeVisible(); + }); + + await test.step('Remove a favorite from the left navigation menu', async () => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + await page.getByTestId('favorite-button').click(); + const dialog = page.getByRole('dialog'); + await expect(dialog).toContainText('Add to favorites'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(sidebar).toContainText('Overview'); + + await page.getByTestId('remove-favorite-button').click(); + + await sidebar.getByRole('button', { name: 'Favorites' }).click(); + await expect(page.getByTestId('no-favorites-message')).toBeVisible(); + }); + + await test.step('Disable add to favorite button when limit reached', async () => { + const pages = [ + '/', + '/k8s/all-namespaces/core~v1~Pod', + '/k8s/all-namespaces/apps~v1~Deployment', + '/k8s/all-namespaces/core~v1~Secret', + '/k8s/all-namespaces/core~v1~ConfigMap', + '/k8s/cluster/core~v1~Node', + '/k8s/all-namespaces/batch~v1~CronJob', + '/k8s/all-namespaces/batch~v1~Job', + '/k8s/all-namespaces/apps~v1~ReplicaSet', + '/k8s/all-namespaces/core~v1~ReplicationController', + ]; + + for (let i = 0; i < pages.length; i++) { + await page.goto(pages[i]); + await page.waitForLoadState('networkidle'); + await page.getByTestId('favorite-button').first().click(); + const dialog = page.getByRole('dialog'); + await expect(dialog).toContainText('Add to favorites'); + const nameInput = page.getByTestId('input-name'); + await nameInput.clear(); + await nameInput.fill(`test-favorite-${i}`); + await nameInput.press('Enter'); + await expect(dialog).not.toBeVisible(); + } + + await page.goto('/k8s/all-namespaces/apps~v1~DaemonSet'); + await page.waitForLoadState('networkidle'); + await expect(page.getByTestId('favorite-button').first()).toBeDisabled(); + }); + }); +}); diff --git a/frontend/e2e/tests/console/i18n/pseudolocalization.spec.ts b/frontend/e2e/tests/console/i18n/pseudolocalization.spec.ts new file mode 100644 index 00000000000..ff918e87b47 --- /dev/null +++ b/frontend/e2e/tests/console/i18n/pseudolocalization.spec.ts @@ -0,0 +1,75 @@ +import type { Page } from '@playwright/test'; +import { test, expect } from '../../../fixtures'; + +const PSEUDO_LOCALIZED_PATTERN = /\[[^a-zA-Z]+\]/; + +const dashboardUrl = '/dashboards?pseudolocalization=true&lng=en'; + +async function expectPseudoLocalized(page: Page, testId: string) { + const elements = page.getByTestId(testId); + const count = await elements.count(); + expect(count).toBeGreaterThan(0); + for (let i = 0; i < count; i++) { + const text = await elements.nth(i).textContent(); + if (text && text.length > 0) { + expect(text).toMatch(PSEUDO_LOCALIZED_PATTERN); + } + } +} + +test.describe('Pseudolocalization', { tag: ['@admin'] }, () => { + test.use({ locale: 'en' }); + + test('pseudolocalizes dashboard masthead, activity card, and utilization card', async ({ + page, + }) => { + await page.goto(dashboardUrl); + await page.waitForLoadState('networkidle'); + + await test.step('Verify masthead help menu is pseudolocalized', async () => { + await page.getByTestId('help-dropdown-toggle').click(); + + const dropdown = page.getByTestId('help-dropdown'); + const menus = dropdown.locator('ul[role="menu"]'); + await expect(menus).toHaveCount(2); + + const menuItems = dropdown.getByTestId('application-launcher-item'); + const count = await menuItems.count(); + expect(count).toBeGreaterThan(0); + for (let i = 0; i < count; i++) { + const text = await menuItems.nth(i).textContent(); + if (text && text.length > 0) { + expect(text).toMatch(PSEUDO_LOCALIZED_PATTERN); + } + } + + await page.getByTestId('help-dropdown-toggle').click(); + }); + + await test.step('Verify activity card is pseudolocalized', async () => { + await expectPseudoLocalized(page, 'activity'); + await expectPseudoLocalized(page, 'activity-recent-title'); + await expectPseudoLocalized(page, 'ongoing-title'); + await expectPseudoLocalized(page, 'events-view-all-link'); + await expectPseudoLocalized(page, 'events-pause-button'); + }); + + await test.step('Verify utilization card is pseudolocalized', async () => { + const utilizationCard = page.getByTestId('utilization-card'); + await expect(utilizationCard).toBeVisible(); + + const title = utilizationCard.getByTestId('utilization-card__title'); + await expect(title).toHaveText(PSEUDO_LOCALIZED_PATTERN); + + const itemTexts = utilizationCard.getByTestId('utilization-card-item-text'); + const count = await itemTexts.count(); + expect(count).toBeGreaterThan(0); + for (let i = 0; i < count; i++) { + const text = await itemTexts.nth(i).textContent(); + if (text && text.length > 0) { + expect(text).toMatch(PSEUDO_LOCALIZED_PATTERN); + } + } + }); + }); +}); diff --git a/frontend/e2e/tests/olm/create-namespace.spec.ts b/frontend/e2e/tests/olm/create-namespace.spec.ts new file mode 100644 index 00000000000..d7498e7a9a1 --- /dev/null +++ b/frontend/e2e/tests/olm/create-namespace.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; + +const operatorName = '3scale API Management'; + +test.describe('Create namespace from install operators', { tag: ['@admin'] }, () => { + let k8sClient: KubernetesClient; + let nsName: string; + + test.beforeEach(async ({ k8sClient: client }) => { + k8sClient = client; + nsName = `test-create-ns-${Date.now()}`; + }); + + test.afterEach(async () => { + try { + await k8sClient.deleteCustomResource( + 'operators.coreos.com', + 'v1alpha1', + nsName, + 'subscriptions', + '3scale-community-operator', + ); + } catch { + // Ignore if not created + } + try { + const csvs = (await k8sClient.listCustomResources( + 'operators.coreos.com', + 'v1alpha1', + nsName, + 'clusterserviceversions', + )) as Array<{ metadata?: { name?: string } }>; + for (const csv of csvs) { + if (csv.metadata?.name) { + await k8sClient.deleteCustomResource( + 'operators.coreos.com', + 'v1alpha1', + nsName, + 'clusterserviceversions', + csv.metadata.name, + ); + } + } + } catch { + // Ignore cleanup errors + } + try { + await k8sClient.deleteProject(nsName); + } catch { + // Ignore if not created + } + }); + + test('creates namespace from operator install page', async ({ page }) => { + await test.step('Navigate to catalog and open operator details', async () => { + await page.goto('/catalog/ns/default?catalogType=operator'); + await page.waitForLoadState('networkidle'); + + await page.getByPlaceholder('Filter by keyword...').fill(operatorName); + await page.getByTestId(`operator-${operatorName}`).click(); + }); + + await test.step('Click Install in operator details modal', async () => { + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + + const installLink = dialog.getByRole('button', { name: 'Install' }); + await expect(installLink).toBeVisible(); + await installLink.click(); + }); + + await test.step('Select single namespace installation mode', async () => { + await expect(page.getByRole('heading', { name: 'Install Operator' })).toBeVisible(); + const radio = page.getByTestId('A specific namespace on the cluster-radio-input'); + await expect(radio).toBeVisible(); + await radio.click(); + }); + + await test.step('Create a new namespace from the dropdown', async () => { + await page.getByTestId('dropdown-selectbox').click(); + await page.getByTestId(/^console-select-item-Create_/).click(); + + await expect(page.getByTestId('input-name')).toBeVisible(); + await page.getByTestId('input-name').fill(nsName); + await page.getByTestId('confirm-action').click(); + + await expect(page.getByRole('dialog')).not.toBeVisible(); + }); + + await test.step('Verify the dropdown shows the new namespace', async () => { + await expect(page.getByTestId('dropdown-selectbox')).toContainText(nsName); + }); + + await test.step('Install the operator and verify success', async () => { + await page.getByTestId('install-operator').click(); + + const successButton = page.getByTestId('view-installed-operators-btn'); + await expect(successButton).toContainText(`View installed Operators in Namespace ${nsName}`, { + timeout: 60000, + }); + }); + }); +}); diff --git a/frontend/packages/console-app/src/components/nodes/node-dashboard/UtilizationCard.tsx b/frontend/packages/console-app/src/components/nodes/node-dashboard/UtilizationCard.tsx index 25db4888e44..b9be9a2aaa5 100644 --- a/frontend/packages/console-app/src/components/nodes/node-dashboard/UtilizationCard.tsx +++ b/frontend/packages/console-app/src/components/nodes/node-dashboard/UtilizationCard.tsx @@ -51,7 +51,7 @@ const UtilizationCard: FC = () => { ); return ( - + = ({ 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/console-shared/src/components/links/ExternalLink.tsx b/frontend/packages/console-shared/src/components/links/ExternalLink.tsx index 4f8062995ab..c8fcfb731b5 100644 --- a/frontend/packages/console-shared/src/components/links/ExternalLink.tsx +++ b/frontend/packages/console-shared/src/components/links/ExternalLink.tsx @@ -48,6 +48,7 @@ export const ExternalLink: FC = ({ // Overriding the `display` breaks the icon spacing, so we need to add our own iconProps={{ className: 'pf-v6-u-ml-xs' }} data-test-id={dataTestID} + data-test={dataTestID} href={href} isInline variant="link" diff --git a/frontend/packages/console-shared/src/hooks/useCRDAdditionalPrinterColumns.ts b/frontend/packages/console-shared/src/hooks/useCRDAdditionalPrinterColumns.ts index b02443a3521..b178151f969 100644 --- a/frontend/packages/console-shared/src/hooks/useCRDAdditionalPrinterColumns.ts +++ b/frontend/packages/console-shared/src/hooks/useCRDAdditionalPrinterColumns.ts @@ -13,6 +13,10 @@ export const useCRDAdditionalPrinterColumns = ( const [loaded, setLoaded] = useState(false); useEffect(() => { + if (!model) { + setLoaded(true); + return; + } coFetchJSON(`/api/console/crd-columns/${model.plural}.${model.apiGroup}`) .then((response) => { setCRDAPC(response); @@ -23,7 +27,7 @@ export const useCRDAdditionalPrinterColumns = ( // eslint-disable-next-line no-console console.log(e.message); }); - }, [model.plural, model.apiGroup]); + }, [model]); - return [CRDAPC?.[model.apiVersion] ?? [], loaded]; + return [CRDAPC?.[model?.apiVersion] ?? [], loaded]; }; 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/tests/crd-extensions/console-cli-download.cy.ts b/frontend/packages/integration-tests/tests/crd-extensions/console-cli-download.cy.ts deleted file mode 100644 index bcca57a8aa3..00000000000 --- a/frontend/packages/integration-tests/tests/crd-extensions/console-cli-download.cy.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { safeLoad, safeDump } from 'js-yaml'; -import * as _ from 'lodash'; -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import * as yamlEditor from '../../views/yaml-editor'; - -const crd = 'ConsoleCLIDownload'; - -describe(`${crd} CRD`, () => { - const name = `${testName}-ccd`; - // cannot use default YAML template since it contains new lines - // in the description and that breaks with safeLoad - const crdObj = { - apiVersion: 'console.openshift.io/v1', - kind: crd, - metadata: { - name, - }, - spec: { - displayName: name, - description: - 'This is an example CLI download description that can include markdown such as paragraphs, unordered lists, code, [links](https://www.example.com), etc.', - links: [{ href: 'https://www.example.com', text: 'Example CLI Download' }], - }, - }; - - before(() => { - cy.login(); - }); - - beforeEach(() => { - cy.initAdmin(); - }); - - afterEach(() => { - checkErrors(); - }); - - it(`creates, displays, and deletes a new ${crd} instance`, () => { - cy.visit(`/k8s/cluster/customresourcedefinitions?name=${crd}`); - listPage.isCreateButtonVisible(); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickKebabAction(crd, 'View instances'); - listPage.titleShouldHaveText(crd); - listPage.clickCreateYAMLbutton(); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep({}, crdObj, safeLoad(content)); - yamlEditor.setEditorContent(safeDump(newContent, { sortKeys: true })).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - }); - }); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}`); - detailsPage.titleShouldContain(name); - - cy.visit(`/command-line-tools`); - cy.get(`[data-test-id=${name}]`).should('contain', name); - - cy.exec(`oc delete ${crd} ${name}`); - }); -}); diff --git a/frontend/packages/integration-tests/tests/crd-extensions/console-external-log-link.cy.ts b/frontend/packages/integration-tests/tests/crd-extensions/console-external-log-link.cy.ts deleted file mode 100644 index 7d3f91f4fb5..00000000000 --- a/frontend/packages/integration-tests/tests/crd-extensions/console-external-log-link.cy.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { safeLoad, safeDump } from 'js-yaml'; -import * as _ from 'lodash'; -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import { modal } from '../../views/modal'; -import * as yamlEditor from '../../views/yaml-editor'; - -const crd = 'ConsoleExternalLogLink'; - -describe(`${crd} CRD`, () => { - const name = `${testName}-cell`; - const podName = `${testName}-pod`; - const cell = `[data-test-id=${name}]`; - const text = `${name} Logs`; - const namespaceFilter = '^openshift-'; - - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.deleteProjectWithCLI(testName); - }); - - it(`creates, displays, modifies, and deletes a new ${crd} instance`, () => { - cy.visit(`/k8s/cluster/customresourcedefinitions?name=${crd}`); - listPage.dvRows.shouldBeLoaded(); - listPage.rows.clickRowByName(crd); - detailsPage.titleShouldContain('consoleexternalloglinks.console.openshift.io'); - detailsPage.selectTab('Instances'); - listPage.clickCreateYAMLbutton(); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep( - {}, - { metadata: { name }, spec: { text } }, - safeLoad(content), - ); - yamlEditor.setEditorContent(safeDump(newContent, { sortKeys: true })).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - }); - }); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}`); - detailsPage.titleShouldContain(name); - - cy.visit(`/k8s/ns/${testName}/pods`); - listPage.clickCreateYAMLbutton(); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep( - {}, - { metadata: { name: podName, labels: { app: name } } }, - safeLoad(content), - ); - yamlEditor.setEditorContent(safeDump(newContent, { sortKeys: true })).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - }); - }); - - cy.visit(`/k8s/ns/${testName}/pods/${podName}/logs`); - cy.get(cell).should('exist'); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}/yaml`); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep({}, { spec: { namespaceFilter } }, safeLoad(content)); - yamlEditor.setEditorContent(safeDump(newContent, { sortKeys: true })).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - }); - }); - - cy.visit(`/k8s/ns/${testName}/pods/${podName}/logs`); - cy.get('[data-test="resource-log-toolbar"').should('exist'); - cy.get(cell).should('not.exist'); - - cy.visit(`/k8s/ns/${testName}/pods?name=${podName}`); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickKebabAction(podName, 'Delete Pod'); - modal.shouldBeOpened(); - modal.modalTitleShouldContain('Delete Pod'); - modal.submit(); - modal.shouldBeClosed(); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}`); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickKebabAction(name, `Delete ${crd}`); - modal.shouldBeOpened(); - modal.modalTitleShouldContain(`Delete ${crd}`); - modal.submit(); - modal.shouldBeClosed(); - }); -}); diff --git a/frontend/packages/integration-tests/tests/crd-extensions/console-link.cy.ts b/frontend/packages/integration-tests/tests/crd-extensions/console-link.cy.ts deleted file mode 100644 index 21d3c2b301e..00000000000 --- a/frontend/packages/integration-tests/tests/crd-extensions/console-link.cy.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { safeLoad, safeDump } from 'js-yaml'; -import * as _ from 'lodash'; -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import { modal } from '../../views/modal'; -import * as yamlEditor from '../../views/yaml-editor'; - -const crd = 'ConsoleLink'; - -describe(`${crd} CRD`, () => { - const name = `${testName}-cl`; - const testObjs = [ - { - name, - dropdownMenuName: 'help menu', - dropdownMenu: '[data-test=help-dropdown]', - dropdownToggle: '[data-test=help-dropdown-toggle]', - menuLinkLocation: 'HelpMenu', - menuLinkText: `${name} help menu link`, - }, - { - name, - dropdownMenuName: 'user menu', - dropdownMenu: '[data-test=user-dropdown]', - dropdownToggle: '[data-test=user-dropdown-toggle]', - menuLinkLocation: 'UserMenu', - menuLinkText: `${name} user menu link`, - }, - ]; - - before(() => { - cy.login(); - }); - - beforeEach(() => { - cy.initAdmin(); - }); - - afterEach(() => { - // delete potential orphaned consolelink from possible failed try - cy.exec(`oc delete consolelink ${name}`, { - failOnNonZeroExit: false, - }); - checkErrors(); - }); - - testObjs.forEach( - ({ - name: instanceName, - dropdownMenuName, - dropdownMenu, - dropdownToggle, - menuLinkLocation, - menuLinkText, - }) => { - it(`creates, displays, and deletes a new ${crd} ${dropdownMenuName} instance`, () => { - cy.visit(`/k8s/cluster/customresourcedefinitions?name=${crd}`); - listPage.isCreateButtonVisible(); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickKebabAction(crd, 'View instances'); - listPage.titleShouldHaveText(crd); - listPage.clickCreateYAMLbutton(); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep( - {}, - { - metadata: { name: instanceName }, - spec: { location: menuLinkLocation, text: menuLinkText }, - }, - safeLoad(content), - ); - yamlEditor.setEditorContent(safeDump(newContent, { sortKeys: true })).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - }); - }); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}`); - detailsPage.sectionHeaderShouldExist('ConsoleLink details'); - detailsPage.titleShouldContain(name); - - cy.get(dropdownToggle).click(); - cy.get(dropdownMenu) - .find('[data-test="application-launcher-item"]') - .contains(menuLinkText) - .should('exist'); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}`); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickKebabAction(name, `Delete ${crd}`); - modal.shouldBeOpened(); - modal.modalTitleShouldContain(`Delete ${crd}`); - modal.submit(); - modal.shouldBeClosed(); - }); - }, - ); -}); diff --git a/frontend/packages/integration-tests/tests/crd-extensions/console-notification.cy.ts b/frontend/packages/integration-tests/tests/crd-extensions/console-notification.cy.ts deleted file mode 100644 index e3feed43d4b..00000000000 --- a/frontend/packages/integration-tests/tests/crd-extensions/console-notification.cy.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { safeLoad, safeDump } from 'js-yaml'; -import * as _ from 'lodash'; -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import * as yamlEditor from '../../views/yaml-editor'; - -const crd = 'ConsoleNotification'; - -describe(`${crd} CRD`, () => { - const name = `${testName}-cn`; - const location = 'BannerTop'; - const altLocation = 'BannerBottom'; - const text = `${name} notification that appears ${location}`; - const altText = `${name} notification that appears ${altLocation}`; - const notification = `[data-test=${name}-${location}]`; - const altNotification = `[data-test=${name}-${altLocation}]`; - - before(() => { - cy.login(); - }); - - beforeEach(() => { - cy.initAdmin(); - }); - - afterEach(() => { - checkErrors(); - }); - - it(`creates, displays, modifies, and deletes a new ${crd} instance`, () => { - cy.visit(`/k8s/cluster/customresourcedefinitions?name=${crd}`); - listPage.isCreateButtonVisible(); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickKebabAction(crd, 'View instances'); - listPage.titleShouldHaveText(crd); - listPage.clickCreateYAMLbutton(); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep( - {}, - { metadata: { name }, spec: { location, text } }, - safeLoad(content), - ); - yamlEditor.setEditorContent(safeDump(newContent, { sortKeys: true })).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - }); - }); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}`); - listPage.dvRows.shouldBeLoaded(); - cy.log('Additional printer columns should exist.'); - cy.byTestID('additional-printer-column-header-Text').should('have.text', 'Text'); - cy.byTestID('additional-printer-column-data-Text').should('have.text', text); - cy.byTestID('additional-printer-column-header-Location').should('have.text', 'Location'); - cy.byTestID('additional-printer-column-data-Location').should('have.text', location); - cy.byTestID('additional-printer-column-header-Age').should('have.text', 'Age'); - cy.byTestID('additional-printer-column-data-Age').should('exist'); - cy.log('Created date should not exist since Age does.'); - cy.byTestID('column-header-Created').should('not.exist'); - cy.byTestID('column-data-Created').should('not.exist'); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}`); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(name); - cy.log('Additional printer columns should exist.'); - cy.byTestID('additional-printer-columns').should('exist'); - cy.byTestSelector('details-item-label__Text').should('have.text', 'Text'); - cy.byTestSelector('details-item-value__Text').should('have.text', text); - cy.byTestSelector('details-item-label__Location').should('have.text', 'Location'); - cy.byTestSelector('details-item-value__Location').should('have.text', location); - cy.byTestSelector('details-item-label__Age').should('have.text', 'Age'); - cy.byTestSelector('details-item-value__Age').should('exist'); - - cy.get(notification).contains(text).should('exist').and('be.visible'); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}/yaml`); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep( - {}, - { - metadata: { name }, - spec: { - location: altLocation, - text: altText, - }, - }, - safeLoad(content), - ); - yamlEditor.setEditorContent(safeDump(newContent, { sortKeys: true })).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - }); - }); - - cy.get(altNotification).contains(altText).should('exist').and('be.visible'); - - cy.exec(`oc delete ${crd} ${name}`); - }); -}); diff --git a/frontend/packages/integration-tests/tests/crd-extensions/console-yaml-sample.cy.ts b/frontend/packages/integration-tests/tests/crd-extensions/console-yaml-sample.cy.ts deleted file mode 100644 index aba2eaa343c..00000000000 --- a/frontend/packages/integration-tests/tests/crd-extensions/console-yaml-sample.cy.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { safeLoad, safeDump } from 'js-yaml'; -import * as _ from 'lodash'; -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import * as resourceSidebar from '../../views/resource-sidebar'; -import * as yamlEditor from '../../views/yaml-editor'; - -const crd = 'ConsoleYAMLSample'; -const testJobName = 'test-job'; - -describe(`${crd} CRD`, () => { - const name = `${testName}-cys`; - const namespace = name; - const crdObj = { - apiVersion: 'console.openshift.io/v1', - kind: 'ConsoleYAMLSample', - metadata: { - name, - }, - spec: { - targetResource: { - apiVersion: 'batch/v1', - kind: 'Job', - }, - title: 'Example Job', - description: 'An example Job YAML sample', - yaml: `apiVersion: batch/v1 -kind: Job -metadata: - name: ${testJobName} - namespace: ${namespace} - spec: - template: - metadata: - name: countdown - namespace: ${namespace} - spec: - containers: - - name: counter - image: centos:7 - command: - - "bin/bash" - - "-c" - - "echo Test" - restartPolicy: Never`, - }, - }; - - before(() => { - cy.login(); - cy.initAdmin(); - cy.createProjectWithCLI(testName); - }); - - after(() => { - cy.deleteProjectWithCLI(testName); - checkErrors(); - }); - - it(`creates, displays, tests and deletes a new ${crd} instance`, () => { - cy.visit(`/k8s/cluster/customresourcedefinitions?name=${crd}`); - listPage.isCreateButtonVisible(); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickKebabAction(crd, 'View instances'); - listPage.titleShouldHaveText(crd); - listPage.clickCreateYAMLbutton(); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep({}, crdObj, safeLoad(content)); - yamlEditor.setEditorContent(safeDump(newContent, { sortKeys: true })).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - }); - }); - - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}`); - listPage.dvRows.shouldBeLoaded(); - cy.log('Additional printer columns should not exist.'); - cy.get('[data-test^="additional-printer-column-header-"]').should('not.exist'); - cy.log('Created date should exist since Age does not.'); - cy.byTestID('column-header-Created').should('exist'); - cy.byTestID('column-data-Created').should('exist'); - - // Check if ConsoleYAMLSample CR was created - cy.visit(`/k8s/cluster/console.openshift.io~v1~${crd}/${name}`); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(name); - cy.log('Additional printer columns should not exist.'); - cy.byTestID('additional-printer-columns').should('not.exist'); - - // Create Job from sample - cy.visit(`k8s/ns/${testName}/batch~v1~Job`); - listPage.clickCreateYAMLbutton(); - resourceSidebar.isLoaded(); - yamlEditor.isLoaded(); - resourceSidebar.selectTab('Samples'); - resourceSidebar.isSampleListLoaded(); - resourceSidebar.loadFirstSample(); - yamlEditor.clickSaveCreateButton(); - - // Check if Job was created - cy.visit(`k8s/ns/${testName}/batch~v1~Job/${testJobName}`); - detailsPage.titleShouldContain(testJobName); - - // Delete CRD - cy.exec(`oc delete ${crd} ${name}`); - }); -}); diff --git a/frontend/packages/integration-tests/tests/events/events.cy.ts b/frontend/packages/integration-tests/tests/events/events.cy.ts deleted file mode 100644 index d48836c242b..00000000000 --- a/frontend/packages/integration-tests/tests/events/events.cy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { testName, checkErrors } from '../../support'; - -const name = `${testName}-event-test-pod`; -const testpod = { - apiVersion: 'v1', - kind: 'Pod', - metadata: { - name, - namespace: testName, - }, - spec: { - securityContext: { - runAsNonRoot: true, - seccompProfile: { - type: 'RuntimeDefault', - }, - }, - containers: [ - { - name: 'httpd', - image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest-error', // intentionally invalid image url - securityContext: { - allowPrivilegeEscalation: false, - capabilities: { - drop: ['ALL'], - }, - }, - }, - ], - }, -}; - -describe('Events', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - try { - cy.exec(`echo '${JSON.stringify(testpod)}' | oc create -n ${testName} -f -`); - } catch (error) { - console.error(`\nFailed to create pod ${name}:\n${error}`); - } - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - try { - cy.exec(`kubectl delete pods ${name} -n ${testName}`); - } catch (error) { - console.error(`\nFailed to delete pod ${name}:\n${error}`); - } - cy.deleteProjectWithCLI(testName); - }); - - it('displays events for a newly created Pod', () => { - cy.visit(`/k8s/ns/${testName}/events`); - - cy.log('Pod should exist in events list'); - cy.byTestID(name).should('exist'); - - cy.log('Event type filter should work'); - cy.byTestID('console-select-menu-toggle').click(); - cy.get('[data-test-dropdown-menu="warning"]').click(); - cy.byTestID('event-totals').should('have.text', 'Showing 3 events'); - cy.byTestID('event-warning').should('have.length', 3); - - cy.log('Event text filter should work'); - cy.byLegacyTestID('item-filter').type('Error: ImagePullBackOff'); - cy.byTestID('event-totals').should('have.text', 'Showing 1 event'); - cy.byTestID('event-warning').should('have.length', 1); - }); -}); diff --git a/frontend/packages/integration-tests/tests/favorite/favorite-option.cy.ts b/frontend/packages/integration-tests/tests/favorite/favorite-option.cy.ts deleted file mode 100644 index 328757b78cf..00000000000 --- a/frontend/packages/integration-tests/tests/favorite/favorite-option.cy.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { checkErrors } from '../../support'; -import { nav } from '../../views/nav'; - -describe('Favorites', () => { - before(() => { - // clear any existing sessions - Cypress.session.clearAllSavedSessions(); - cy.login(); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.visit('/'); - }); - - it('Should show No favorites added message when no favorites are added', () => { - nav.sidenav.clickNavLink(['Favorites']); - cy.byTestID('no-favorites-message').should('be.visible'); - }); - - it('Should open Add to Favorites modal on click of favorite icon', () => { - cy.get('[data-test="favorite-button"]').click({ force: true }); - cy.get('[role="dialog"]').contains('Add to favorites'); - }); - - it('Should save a favorite', () => { - cy.visit('/'); - cy.get('[data-test="favorite-button"]').click({ force: true }); - cy.get('[role="dialog"]').contains('Add to favorites'); - cy.get('#confirm-favorite-form-name').should('have.value', 'Overview'); - cy.get('#confirm-favorite-form-name') - .clear({ force: true }) - .type('test-favorite', { force: true }); - cy.contains('button', 'Save').click({ force: true }); - nav.sidenav.shouldHaveNavSection(['Favorites', 'test-favorite']); - }); - - it('Should remove a favorite', () => { - cy.visit('/'); - cy.get('[data-test="favorite-button"]').click({ force: true }); - nav.sidenav.clickNavLink(['Favorites']); - cy.byTestID('no-favorites-message').should('be.visible'); - }); - - it('Should remove a favorite from left navigation menu', () => { - cy.visit('/'); - cy.get('[data-test="favorite-button"]').click({ force: true }); - cy.get('[role="dialog"]').contains('Add to favorites'); - cy.contains('button', 'Save').click({ force: true }); - nav.sidenav.shouldHaveNavSection(['Favorites', 'Overview']); - cy.get('[data-test="remove-favorite-button"]').click({ force: true }); - nav.sidenav.clickNavLink(['Favorites']); - cy.byTestID('no-favorites-message').should('be.visible'); - }); - - it('Should disable add to favorite button when limit reached', () => { - const pages = [ - '/', - '/k8s/all-namespaces/core~v1~Pod', - '/k8s/all-namespaces/apps~v1~Deployment', - '/k8s/all-namespaces/core~v1~Secret', - '/k8s/all-namespaces/core~v1~ConfigMap', - '/k8s/cluster/core~v1~Node', - '/k8s/all-namespaces/batch~v1~CronJob', - '/k8s/all-namespaces/batch~v1~Job', - '/k8s/all-namespaces/apps~v1~ReplicaSet', - '/k8s/all-namespaces/core~v1~ReplicationController', - ]; - - pages.forEach((page, index) => { - cy.visit(page); - cy.get('[data-test="favorite-button"]').first().click({ force: true }); - cy.get('[role="dialog"]').contains('Add to favorites'); - cy.get('#confirm-favorite-form-name') - .clear({ force: true }) - .type(`test-favorite-${index}{enter}`, { force: true }); - if (index < pages.length - 1) { - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(1000); - } - }); - - cy.visit('/k8s/all-namespaces/apps~v1~DaemonSet'); - cy.get('[data-test="favorite-button"]').first().should('be.disabled'); - }); -}); diff --git a/frontend/packages/integration-tests/tests/i18n/pseudolocalization.cy.ts b/frontend/packages/integration-tests/tests/i18n/pseudolocalization.cy.ts deleted file mode 100644 index a8bdaf68691..00000000000 --- a/frontend/packages/integration-tests/tests/i18n/pseudolocalization.cy.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { checkErrors } from '../../support'; -import { masthead } from '../../views/masthead'; - -const url = '/dashboards?pseudolocalization=true&lng=en'; - -describe('Localization', () => { - before(() => { - cy.login(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('pseudolocalizes masthead', () => { - cy.log('test masthead'); - cy.visitWithDefaultLang(url); - masthead.clickMastheadLink('help-dropdown-toggle'); - cy.byTestID('help-dropdown').within(() => { - // wait for both console help menu items and additionalHelpActions items to load - cy.get('ul[role="menu"]').should('have.length', 2); - // Test that all links are translated - cy.get('ul[role="menu"] [role="menuitem"]').isPseudoLocalized(); - }); - }); - - it('pseudolocalizes activity card', () => { - cy.log('test activity card components'); - cy.visitWithDefaultLang(url); - cy.byTestID('activity').isPseudoLocalized(); - cy.byTestID('activity-recent-title').isPseudoLocalized(); - cy.byTestID('ongoing-title').isPseudoLocalized(); - cy.byTestID('events-view-all-link').isPseudoLocalized(); - cy.byTestID('events-pause-button').isPseudoLocalized(); - }); - - it('pseudolocalizes utilization card', () => { - cy.log('test utilization card components'); - cy.visitWithDefaultLang(url); - cy.byLegacyTestID('utilization-card').within(() => { - cy.byTestID('utilization-card__title').isPseudoLocalized(); - cy.byTestID('utilization-card-item-text').isPseudoLocalized(); - }); - }); -}); 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/packages/operator-lifecycle-manager/integration-tests/tests/create-namespace.cy.ts b/frontend/packages/operator-lifecycle-manager/integration-tests/tests/create-namespace.cy.ts deleted file mode 100644 index 45aa28f0f2b..00000000000 --- a/frontend/packages/operator-lifecycle-manager/integration-tests/tests/create-namespace.cy.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { checkErrors, testName } from '@console/cypress-integration-tests/support'; -import { modal } from '@console/cypress-integration-tests/views/modal'; - -describe('Create namespace from install operators', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.deleteProjectWithCLI(testName); - }); - - const nsName = `${testName}-ns`; - - it('creates namespace from operator install page', () => { - const operatorSelector = 'operator-Red Hat Integration - 3scale'; - const operatorName = 'Red Hat Integration - 3scale'; - cy.log('test namespace creation from dropdown'); - cy.visit(`/catalog/ns/${testName}`); - cy.byTestID('tab operator').click(); - cy.byTestID('search-catalog').type(operatorName); - cy.url().should('include', 'keyword'); - cy.byTestID(operatorSelector).click(); - // Wait for the Install button to be visible and have a valid href before clicking. - // The button is conditionally rendered based on useCtaLink hook, which processes - // the CTA href asynchronously. Clicking before href is set causes navigation to fail. - cy.byTestID('catalog-details-modal-cta').should('be.visible').and('have.attr', 'href'); - cy.byTestID('catalog-details-modal-cta').click(); - - // 3scale 2.11 supports only installation mode 'A specific namespace', - // so it was automatically selected. - // But starting with 2.12 it also supports 'All namespaces'. - // So it is required to select this radio option to specify the namespace. - // Regression test: Wait for radio button to be visible before clicking to avoid race conditions - // where the form re-renders asynchronously after the channel/version selectors load. - cy.byTestID('A specific namespace on the cluster-radio-input').should('be.visible').click(); - - // configure operator install ("^=Create_"" will match "Create_Namespace" and "Create_Project") - cy.byTestID('dropdown-selectbox').click().get('[data-test-dropdown-menu^="Create_"]').click(); - - // verify namespace modal is opened - modal.shouldBeOpened(); - cy.byTestID('input-name').type(nsName); - modal.submit(); - modal.shouldBeClosed(); - - // verify the dropdown selection shows the newly created namespace - cy.byTestID('dropdown-selectbox').should('contain', `${nsName}`); - - cy.get('button').contains('Install').click(); - - // verify operator began installation - cy.byTestID('view-installed-operators-btn').should( - 'contain', - `View installed Operators in Namespace ${nsName}`, - ); - - // Verify namespace was created successfully - cy.deleteProject(nsName); - }); -}); 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/console-select.tsx b/frontend/public/components/utils/console-select.tsx index c41852407fa..18a2d865d2d 100644 --- a/frontend/public/components/utils/console-select.tsx +++ b/frontend/public/components/utils/console-select.tsx @@ -83,7 +83,7 @@ const ConsoleSelectItem: FC<{ }> = ({ itemKey, content, selected, isBookmarked }) => ( = ({ 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} />