From c3d83527d87951808f519385ecefe046ae2eca9d Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Fri, 15 May 2026 10:07:03 +0200 Subject: [PATCH 1/2] CONSOLE-5233: Playwright-test-migration-for-console/app --- .gitignore | 1 + AGENTS.md | 4 + frontend/.eslintignore | 1 + frontend/e2e/.eslintrc.json | 5 + frontend/e2e/global.setup.ts | 14 +- frontend/e2e/pages/details-page.ts | 67 ++++++ frontend/e2e/pages/list-page.ts | 161 ++++++++++++++ frontend/e2e/pages/login-page.ts | 44 ++++ frontend/e2e/pages/machine-config-page.ts | 30 +++ frontend/e2e/pages/masthead-page.ts | 13 ++ frontend/e2e/pages/modal-page.ts | 38 ++++ frontend/e2e/pages/nav-page.ts | 78 +++++++ frontend/e2e/pages/yaml-editor-page.ts | 21 ++ ...sion-webhook-warning-notifications.spec.ts | 204 ++++++++++++++++++ .../console/app/auth-multiuser-login.spec.ts | 104 +++++++++ .../e2e/tests/console/app/debug-pod.spec.ts | 190 ++++++++++++++++ .../e2e/tests/console/app/deployments.spec.ts | 67 ++++++ .../tests/console/app/machine-config.spec.ts | 53 +++++ .../app/start-job-from-cronjob.spec.ts | 104 +++++++++ 19 files changed, 1194 insertions(+), 5 deletions(-) create mode 100644 frontend/e2e/.eslintrc.json create mode 100644 frontend/e2e/pages/details-page.ts create mode 100644 frontend/e2e/pages/list-page.ts create mode 100644 frontend/e2e/pages/login-page.ts create mode 100644 frontend/e2e/pages/machine-config-page.ts create mode 100644 frontend/e2e/pages/masthead-page.ts create mode 100644 frontend/e2e/pages/modal-page.ts create mode 100644 frontend/e2e/pages/nav-page.ts create mode 100644 frontend/e2e/pages/yaml-editor-page.ts create mode 100644 frontend/e2e/tests/console/app/admission-webhook-warning-notifications.spec.ts create mode 100644 frontend/e2e/tests/console/app/auth-multiuser-login.spec.ts create mode 100644 frontend/e2e/tests/console/app/debug-pod.spec.ts create mode 100644 frontend/e2e/tests/console/app/deployments.spec.ts create mode 100644 frontend/e2e/tests/console/app/machine-config.spec.ts create mode 100644 frontend/e2e/tests/console/app/start-job-from-cronjob.spec.ts diff --git a/.gitignore b/.gitignore index 7b9a9530880..1ae9898cacc 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ cypress-a11y-report.json /dynamic-demo-plugin/**/dist **/.claude/settings.local.json **/chartstore-*/ +.playwright-mcp/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index c674cdc0fd8..2d5c5b9701c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,6 +124,10 @@ These files are the single source of truth for architecture, coding standards, a - [CONTRIBUTING.md](CONTRIBUTING.md) - contribution workflow and commit message conventions. - [README.md](README.md) - project setup, build instructions, and architecture overview. +## Playwright migration + +We are migrating Cypress e2e tests to Playwright. Use `/migrate-cypress` to convert test files and `/debug-test` to fix failing tests. Shared migration context (translation tables, structural rules, checklist) is in `.claude/migration-context.md`. + ### Dynamic plugin SDK - [Dynamic Plugin SDK documentation](frontend/packages/console-dynamic-plugin-sdk/README.md) - architecture, design principles, and development guidelines. Consult before modifying SDK code. diff --git a/frontend/.eslintignore b/frontend/.eslintignore index 0e36cd165fd..2ed1813539a 100644 --- a/frontend/.eslintignore +++ b/frontend/.eslintignore @@ -12,4 +12,5 @@ Godeps dynamic-demo-plugin .eslintrc.js tsconfig.json +e2e/.eslintrc.json e2e/tsconfig.json diff --git a/frontend/e2e/.eslintrc.json b/frontend/e2e/.eslintrc.json new file mode 100644 index 00000000000..b956cbdd843 --- /dev/null +++ b/frontend/e2e/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "testing-library/prefer-screen-queries": "off" + } +} diff --git a/frontend/e2e/global.setup.ts b/frontend/e2e/global.setup.ts index 7aea58d39a0..fbc98d3ec15 100644 --- a/frontend/e2e/global.setup.ts +++ b/frontend/e2e/global.setup.ts @@ -2,10 +2,12 @@ import * as fs from 'fs'; import * as path from 'path'; import type { FullConfig } from '@playwright/test'; -import { chromium } from '@playwright/test'; +import { chromium, selectors } from '@playwright/test'; import KubernetesClient from './clients/kubernetes-client'; +selectors.setTestIdAttribute('data-test'); + const STORAGE_STATE_DIR = path.resolve(__dirname, '.auth'); const CONFIG_FILE = path.resolve(__dirname, '.test-config.json'); @@ -40,17 +42,19 @@ async function performBrowserLogin(opts: LoginOptions): Promise { return; } - // Wait for login page + // Wait for login page (may be IDP selection, direct login form, or login button) + const providerButton = page.getByText(opts.provider, { exact: true }); await page .locator('[data-test-id="login"]') .or(page.locator('#inputUsername')) + .or(providerButton) .first() .waitFor({ state: 'visible', timeout: 30_000 }); - // Click provider button if visible - const providerButton = page.getByText(opts.provider, { exact: true }); - if ((await providerButton.count()) > 0) { + // Click provider button if visible (multi-IDP clusters show a selection page first) + if ((await providerButton.count()) > 0 && (await providerButton.isVisible())) { await providerButton.click(); + await page.locator('#inputUsername').waitFor({ state: 'visible', timeout: 30_000 }); } // Fill credentials and submit diff --git a/frontend/e2e/pages/details-page.ts b/frontend/e2e/pages/details-page.ts new file mode 100644 index 00000000000..32935d7ee26 --- /dev/null +++ b/frontend/e2e/pages/details-page.ts @@ -0,0 +1,67 @@ +import { expect } from '@playwright/test'; +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class DetailsPage extends BasePage { + private readonly pageHeading = this.page.locator('[data-test="page-heading"]'); + private readonly resourceTitle = this.page.locator('[data-test-id="resource-title"]'); + private readonly skeletonView = this.page.getByTestId('skeleton-detail-view'); + private readonly actionsMenuButton = this.page.locator('[data-test-id="actions-menu-button"]'); + readonly breadcrumbLink0 = this.page.locator('[data-test-id="breadcrumb-link-0"]'); + readonly statusPopoverButton = this.page.getByTestId('popover-status-button'); + readonly enableAutoscaleButton = this.page.getByTestId('enable-autoscale'); + readonly xtermViewport = this.page.locator('.xterm-viewport'); + readonly resourcesSuccessMessage = this.page.getByTestId('resources-successfully-created'); + readonly eventTotals = this.page.getByTestId('event-totals'); + admissionWarning(testId: string): Locator { + return this.page.getByTestId(testId); + } + + debugContainerLink(containerName?: string): Locator { + const testId = containerName + ? `popup-debug-container-link-${containerName}` + : 'debug-container-link'; + return this.page.getByTestId(testId); + } + + async titleShouldContain(title: string): Promise { + await this.pageHeading.waitFor({ state: 'visible', timeout: 30_000 }); + await expect(this.pageHeading).toContainText(title, { timeout: 30_000 }); + } + + async sectionHeaderShouldExist(sectionHeading: string): Promise { + await expect( + this.page.locator(`[data-test-section-heading="${sectionHeading}"]`), + ).toBeVisible(); + } + + async isLoaded(): Promise { + await expect(this.skeletonView).toBeHidden({ timeout: 30_000 }); + await this.resourceTitle.waitFor({ state: 'visible', timeout: 30_000 }); + await expect(this.resourceTitle).not.toBeEmpty(); + } + + async selectTab(name: string): Promise { + const tab = this.page.locator(`[data-test-id="horizontal-link-${name}"]`); + await this.robustClick(tab); + } + + async clickPageActionFromDropdown(actionID: string): Promise { + await this.robustClick(this.actionsMenuButton); + const action = this.page.locator(`[data-test-action="${actionID}"]:not([disabled])`); + await this.robustClick(action); + } + + async clickBreadcrumb(): Promise { + await this.robustClick(this.breadcrumbLink0); + } + + async clickStatusPopover(): Promise { + await this.robustClick(this.statusPopoverButton, { timeout: 60_000 }); + } + + async clickDebugContainerLink(containerName?: string): Promise { + await this.robustClick(this.debugContainerLink(containerName)); + } +} diff --git a/frontend/e2e/pages/list-page.ts b/frontend/e2e/pages/list-page.ts new file mode 100644 index 00000000000..5042d1c27ad --- /dev/null +++ b/frontend/e2e/pages/list-page.ts @@ -0,0 +1,161 @@ +import { expect } from '@playwright/test'; +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class ListPage extends BasePage { + private readonly heading = this.page.locator('[data-test="page-heading"] h1'); + + async titleShouldHaveText(title: string): Promise { + await expect(this.heading).toContainText(title); + } + + // --- Resource row helpers (older VirtualizedTable) --- + + async rowsShouldExist(resourceName: string): Promise { + await expect( + this.page.locator('[data-test-rows="resource-row"]').filter({ hasText: resourceName }), + ).toBeVisible({ timeout: 60_000 }); + } + + async rowsShouldNotExist(resourceName: string): Promise { + await expect(this.page.locator(`[data-test-id="${resourceName}"]`)).toBeHidden({ + timeout: 90_000, + }); + } + + async rowsClickKebabAction(resourceName: string, actionName: string): Promise { + const row = this.page + .locator('[data-test-rows="resource-row"]') + .filter({ hasText: resourceName }); + const kebab = row.locator('[data-test-id="kebab-button"]'); + await this.robustClick(kebab); + const action = this.page.locator(`[data-test-action="${actionName}"]:not([disabled])`); + await this.robustClick(action); + } + + async rowsClickStatusButton(resourceName: string): Promise { + const row = this.page + .locator('[data-test-rows="resource-row"]') + .filter({ hasText: resourceName }); + const statusButton = row.getByTestId('popover-status-button'); + await this.robustClick(statusButton, { timeout: 60_000 }); + } + + async filterByStatus(status: string): Promise { + const filterToggle = this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]'); + if (await filterToggle.isVisible().catch(() => false)) { + await this.robustClick(filterToggle); + const filterItem = this.page.locator( + `[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${status}"]`, + ); + await this.robustClick(filterItem); + await this.robustClick(filterToggle); + } else { + const filterDropdownToggle = this.page.locator( + '[data-test-id="filter-dropdown-toggle"] button', + ); + if (await filterDropdownToggle.isVisible().catch(() => false)) { + await this.robustClick(filterDropdownToggle); + await this.page.locator(`#${status}`).click(); + await this.robustClick(filterDropdownToggle); + } + } + } + + // --- DataView row helpers (ConsoleDataView) --- + // These use generic table locators that work even if data-test attributes + // are not forwarded to the DOM by PatternFly DataView components. + + private dvCell(resourceName: string, cellName = 'name'): Locator { + return this.page.locator(`[data-test="data-view-cell-${resourceName}-${cellName}"]`); + } + + private dvRow(resourceName: string): Locator { + return this.page.locator('table tbody tr').filter({ + has: this.page.getByRole('link', { name: resourceName, exact: true }), + }); + } + + async dvRowsShouldBeLoaded(): Promise { + await expect(this.page.getByTestId('data-view-table')).toBeVisible({ timeout: 60_000 }); + } + + private async resolveRow(resourceName: string): Promise { + const cell = this.dvCell(resourceName); + if (await cell.isVisible({ timeout: 5_000 }).catch(() => false)) { + return cell.locator('xpath=ancestor::tr'); + } + return this.dvRow(resourceName); + } + + async dvRowsShouldExist(resourceName: string, cellName = 'name'): Promise { + const cell = this.dvCell(resourceName, cellName); + const row = this.dvRow(resourceName); + try { + await expect(cell).toBeVisible({ timeout: 30_000 }); + } catch { + await this.page.reload({ waitUntil: 'domcontentloaded' }); + try { + await expect(cell).toBeVisible({ timeout: 30_000 }); + } catch { + await expect(row).toBeVisible({ timeout: 30_000 }); + } + } + } + + async dvRowsShouldNotExist(resourceName: string): Promise { + const cell = this.dvCell(resourceName); + await expect(cell).toBeHidden({ timeout: 90_000 }); + } + + async dvRowsCountShouldBe(count: number): Promise { + await expect(this.page.locator('table tbody tr')).toHaveCount(count, { timeout: 60_000 }); + } + + async dvRowsClickKebabAction(resourceName: string, actionName: string): Promise { + const row = await this.resolveRow(resourceName); + const kebab = row.locator('[data-test-id="kebab-button"]'); + await this.robustClick(kebab); + const action = this.page.locator(`[data-test-action="${actionName}"]:not([disabled])`); + await this.robustClick(action); + } + + async dvRowsClickStatusButton(resourceName: string): Promise { + const row = await this.resolveRow(resourceName); + const statusButton = row.getByTestId('popover-status-button'); + await this.robustClick(statusButton, { timeout: 60_000 }); + } + + async dvFilterByName(name: string): Promise { + const filters = this.page.locator('[data-ouia-component-id="DataViewFilters"]'); + await this.robustClick(filters.locator('.pf-v6-c-menu-toggle').first()); + await this.robustClick( + this.page.locator('.pf-v6-c-menu__list-item').filter({ hasText: 'Name' }), + ); + const input = this.page.locator('[aria-label="Filter by name"]'); + await input.clear(); + await input.fill(name); + } + + async dvFilterBy(filterName: string, checkboxLabel: string): Promise { + await this.dvRowsShouldBeLoaded(); + const filters = this.page.locator('[data-ouia-component-id="DataViewFilters"]'); + await this.robustClick(filters.locator('.pf-v6-c-menu-toggle').first()); + await this.robustClick( + this.page.locator('.pf-v6-c-menu__list-item').filter({ hasText: filterName }), + ); + await this.robustClick(this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]')); + const filterItem = this.page.locator( + `[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${checkboxLabel}"]`, + ); + await expect(filterItem).toBeVisible(); + await this.robustClick(filterItem); + await expect(this.page).toHaveURL(new RegExp(`=${checkboxLabel}`), { timeout: 10_000 }); + await this.robustClick(this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]')); + } + + get clickCreateYAMLButton(): Locator { + return this.page.getByTestId('item-create'); + } +} diff --git a/frontend/e2e/pages/login-page.ts b/frontend/e2e/pages/login-page.ts new file mode 100644 index 00000000000..620eec50f83 --- /dev/null +++ b/frontend/e2e/pages/login-page.ts @@ -0,0 +1,44 @@ +import BasePage from './base-page'; + +export class LoginPage extends BasePage { + private readonly loginButton = this.page.locator('[data-test-id="login"]'); + private readonly usernameInput = this.page.locator('#inputUsername'); + private readonly passwordInput = this.page.locator('#inputPassword'); + private readonly submitButton = this.page.locator('button[type="submit"]'); + private readonly userDropdownToggle = this.page.getByTestId('user-dropdown-toggle'); + + providerButton(provider: string) { + return this.page.getByText(provider, { exact: true }); + } + + async loginAs(provider: string, username: string, password: string): Promise { + const baseURL = process.env.WEB_CONSOLE_URL || 'http://localhost:9000'; + await this.page.goto(baseURL, { timeout: 90_000, waitUntil: 'domcontentloaded' }); + + const authDisabled = await this.page + .evaluate(() => (window as any).SERVER_FLAGS?.authDisabled) + .catch(() => false); + + if (authDisabled) { + return false; + } + + const providerBtn = this.providerButton(provider); + await this.loginButton + .or(this.usernameInput) + .or(providerBtn) + .first() + .waitFor({ state: 'visible', timeout: 30_000 }); + + if ((await providerBtn.count()) > 0 && (await providerBtn.isVisible())) { + await providerBtn.click(); + await this.usernameInput.waitFor({ state: 'visible', timeout: 30_000 }); + } + + await this.usernameInput.fill(username); + await this.passwordInput.fill(password); + await this.submitButton.click(); + await this.userDropdownToggle.waitFor({ state: 'visible', timeout: 60_000 }); + return true; + } +} diff --git a/frontend/e2e/pages/machine-config-page.ts b/frontend/e2e/pages/machine-config-page.ts new file mode 100644 index 00000000000..30e7c05b1c1 --- /dev/null +++ b/frontend/e2e/pages/machine-config-page.ts @@ -0,0 +1,30 @@ +import { expect } from '@playwright/test'; +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class MachineConfigPage extends BasePage { + readonly configFilePath = this.page.getByTestId('config-file-path-0'); + readonly copyToClipboard = this.page.locator('.co-copy-to-clipboard__text'); + + sectionHeading(heading: string): Locator { + return this.page.locator(`[data-test-section-heading="${heading}"]`); + } + + errorHeading(text: string): Locator { + return this.page.getByText(text); + } + + async checkConfigFileDetails(mode: number, overwrite: boolean, content: string): Promise { + await this.configFilePath.scrollIntoViewIfNeeded(); + await this.page.locator('button[aria-label="Info"]').first().click(); + const descriptionList = this.page.locator('[class*="description-list"]'); + await expect(descriptionList.getByText(String(mode), { exact: true })).toBeVisible(); + await expect(descriptionList.getByText(String(overwrite), { exact: true })).toBeVisible(); + const decoded = decodeURIComponent(content) + .replace(/^(data:,)/, '') + .slice(0, 30); + const codeBlock = this.page.locator('code').first(); + await expect(codeBlock).toContainText(decoded); + } +} diff --git a/frontend/e2e/pages/masthead-page.ts b/frontend/e2e/pages/masthead-page.ts new file mode 100644 index 00000000000..192e1fa27d0 --- /dev/null +++ b/frontend/e2e/pages/masthead-page.ts @@ -0,0 +1,13 @@ +import { expect } from '@playwright/test'; + +import BasePage from './base-page'; + +export class MastheadPage extends BasePage { + readonly loadingIndicator = this.page.getByTestId('loading-indicator'); + readonly globalNotifications = this.page.getByTestId('global-notifications'); + + async usernameShouldHaveText(text: string): Promise { + const toggle = this.page.getByTestId('user-dropdown-toggle'); + await expect(toggle).toHaveText(text); + } +} diff --git a/frontend/e2e/pages/modal-page.ts b/frontend/e2e/pages/modal-page.ts new file mode 100644 index 00000000000..f67970b4329 --- /dev/null +++ b/frontend/e2e/pages/modal-page.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; + +import BasePage from './base-page'; + +export class ModalPage extends BasePage { + private get cancelButton() { + return this.page.locator('[data-test-id="modal-cancel-action"]'); + } + + private get submitButton() { + return this.page.locator('button[type=submit]'); + } + + async shouldBeOpened(): Promise { + await this.cancelButton.scrollIntoViewIfNeeded(); + await expect(this.cancelButton).toBeVisible({ timeout: 20_000 }); + } + + async shouldBeClosed(): Promise { + await expect(this.cancelButton).toBeHidden(); + } + + async submit(): Promise { + await this.submitButton.click(); + } + + async cancel(): Promise { + await this.cancelButton.click(); + } + + async submitShouldBeDisabled(): Promise { + await expect(this.submitButton).toBeDisabled(); + } + + async submitShouldBeEnabled(): Promise { + await expect(this.submitButton).toBeEnabled(); + } +} diff --git a/frontend/e2e/pages/nav-page.ts b/frontend/e2e/pages/nav-page.ts new file mode 100644 index 00000000000..baaa97061c5 --- /dev/null +++ b/frontend/e2e/pages/nav-page.ts @@ -0,0 +1,78 @@ +import { expect } from '@playwright/test'; + +import BasePage from './base-page'; + +export class NavPage extends BasePage { + readonly clusterSettingsHeading = this.page.locator( + '[data-test-id="cluster-settings-page-heading"]', + ); + + private get sidebar() { + return this.page.locator('#page-sidebar'); + } + + private get perspectiveSwitcherToggle() { + return this.page.locator('[data-test-id="perspective-switcher-toggle"]'); + } + + async perspectiveSwitcherShouldHaveText(text: string): Promise { + const toggle = this.perspectiveSwitcherToggle; + await toggle.scrollIntoViewIfNeeded(); + + const isSinglePerspective = (await toggle.getAttribute('id')) === 'core-platform-perspective'; + if (isSinglePerspective) { + await expect(toggle).toContainText(text, { timeout: 30_000 }); + } else { + await expect(toggle.locator('.pf-v6-c-menu-toggle__text')).toContainText(text, { + timeout: 30_000, + }); + } + } + + async changePerspectiveTo(perspective: string): Promise { + await this.page.waitForLoadState('domcontentloaded'); + const toggle = this.perspectiveSwitcherToggle; + await toggle.scrollIntoViewIfNeeded(); + await toggle.waitFor({ state: 'visible' }); + + const isSinglePerspective = (await toggle.getAttribute('id')) === 'core-platform-perspective'; + if (isSinglePerspective) { + return; + } + + const currentText = await toggle.locator('.pf-v6-c-menu-toggle__text').textContent(); + + if (currentText?.trim() === perspective) { + return; + } + + await this.robustClick(toggle); + await expect(toggle).toHaveAttribute('aria-expanded', 'true', { timeout: 5_000 }); + const option = this.page + .locator('[data-test-id="perspective-switcher-menu-option"]') + .filter({ hasText: perspective }); + await this.robustClick(option); + } + + async shouldHaveNavSection(path: string[]): Promise { + for (const item of path) { + await expect(this.sidebar).toContainText(item); + } + } + + async shouldNotHaveNavSection(path: string[]): Promise { + const target = path[path.length - 1]; + await expect(this.sidebar.getByText(target, { exact: true })).toBeHidden(); + } + + async clickNavLink(path: string[]): Promise { + const navItem = this.sidebar.getByText(path[0]); + const expanded = await navItem.getAttribute('aria-expanded'); + if (expanded !== 'true') { + await this.robustClick(navItem); + } + if (path.length === 2) { + await this.robustClick(this.sidebar.getByText(path[1])); + } + } +} diff --git a/frontend/e2e/pages/yaml-editor-page.ts b/frontend/e2e/pages/yaml-editor-page.ts new file mode 100644 index 00000000000..166fd1ae68a --- /dev/null +++ b/frontend/e2e/pages/yaml-editor-page.ts @@ -0,0 +1,21 @@ +import BasePage from './base-page'; + +export class YamlEditorPage extends BasePage { + async isImportLoaded(): Promise { + await this.page.locator('.monaco-editor textarea').first().waitFor({ + state: 'visible', + timeout: 30_000, + }); + } + + async setEditorContent(text: string): Promise { + await this.page.evaluate((content) => { + const models = (window as any).monaco.editor.getModels(); + models[0].setValue(content); + }, text); + } + + async clickSaveCreateButton(): Promise { + await this.page.getByTestId('save-changes').click(); + } +} diff --git a/frontend/e2e/tests/console/app/admission-webhook-warning-notifications.spec.ts b/frontend/e2e/tests/console/app/admission-webhook-warning-notifications.spec.ts new file mode 100644 index 00000000000..188238e732b --- /dev/null +++ b/frontend/e2e/tests/console/app/admission-webhook-warning-notifications.spec.ts @@ -0,0 +1,204 @@ +import { test, expect } from '../../../fixtures'; +import { DetailsPage } from '../../../pages/details-page'; +import { YamlEditorPage } from '../../../pages/yaml-editor-page'; + +const POD_NAME = 'pod1'; +const DEPLOY_NAME = 'deploy1'; +const CONTAINER_NAME = 'container1'; +const WARNING_FOO = '299 - "[pod-must-have-label-foo] you must provide labels: {"foo"}"'; +const WARNING_BAR = '299 - "[deployment-must-have-label-bar] you must provide labels: {"bar"}"'; +const LEARN_MORE_ID = 'admission-webhook-warning-learn-more'; +const WARNING_ID = 'admission-webhook-warning'; + +test.describe('Admission Webhook warning notification', () => { + const testNs = `e2e-admission-${Date.now()}`; + + const pod1ReqObj = `apiVersion: v1 +kind: Pod +metadata: + name: ${POD_NAME}-a + labels: + app: httpd + namespace: ${testNs} +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: ${CONTAINER_NAME} + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest' + ports: + - containerPort: 8080 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL`; + + const bulkResourcesReqObj = `apiVersion: v1 +kind: Pod +metadata: + name: ${POD_NAME}-b + labels: + app: httpd + namespace: ${testNs} +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: ${CONTAINER_NAME} + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest' + ports: + - containerPort: 8080 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${DEPLOY_NAME} + annotations: {} + namespace: ${testNs} +spec: + selector: + matchLabels: + app: deploy1 + replicas: 3 + template: + metadata: + labels: + app: deploy1 + spec: + containers: + - name: ${CONTAINER_NAME} + image: >- + image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest + ports: + - containerPort: 8080 + protocol: TCP + env: + - name: app + value: frontennd + imagePullSecrets: [] + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + paused: false +`; + + test.beforeAll(async ({ k8sClient }) => { + await k8sClient.createNamespace(testNs); + await k8sClient.waitForNamespaceReady(testNs); + }); + + test.afterAll(async ({ k8sClient }) => { + await k8sClient.deleteNamespace(testNs); + }); + + test('Create a pod and display Admission Webhook warning notification', async ({ page }) => { + const yamlEditor = new YamlEditorPage(page); + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/import`); + await yamlEditor.isImportLoaded(); + await yamlEditor.setEditorContent(pod1ReqObj); + + await page.route(`**/api/kubernetes/api/v1/namespaces/${testNs}/pods`, async (route) => { + if (route.request().method() !== 'POST') { + await route.continue(); + return; + } + const response = await route.fetch(); + await route.fulfill({ + response, + headers: { + ...response.headers(), + Warning: WARNING_FOO, + }, + }); + }); + + await yamlEditor.clickSaveCreateButton(); + await detailsPage.sectionHeaderShouldExist('Pod details'); + + const warning = detailsPage.admissionWarning(WARNING_ID); + await expect(warning).toContainText('Admission Webhook Warning'); + await expect(warning).toContainText(`Pod ${POD_NAME}-a violates policy ${WARNING_FOO}`); + + const learnMore = detailsPage.admissionWarning(LEARN_MORE_ID); + await expect(learnMore).toContainText('Learn more'); + await learnMore.click(); + }); + + test('Create bulk resources and display Admission Webhook warning notifications', async ({ + page, + }) => { + const yamlEditor = new YamlEditorPage(page); + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/import`); + await yamlEditor.isImportLoaded(); + await yamlEditor.setEditorContent(bulkResourcesReqObj); + + await page.route(`**/api/kubernetes/api/v1/namespaces/${testNs}/pods`, async (route) => { + if (route.request().method() !== 'POST') { + await route.continue(); + return; + } + const response = await route.fetch(); + await route.fulfill({ + response, + headers: { + ...response.headers(), + Warning: WARNING_FOO, + }, + }); + }); + + await page.route( + `**/api/kubernetes/apis/apps/v1/namespaces/${testNs}/deployments`, + async (route) => { + if (route.request().method() !== 'POST') { + await route.continue(); + return; + } + const response = await route.fetch(); + await route.fulfill({ + response, + headers: { + ...response.headers(), + Warning: WARNING_BAR, + }, + }); + }, + ); + + await yamlEditor.clickSaveCreateButton(); + + await expect(detailsPage.resourcesSuccessMessage).toContainText( + 'Resources successfully created', + ); + + const warning = detailsPage.admissionWarning(WARNING_ID); + await expect(warning).toHaveCount(2); + await expect(warning.first()).toContainText('Admission Webhook Warning'); + await expect( + warning.filter({ hasText: `Pod ${POD_NAME}-b violates policy ${WARNING_FOO}` }), + ).toBeVisible(); + await expect( + warning.filter({ hasText: `Deployment ${DEPLOY_NAME} violates policy ${WARNING_BAR}` }), + ).toBeVisible(); + + const learnMore = detailsPage.admissionWarning(LEARN_MORE_ID); + await expect(learnMore.first()).toContainText('Learn more'); + await learnMore.first().click(); + }); +}); diff --git a/frontend/e2e/tests/console/app/auth-multiuser-login.spec.ts b/frontend/e2e/tests/console/app/auth-multiuser-login.spec.ts new file mode 100644 index 00000000000..86def17f0d9 --- /dev/null +++ b/frontend/e2e/tests/console/app/auth-multiuser-login.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from '../../../fixtures'; +import { LoginPage } from '../../../pages/login-page'; +import { MastheadPage } from '../../../pages/masthead-page'; +import { NavPage } from '../../../pages/nav-page'; + +const KUBEADMIN_IDP = 'kube:admin'; +const KUBEADMIN_USERNAME = 'kubeadmin'; + +test.describe('Auth test', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test("logs in as 'test' user via htpasswd identity provider", async ({ page }) => { + const kubeadminPassword = process.env.BRIDGE_KUBEADMIN_PASSWORD; + const htpasswdPassword = process.env.BRIDGE_HTPASSWD_PASSWORD; + + if (!kubeadminPassword || !htpasswdPassword) { + test.skip(); + return; + } + + const idp = process.env.BRIDGE_HTPASSWD_IDP || 'test'; + const username = process.env.BRIDGE_HTPASSWD_USERNAME || 'test'; + const passwd = htpasswdPassword || 'test'; + + const loginPage = new LoginPage(page); + const masthead = new MastheadPage(page); + const nav = new NavPage(page); + + await test.step('Login as test user', async () => { + const loggedIn = await loginPage.loginAs(idp, username, passwd); + if (!loggedIn) { + test.skip(true, 'Auth is disabled - skipping auth test'); + } + }); + + await test.step('Verify user logged in', async () => { + await masthead.usernameShouldHaveText(username); + }); + + await test.step('Switch to Core platform perspective', async () => { + await nav.changePerspectiveTo('Core platform'); + await nav.perspectiveSwitcherShouldHaveText('Core platform'); + }); + + await test.step('Verify test user has restricted access', async () => { + await nav.shouldNotHaveNavSection(['Administration', 'Cluster Status']); + await nav.shouldNotHaveNavSection(['Administration', 'Cluster Settings']); + await nav.shouldNotHaveNavSection(['Administration', 'Namespaces']); + await nav.shouldNotHaveNavSection(['Administration', 'Custom Resource Definitions']); + await nav.shouldNotHaveNavSection(['Ecosystem', 'Software Catalog']); + await nav.shouldNotHaveNavSection(['Storage', 'Persistent Volumes']); + await nav.shouldNotHaveNavSection(['Compute']); + await nav.shouldNotHaveNavSection(['Monitoring']); + }); + }); + + test("log in as 'kubeadmin' user", async ({ page }) => { + const kubeadminPassword = process.env.BRIDGE_KUBEADMIN_PASSWORD; + if (!kubeadminPassword) { + test.skip(); + return; + } + + const loginPage = new LoginPage(page); + const masthead = new MastheadPage(page); + const nav = new NavPage(page); + + await test.step('Login as kubeadmin', async () => { + const loggedIn = await loginPage.loginAs( + KUBEADMIN_IDP, + KUBEADMIN_USERNAME, + kubeadminPassword, + ); + if (!loggedIn) { + test.skip(true, 'Auth is disabled - skipping auth test'); + } + }); + + await test.step('Verify kubeadmin logged in', async () => { + await expect(masthead.loadingIndicator).toBeHidden(); + await masthead.usernameShouldHaveText(KUBEADMIN_IDP); + await expect(masthead.globalNotifications).toContainText( + 'You are logged in as a temporary administrative user.', + ); + }); + + await test.step('Verify Core platform perspective', async () => { + await nav.perspectiveSwitcherShouldHaveText('Core platform'); + + const baseURL = process.env.WEB_CONSOLE_URL || 'http://localhost:9000'; + if (!baseURL.includes('localhost')) { + await nav.changePerspectiveTo('Core platform'); + await nav.perspectiveSwitcherShouldHaveText('Core platform'); + } + }); + + await test.step('Verify kubeadmin has admin access', async () => { + await nav.shouldHaveNavSection(['Compute']); + await nav.shouldHaveNavSection(['Operators']); + await nav.clickNavLink(['Administration', 'Cluster Settings']); + await expect(nav.clusterSettingsHeading).toBeVisible(); + }); + }); +}); diff --git a/frontend/e2e/tests/console/app/debug-pod.spec.ts b/frontend/e2e/tests/console/app/debug-pod.spec.ts new file mode 100644 index 00000000000..e0aa57faedf --- /dev/null +++ b/frontend/e2e/tests/console/app/debug-pod.spec.ts @@ -0,0 +1,190 @@ +import { test, expect } from '../../../fixtures'; +import { DetailsPage } from '../../../pages/details-page'; +import { ListPage } from '../../../pages/list-page'; +import { YamlEditorPage } from '../../../pages/yaml-editor-page'; + +const POD_NAME = 'pod1'; +const CONTAINER_NAME = 'container1'; +const podToDebug = `apiVersion: v1 +kind: Pod +metadata: + name: ${POD_NAME} +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: ${CONTAINER_NAME} + image: quay.io/fedora/fedora + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + restartPolicy: Always`; + +async function pollForPodCrashState( + k8sClient: any, + namespace: string, + podName: string, + timeoutMs: number, +): Promise<{ ready: boolean; reason: string }> { + const deadline = Date.now() + timeoutMs; + let lastReason = 'pod not found'; + + while (Date.now() < deadline) { + try { + const pods = await k8sClient.getPods(namespace); + const pod = pods.find((p: any) => p.metadata?.name === podName); + if (!pod) { + lastReason = 'pod not found'; + } else if (!pod.status?.containerStatuses?.length) { + lastReason = `phase=${pod.status?.phase || 'unknown'}, no containerStatuses yet`; + } else { + const container = pod.status.containerStatuses[0]; + const waitingReason = container?.state?.waiting?.reason; + const restartCount = container?.restartCount ?? 0; + + if (waitingReason === 'CrashLoopBackOff' || restartCount >= 1) { + return { ready: true, reason: waitingReason || `restartCount=${restartCount}` }; + } + + if (waitingReason === 'ImagePullBackOff' || waitingReason === 'ErrImagePull') { + lastReason = `image pull failed: ${waitingReason}`; + } else if (waitingReason) { + lastReason = `waiting: ${waitingReason}`; + } else if (container?.state?.running) { + lastReason = 'container running (not crashing yet)'; + } else if (container?.state?.terminated) { + lastReason = `terminated: reason=${container.state.terminated.reason}, exitCode=${container.state.terminated.exitCode}`; + } else { + lastReason = `unknown state: ${JSON.stringify(container?.state)}`; + } + } + } catch (err) { + lastReason = `error: ${err instanceof Error ? err.message : String(err)}`; + } + await new Promise((r) => setTimeout(r, 3_000)); + } + return { ready: false, reason: lastReason }; +} + +test.describe.serial('Debug pod', () => { + const testNs = `e2e-debug-pod-${Date.now()}`; + + test.beforeAll(async ({ k8sClient }) => { + // Create namespace WITHOUT openshift.io/run-level label so that SCC admission + // injects the correct runAsUser for pods with runAsNonRoot: true + await k8sClient.coreV1Api.createNamespace({ + body: { metadata: { name: testNs } }, + }); + await k8sClient.waitForNamespaceReady(testNs); + }); + + test.afterAll(async ({ k8sClient }) => { + await k8sClient.deleteNamespace(testNs); + }); + + test('Create pod that has crashbackloop error', async ({ page, k8sClient }) => { + test.setTimeout(300_000); + const yamlEditor = new YamlEditorPage(page); + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/import`); + await yamlEditor.isImportLoaded(); + await yamlEditor.setEditorContent(podToDebug); + await yamlEditor.clickSaveCreateButton(); + await expect(page.getByTestId('yaml-error')).toBeHidden(); + await detailsPage.sectionHeaderShouldExist('Pod details'); + + // Wait for pod to enter CrashLoopBackOff so debug links appear in subsequent tests + const podState = await pollForPodCrashState(k8sClient, testNs, POD_NAME, 120_000); + expect(podState.ready, `Pod never crashed. Last state: ${podState.reason}`).toBe(true); + }); + + test('Opens debug terminal page from Logs subsection', async ({ page }) => { + test.setTimeout(300_000); + const listPage = new ListPage(page); + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/pods`); + await listPage.dvRowsShouldExist(POD_NAME); + await page.goto(`/k8s/ns/${testNs}/pods/${POD_NAME}`); + await detailsPage.isLoaded(); + await detailsPage.selectTab('Logs'); + await detailsPage.isLoaded(); + await detailsPage.debugContainerLink().waitFor({ state: 'visible', timeout: 30_000 }); + await detailsPage.clickDebugContainerLink(); + await listPage.titleShouldHaveText(`Debug ${CONTAINER_NAME}`); + await expect(detailsPage.xtermViewport).toBeAttached({ timeout: 30_000 }); + await detailsPage.clickBreadcrumb(); + await listPage.dvRowsShouldExist(POD_NAME); + }); + + test('Opens debug terminal page from Pod Details - Status tool tip', async ({ page }) => { + test.setTimeout(300_000); + const listPage = new ListPage(page); + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/pods/${POD_NAME}`); + await detailsPage.isLoaded(); + await detailsPage.clickStatusPopover(); + // Regression test for OCPBUGS-83813: Wait for popover content to be stable before clicking + // https://issues.redhat.com/browse/OCPBUGS-83813 + const debugLink = detailsPage.debugContainerLink(CONTAINER_NAME); + await expect(debugLink).toBeVisible({ timeout: 30_000 }); + await detailsPage.clickDebugContainerLink(CONTAINER_NAME); + await listPage.titleShouldHaveText(`Debug ${CONTAINER_NAME}`); + await expect(detailsPage.xtermViewport).toBeAttached({ timeout: 30_000 }); + await detailsPage.clickBreadcrumb(); + await listPage.dvRowsShouldExist(POD_NAME); + }); + + test('Opens debug terminal page from Pods Page - Status tool tip', async ({ + page, + k8sClient, + }) => { + test.setTimeout(300_000); + const listPage = new ListPage(page); + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/pods`); + await listPage.dvRowsShouldExist(POD_NAME); + await listPage.dvRowsClickStatusButton(POD_NAME); + // Regression test for OCPBUGS-83813: Wait for popover content to be stable before clicking + // https://issues.redhat.com/browse/OCPBUGS-83813 + const debugLink = detailsPage.debugContainerLink(CONTAINER_NAME); + await expect(debugLink).toBeVisible({ timeout: 30_000 }); + await debugLink.click(); + await listPage.titleShouldHaveText(`Debug ${CONTAINER_NAME}`); + await expect(detailsPage.xtermViewport).toBeAttached({ timeout: 30_000 }); + + // Debug pod should not copy main pod network info + const pods = await k8sClient.getPods(testNs); + expect(pods.length).toBeGreaterThanOrEqual(2); + const ipAddressOne = pods[0]?.status?.podIP; + const ipAddressTwo = pods[1]?.status?.podIP; + expect(ipAddressOne).not.toEqual(ipAddressTwo); + + await detailsPage.clickBreadcrumb(); + await listPage.dvRowsShouldExist(POD_NAME); + }); + + test('Debug pod should be terminated after leaving debug container page', async ({ + page, + k8sClient, + }) => { + const listPage = new ListPage(page); + + await page.goto(`/k8s/ns/${testNs}/pods`); + await listPage.dvRowsShouldExist(POD_NAME); + await listPage.filterByStatus('Running'); + + const pods = await k8sClient.getPods(testNs); + const debugPod = pods.find((p) => p.metadata?.name !== POD_NAME); + if (debugPod?.metadata?.name) { + await listPage.dvRowsShouldNotExist(debugPod.metadata.name); + } + }); +}); diff --git a/frontend/e2e/tests/console/app/deployments.spec.ts b/frontend/e2e/tests/console/app/deployments.spec.ts new file mode 100644 index 00000000000..ca0a5719cfb --- /dev/null +++ b/frontend/e2e/tests/console/app/deployments.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '../../../fixtures'; +import { DetailsPage } from '../../../pages/details-page'; + +test.describe.serial('Deployment resource details page', () => { + const testNs = `e2e-deployments-${Date.now()}`; + const workloadName = `deployment-e2e`; + + test.beforeAll(async ({ k8sClient }) => { + await k8sClient.createNamespace(testNs); + await k8sClient.waitForNamespaceReady(testNs); + + await k8sClient.appsV1Api.createNamespacedDeployment({ + namespace: testNs, + body: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: workloadName, namespace: testNs }, + spec: { + replicas: 0, + selector: { matchLabels: { app: workloadName } }, + template: { + metadata: { labels: { app: workloadName } }, + spec: { + containers: [{ name: 'httpd', image: 'httpd' }], + }, + }, + }, + }, + }); + + await k8sClient.createCustomResource('autoscaling', 'v1', testNs, 'horizontalpodautoscalers', { + apiVersion: 'autoscaling/v1', + kind: 'HorizontalPodAutoscaler', + metadata: { name: workloadName, namespace: testNs }, + spec: { + scaleTargetRef: { + apiVersion: 'apps/v1', + kind: 'Deployment', + name: workloadName, + }, + minReplicas: 1, + maxReplicas: 10, + }, + }); + }); + + test.afterAll(async ({ k8sClient }) => { + await k8sClient.deleteNamespace(testNs); + }); + + test('Enable deployment autoscale button should exist', async ({ page }) => { + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/deployments/${workloadName}`); + await detailsPage.isLoaded(); + await expect(detailsPage.enableAutoscaleButton).toBeVisible(); + await detailsPage.enableAutoscaleButton.click(); + }); + + test('Enable deployment autoscale button should not exist', async ({ page }) => { + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/deployments/${workloadName}`); + await detailsPage.isLoaded(); + await expect(detailsPage.enableAutoscaleButton).toBeHidden({ timeout: 10_000 }); + }); +}); diff --git a/frontend/e2e/tests/console/app/machine-config.spec.ts b/frontend/e2e/tests/console/app/machine-config.spec.ts new file mode 100644 index 00000000000..cc9a83b6ef0 --- /dev/null +++ b/frontend/e2e/tests/console/app/machine-config.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '../../../fixtures'; +import { DetailsPage } from '../../../pages/details-page'; +import { MachineConfigPage } from '../../../pages/machine-config-page'; + +const MC_WITH_CONFIG_FILES = '00-master'; +const MC_WITHOUT_CONFIG_FILES = '99-master-ssh'; +const MC_DETAILS_PAGE_URL = '/k8s/cluster/machineconfiguration.openshift.io~v1~MachineConfig/'; +const MC_SECTION_HEADING = 'Configuration files'; + +test.describe('MachineConfig resource details page', () => { + test(`${MC_WITH_CONFIG_FILES} displays configuration files`, async ({ page, k8sClient }) => { + const detailsPage = new DetailsPage(page); + const mcPage = new MachineConfigPage(page); + + await page.goto(`${MC_DETAILS_PAGE_URL}${MC_WITH_CONFIG_FILES}`); + await detailsPage.isLoaded(); + await detailsPage.titleShouldContain(MC_WITH_CONFIG_FILES); + + await expect(mcPage.sectionHeading(MC_SECTION_HEADING)).toBeVisible(); + await expect(mcPage.configFilePath).toBeVisible(); + await expect(mcPage.copyToClipboard.first()).toBeVisible(); + + const mcResource: any = await k8sClient.customObjectsApi.getClusterCustomObject({ + group: 'machineconfiguration.openshift.io', + version: 'v1', + plural: 'machineconfigs', + name: MC_WITH_CONFIG_FILES, + }); + const fileEntry = mcResource?.spec?.config?.storage?.files?.[0]; + expect(fileEntry).toHaveProperty('contents'); + expect(fileEntry).toHaveProperty('mode'); + expect(fileEntry).toHaveProperty('overwrite'); + const { + contents: { source }, + mode, + overwrite, + } = fileEntry; + await mcPage.checkConfigFileDetails(mode, overwrite, source); + }); + + test(`${MC_WITHOUT_CONFIG_FILES} does not display configuration files`, async ({ page }) => { + const detailsPage = new DetailsPage(page); + const mcPage = new MachineConfigPage(page); + + await page.goto(`${MC_DETAILS_PAGE_URL}${MC_WITHOUT_CONFIG_FILES}`); + await detailsPage.isLoaded(); + await detailsPage.titleShouldContain(MC_WITHOUT_CONFIG_FILES); + + await expect(mcPage.sectionHeading(MC_SECTION_HEADING)).toBeHidden(); + await expect(mcPage.configFilePath).toBeHidden(); + await expect(mcPage.copyToClipboard).toHaveCount(0); + }); +}); diff --git a/frontend/e2e/tests/console/app/start-job-from-cronjob.spec.ts b/frontend/e2e/tests/console/app/start-job-from-cronjob.spec.ts new file mode 100644 index 00000000000..d33af6dd079 --- /dev/null +++ b/frontend/e2e/tests/console/app/start-job-from-cronjob.spec.ts @@ -0,0 +1,104 @@ +import { test, expect } from '../../../fixtures'; +import { DetailsPage } from '../../../pages/details-page'; +import { ListPage } from '../../../pages/list-page'; +import { YamlEditorPage } from '../../../pages/yaml-editor-page'; + +const CRONJOB_NAME = 'cronjob1'; + +test.describe.serial('Start a Job from a CronJob', () => { + const testNs = `e2e-cronjob-${Date.now()}`; + + const cronJobPayload = `apiVersion: batch/v1 +kind: CronJob +metadata: + name: ${CRONJOB_NAME} + namespace: ${testNs} +spec: + schedule: '@daily' + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox + args: + - /bin/sh + - '-c' + - date; echo Hello from the Openshift cluster + restartPolicy: OnFailure`; + + test.beforeAll(async ({ k8sClient }) => { + await k8sClient.createNamespace(testNs); + await k8sClient.waitForNamespaceReady(testNs); + }); + + test.afterAll(async ({ k8sClient }) => { + await k8sClient.deleteNamespace(testNs); + }); + + test('verify "Start Job" on the CronJob details page', async ({ page }) => { + const yamlEditor = new YamlEditorPage(page); + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/import`); + await yamlEditor.isImportLoaded(); + await yamlEditor.setEditorContent(cronJobPayload); + await yamlEditor.clickSaveCreateButton(); + await detailsPage.sectionHeaderShouldExist('CronJob details'); + + await detailsPage.clickPageActionFromDropdown('Start Job'); + await detailsPage.isLoaded(); + await detailsPage.sectionHeaderShouldExist('Job details'); + await detailsPage.titleShouldContain(CRONJOB_NAME); + }); + + test('verify "Start Job" on the CronJob list page', async ({ page }) => { + const listPage = new ListPage(page); + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/cronjobs`); + await listPage.dvRowsShouldExist(CRONJOB_NAME); + + // LazyActionMenu loads actions lazily and WebSocket updates can re-render the + // table (resetting menu state), so retry opening the kebab if the action disappears. + const row = page.locator('table tbody tr').filter({ + has: page.getByRole('link', { name: CRONJOB_NAME, exact: true }), + }); + const kebab = row.locator('[data-test-id="kebab-button"]'); + const action = page.locator('[data-test-action="Start Job"]:not([disabled])'); + + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + await kebab.hover(); + await kebab.click(); + try { + await action.waitFor({ state: 'visible', timeout: 5_000 }); + break; + } catch { + // Menu may have closed due to table re-render; retry + } + } + await action.click(); + + await detailsPage.isLoaded(); + await detailsPage.sectionHeaderShouldExist('Job details'); + await detailsPage.titleShouldContain(CRONJOB_NAME); + }); + + test('verify the number of Jobs in CronJob > Jobs tab list page', async ({ page }) => { + const listPage = new ListPage(page); + + await page.goto(`/k8s/ns/${testNs}/cronjobs`); + await listPage.dvRowsShouldExist(CRONJOB_NAME); + await page.goto(`/k8s/ns/${testNs}/cronjobs/${CRONJOB_NAME}/jobs`); + await listPage.dvRowsCountShouldBe(2); + }); + + test('verify the number of events in CronJob > Events tab list page', async ({ page }) => { + const detailsPage = new DetailsPage(page); + + await page.goto(`/k8s/ns/${testNs}/cronjobs/${CRONJOB_NAME}/events`); + await expect(detailsPage.eventTotals).toHaveText('Showing 2 events', { timeout: 10_000 }); + }); +}); From 0dd5073acc09d434d2314fba5e4675ac7824425f Mon Sep 17 00:00:00 2001 From: Robert Luby Date: Fri, 15 May 2026 10:16:47 +0200 Subject: [PATCH 2/2] CONSOLE-5233: remove migrated cypress tests --- ...ission-webhook-warning-notifications.cy.ts | 167 ------------------ .../tests/app/auth-multiuser-login.cy.ts | 89 ---------- .../tests/app/debug-pod.cy.ts | 113 ------------ .../tests/app/deployments.cy.ts | 55 ------ .../tests/app/machine-config.cy.ts | 68 ------- .../tests/app/start-job-from-cronjob.cy.ts | 76 -------- 6 files changed, 568 deletions(-) delete mode 100644 frontend/packages/integration-tests/tests/app/admission-webhook-warning-notifications.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/auth-multiuser-login.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/debug-pod.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/deployments.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/machine-config.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/start-job-from-cronjob.cy.ts diff --git a/frontend/packages/integration-tests/tests/app/admission-webhook-warning-notifications.cy.ts b/frontend/packages/integration-tests/tests/app/admission-webhook-warning-notifications.cy.ts deleted file mode 100644 index a418150e6dd..00000000000 --- a/frontend/packages/integration-tests/tests/app/admission-webhook-warning-notifications.cy.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import * as yamlEditor from '../../views/yaml-editor'; - -const POD_NAME = 'pod1'; -const DEPLOY_NAME = 'deploy1'; -const CONTAINER_NAME = 'container1'; -const WARNING_FOO = '299 - "[pod-must-have-label-foo] you must provide labels: {"foo"}"'; -const WARNING_BAR = '299 - "[deployment-must-have-label-bar] you must provide labels: {"bar"}"'; -const WAIT_OPTION = { timeout: 5000 }; -const POD_CREATED_ALIAS = 'podCreated'; -const BULK_RESOURCES_CREATED_ALIAS = 'bulkResourcesCreated'; -const LEARN_MORE_ID = 'admission-webhook-warning-learn-more'; -const WARNING_ID = 'admission-webhook-warning'; -const resources = [ - { kind: 'Pod', name: `${POD_NAME}-b`, warning: WARNING_FOO, resource: 'pods', path: 'api' }, - { - kind: 'Deployment', - name: DEPLOY_NAME, - warning: WARNING_BAR, - resource: 'deployments', - path: 'apis/apps', - }, -]; -const pod1ReqObj = `apiVersion: v1 -kind: Pod -metadata: - name: ${POD_NAME}-a - labels: - app: httpd - namespace: ${testName} -spec: - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - name: ${CONTAINER_NAME} - image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest' - ports: - - containerPort: 8080 - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL`; - -const bulkResourcesReqObj = `apiVersion: v1 -kind: Pod -metadata: - name: ${POD_NAME}-b - labels: - app: httpd - namespace: ${testName} -spec: - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - name: ${CONTAINER_NAME} - image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest' - ports: - - containerPort: 8080 - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ${DEPLOY_NAME} - annotations: {} - namespace: ${testName} -spec: - selector: - matchLabels: - app: deploy1 - replicas: 3 - template: - metadata: - labels: - app: deploy1 - spec: - containers: - - name: ${CONTAINER_NAME} - image: >- - image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest - ports: - - containerPort: 8080 - protocol: TCP - env: - - name: app - value: frontennd - imagePullSecrets: [] - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 25% - maxUnavailable: 25% - paused: false -`; - -describe('Admission Webhook warning notification', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.visit('/'); - cy.deleteProjectWithCLI(testName); - }); - - it('Create a pod and display Admission Webhook warning notification', () => { - cy.visit(`/k8s/ns/${testName}/import`); - yamlEditor.isImportLoaded(); - yamlEditor.setEditorContent(pod1ReqObj).then(() => { - cy.intercept('POST', `/api/kubernetes/api/v1/namespaces/${testName}/pods`, (req) => { - req.continue((res) => { - res.headers = { - Warning: WARNING_FOO, - }; - }); - }).as(POD_CREATED_ALIAS); - yamlEditor.clickSaveCreateButton(); - cy.wait(`@${POD_CREATED_ALIAS}`, WAIT_OPTION); - detailsPage.sectionHeaderShouldExist('Pod details'); - cy.byTestID(WARNING_ID).contains('Admission Webhook Warning'); - cy.byTestID(WARNING_ID).contains(`Pod ${POD_NAME}-a violates policy ${WARNING_FOO}`); - cy.byTestID(LEARN_MORE_ID).contains('Learn more').click(); - }); - }); - - it('Create bulk resources and display Admission Webhook warning notifications', () => { - cy.visit(`/k8s/ns/${testName}/import`); - yamlEditor.isImportLoaded(); - yamlEditor.setEditorContent(bulkResourcesReqObj).then(() => { - for (const resource of resources) { - cy.intercept( - 'POST', - `/api/kubernetes/${resource.path}/v1/namespaces/${testName}/${resource.resource}`, - (req) => { - req.continue((res) => { - res.headers = { - Warning: resource.warning, - }; - }); - }, - ).as(BULK_RESOURCES_CREATED_ALIAS); - } - yamlEditor.clickSaveCreateButton(); - cy.wait(`@${BULK_RESOURCES_CREATED_ALIAS}`, WAIT_OPTION); - cy.byTestID('resources-successfully-created').contains('Resources successfully created'); - cy.byTestID(WARNING_ID).contains('Admission Webhook Warning'); - cy.byTestID(WARNING_ID).contains(`Pod ${POD_NAME}-b violates policy ${WARNING_FOO}`); - cy.byTestID(WARNING_ID).contains(`Deployment ${DEPLOY_NAME} violates policy ${WARNING_BAR}`); - cy.byTestID(LEARN_MORE_ID).contains('Learn more').click(); - }); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/auth-multiuser-login.cy.ts b/frontend/packages/integration-tests/tests/app/auth-multiuser-login.cy.ts deleted file mode 100644 index c373f4d85d1..00000000000 --- a/frontend/packages/integration-tests/tests/app/auth-multiuser-login.cy.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { checkErrors } from '../../support'; -import { masthead } from '../../views/masthead'; -import { nav } from '../../views/nav'; - -describe('Auth test', () => { - const KUBEADMIN_IDP = 'kube:admin'; - const KUBEADMIN_USERNAME = 'kubeadmin'; - - beforeEach(() => { - // clear any existing sessions - Cypress.session.clearAllSavedSessions(); - }); - - afterEach(() => { - checkErrors(); - Cypress.session.clearAllSavedSessions(); - }); - - it(`logs in as 'test' user via htpasswd identity provider`, function () { - cy.env(['BRIDGE_KUBEADMIN_PASSWORD', 'BRIDGE_HTPASSWD_PASSWORD']).then( - ({ BRIDGE_KUBEADMIN_PASSWORD, BRIDGE_HTPASSWD_PASSWORD }) => { - if (!BRIDGE_KUBEADMIN_PASSWORD) { - this.skip(); - return; - } - const idp = Cypress.expose('BRIDGE_HTPASSWD_IDP') || 'test'; - const username = Cypress.expose('BRIDGE_HTPASSWD_USERNAME') || 'test'; - const passwd = BRIDGE_HTPASSWD_PASSWORD || 'test'; - cy.login(idp, username, passwd); - cy.url().should('include', Cypress.config('baseUrl')); - - // test Developer perspective is default for test user - // Below line to be uncommented after pr https://github.com/openshift/console-operator/pull/954 is merged - masthead.username.shouldHaveText(username); - - cy.log('switches from dev to admin perspective'); - // nav.sidenav.switcher.shouldHaveText('Developer'); - nav.sidenav.switcher.changePerspectiveTo('Core platform'); - nav.sidenav.switcher.shouldHaveText('Core platform'); - - cy.log('does not show admin nav items in Administration to test user'); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(10000); // wait for feature FLAGS to load - nav.sidenav.shouldNotHaveNavSection(['Administration', 'Cluster Status']); - nav.sidenav.shouldNotHaveNavSection(['Administration', 'Cluster Settings']); - nav.sidenav.shouldNotHaveNavSection(['Administration', 'Namespaces']); - nav.sidenav.shouldNotHaveNavSection(['Administration', 'Custom Resource Definitions']); - - cy.log('does not show admin nav items in Ecosystem to test user'); - nav.sidenav.shouldNotHaveNavSection(['Ecosystem', 'Software Catalog']); - - cy.log('does not show admin nav items in Storage to test user'); - nav.sidenav.shouldNotHaveNavSection(['Storage', 'Persistent Volumes']); - - cy.log('does not show Compute or Monitoring to test user'); - nav.sidenav.shouldNotHaveNavSection(['Compute']); - nav.sidenav.shouldNotHaveNavSection(['Monitoring']); - }, - ); - }); - - it(`log in as 'kubeadmin' user`, () => { - cy.env(['BRIDGE_KUBEADMIN_PASSWORD']).then(({ BRIDGE_KUBEADMIN_PASSWORD }) => { - cy.login(KUBEADMIN_IDP, KUBEADMIN_USERNAME, BRIDGE_KUBEADMIN_PASSWORD); - cy.byTestID('loading-indicator').should('not.exist'); - cy.url().should('include', Cypress.config('baseUrl')); - masthead.username.shouldHaveText(KUBEADMIN_IDP); - cy.byTestID('global-notifications').contains( - 'You are logged in as a temporary administrative user. Update the cluster OAuth configuration to allow others to log in.', - ); - - // test Administrator perspective is default for kubeadmin - nav.sidenav.switcher.shouldHaveText('Core platform'); - // test guided tour is displayed first time switching to 'Developer' perspective - // skip if running localhost - if (!Cypress.config('baseUrl').includes('localhost')) { - // nav.sidenav.switcher.changePerspectiveTo('Developer'); - // nav.sidenav.switcher.shouldHaveText('Developer'); - nav.sidenav.switcher.changePerspectiveTo('Core platform'); - nav.sidenav.switcher.shouldHaveText('Core platform'); - } - cy.log('verify sidenav menus and Administration menu access for cluster admin user'); - nav.sidenav.shouldHaveNavSection(['Compute']); - nav.sidenav.shouldHaveNavSection(['Operators']); - nav.sidenav.clickNavLink(['Administration', 'Cluster Settings']); - cy.byLegacyTestID('cluster-settings-page-heading').should('be.visible'); - }); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/debug-pod.cy.ts b/frontend/packages/integration-tests/tests/app/debug-pod.cy.ts deleted file mode 100644 index e5714a694f3..00000000000 --- a/frontend/packages/integration-tests/tests/app/debug-pod.cy.ts +++ /dev/null @@ -1,113 +0,0 @@ -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 POD_NAME = `pod1`; -const CONTAINER_NAME = `container1`; -const XTERM_CLASS = `[class="xterm-viewport"]`; -const podToDebug = `apiVersion: v1 -kind: Pod -metadata: - name: ${POD_NAME} -spec: - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - name: ${CONTAINER_NAME} - image: quay.io/fedora/fedora - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - restartPolicy: Always`; - -describe('Debug pod', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.visit('/'); - cy.deleteProjectWithCLI(testName); - }); - - it('Create pod that has crashbackloop error', () => { - cy.visit(`/k8s/ns/${testName}/import`); - yamlEditor.isImportLoaded(); - yamlEditor.setEditorContent(podToDebug).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - detailsPage.sectionHeaderShouldExist('Pod details'); - }); - }); - - it('Opens debug terminal page from Logs subsection', () => { - cy.visit(`/k8s/ns/${testName}/pods`); - listPage.dvRows.shouldExist(POD_NAME); - cy.visit(`/k8s/ns/${testName}/pods/${POD_NAME}`); - detailsPage.isLoaded(); - detailsPage.selectTab('Logs'); - detailsPage.isLoaded(); - cy.byTestID('debug-container-link').click(); - listPage.titleShouldHaveText(`Debug ${CONTAINER_NAME}`); - cy.get(XTERM_CLASS).should('exist'); - cy.get('[data-test-id="breadcrumb-link-0"]').click(); - listPage.dvRows.shouldExist(POD_NAME); - }); - - it('Opens debug terminal page from Pod Details - Status tool tip', () => { - cy.visit(`/k8s/ns/${testName}/pods/${POD_NAME}`); - detailsPage.isLoaded(); - cy.byTestID('popover-status-button', { timeout: 60000 }).click(); - // Regression test for OCPBUGS-83813: Wait for popover content to be stable before clicking - // https://issues.redhat.com/browse/OCPBUGS-83813 - cy.byTestID(`popup-debug-container-link-${CONTAINER_NAME}`).should('be.visible'); - cy.byTestID(`popup-debug-container-link-${CONTAINER_NAME}`).click(); - listPage.titleShouldHaveText(`Debug ${CONTAINER_NAME}`); - cy.get(XTERM_CLASS).should('exist'); - cy.get('[data-test-id="breadcrumb-link-0"]').click(); - listPage.dvRows.shouldExist(POD_NAME); - }); - - it('Opens debug terminal page from Pods Page - Status tool tip', () => { - cy.visit(`/k8s/ns/${testName}/pods`); - listPage.dvRows.shouldExist(POD_NAME); - listPage.dvRows.clickStatusButton(POD_NAME); - // Regression test for OCPBUGS-83813: Wait for popover content to be stable before clicking - // https://issues.redhat.com/browse/OCPBUGS-83813 - cy.byTestID(`popup-debug-container-link-${CONTAINER_NAME}`).should('be.visible').click(); - listPage.titleShouldHaveText(`Debug ${CONTAINER_NAME}`); - cy.get(XTERM_CLASS).should('exist'); - - cy.log('debug pod should not copy main pod network info'); - cy.exec( - `oc get pods -n ${testName} -o jsonpath='{.items[0].status.podIP}{"#"}{.items[1].status.podIP}'`, - ).then((result) => { - const [ipAddressOne, ipAddressTwo] = result.stdout.split('#'); - expect(`${ipAddressOne}`).to.not.equal(`${ipAddressTwo}`); - }); - cy.get('[data-test-id="breadcrumb-link-0"]').click(); - listPage.dvRows.shouldExist(POD_NAME); - }); - - it('Debug pod should be terminated after leaving debug container page', () => { - cy.visit(`/k8s/ns/${testName}/pods`); - listPage.dvRows.shouldExist(POD_NAME); - listPage.dvFilter.by('Status', 'Running'); - cy.exec( - `oc get pods -n ${testName} -o jsonpath='{.items[0].metadata.name}{"#"}{.items[1].metadata.name}'`, - ).then((result) => { - const debugPodName = result.stdout.split('#')[1]; - listPage.dvRows.shouldNotExist(debugPodName); - }); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/deployments.cy.ts b/frontend/packages/integration-tests/tests/app/deployments.cy.ts deleted file mode 100644 index 1f5f56f00b8..00000000000 --- a/frontend/packages/integration-tests/tests/app/deployments.cy.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import { modal } from '../../views/modal'; - -describe('Deployment resource details page', () => { - let WORKLOAD_NAME; - let CREATE_DEPLOYMENT; - let CREATE_HPA; - - before(() => { - cy.login(); - cy.initAdmin(); - cy.createProjectWithCLI(testName); - - WORKLOAD_NAME = `deployment-${testName}`; - CREATE_DEPLOYMENT = `oc create deployment ${WORKLOAD_NAME} --image=httpd --replicas=0`; - CREATE_HPA = `oc autoscale deployment ${WORKLOAD_NAME} --min=1 --max=10`; - - // Create a deployment named foo with 0 replicas using the cli - cy.exec(CREATE_DEPLOYMENT, { - failOnNonZeroExit: false, - }); - // Create an HorizontalPodAutoscaler using the cli that autoscales the deployment foo - cy.exec(CREATE_HPA, { failOnNonZeroExit: false }); - cy.visit(`/k8s/ns/${testName}/deployments`); - }); - - beforeEach(() => { - cy.visitAndWait(`/k8s/ns/${testName}/deployments/${WORKLOAD_NAME}`); - detailsPage.isLoaded(); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.visit(`/k8s/ns/${testName}/deployments`); - listPage.dvRows.shouldBeLoaded(); - listPage.dvFilter.byName(WORKLOAD_NAME); - listPage.dvRows.clickKebabAction(WORKLOAD_NAME, 'Delete Deployment'); - modal.shouldBeOpened(); - modal.submit(); - modal.shouldBeClosed(); - cy.deleteProjectWithCLI(testName); - }); - - it('Enable deployment autoscale button should exist', () => { - cy.byTestID('enable-autoscale').should('exist').click(); - }); - it('Enable deployment autoscale button should not exist', () => { - cy.byTestID('enable-autoscale').should('not.exist'); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/machine-config.cy.ts b/frontend/packages/integration-tests/tests/app/machine-config.cy.ts deleted file mode 100644 index 6f8bd0bc50f..00000000000 --- a/frontend/packages/integration-tests/tests/app/machine-config.cy.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { checkErrors } from '../../support'; -import { detailsPage } from '../../views/details-page'; - -const MC_WITH_CONFIG_FILES = '00-master'; -const MC_WITHOUT_CONFIG_FILES = '99-master-ssh'; -const MC_DETAILS_PAGE_URL = '/k8s/cluster/machineconfiguration.openshift.io~v1~MachineConfig/'; -const MC_SECTION_HEADING = 'Configuration files'; -const MC_CONFIG_FILE_PATH_ID = 'config-file-path-0'; -const MC_C2C = '.co-copy-to-clipboard__text'; -const checkMachineConfigDetails = (mode, overwrite, content) => { - cy.byTestID(MC_CONFIG_FILE_PATH_ID).scrollIntoView(); - cy.get('button[aria-label="Info"]').first().click(); - cy.contains(mode).should('exist'); - cy.contains(overwrite.toString()).should('exist'); - cy.get('code') - .first() - .should(($code) => { - const text = $code.text(); - expect(text).to.include( - decodeURIComponent(content) - .replace(/^(data:,)/, '') - .slice(0, 30), - ); - }); -}; - -describe('MachineConfig resource details page', () => { - before(() => { - cy.login(); - cy.initAdmin(); - }); - - afterEach(() => { - checkErrors(); - }); - - it(`${MC_WITH_CONFIG_FILES} displays configuration files`, () => { - cy.visit(`${MC_DETAILS_PAGE_URL}${MC_WITH_CONFIG_FILES}`); - detailsPage.titleShouldContain(`${MC_WITH_CONFIG_FILES}`); - detailsPage.isLoaded(); - cy.byTestSectionHeading(MC_SECTION_HEADING).should('exist'); - cy.byTestID(MC_CONFIG_FILE_PATH_ID).should('exist'); - cy.get(MC_C2C).should('exist'); - cy.exec(`oc get mc ${MC_WITH_CONFIG_FILES} -o jsonpath='{.spec.config.storage.files[0]}'`).then( - (result) => { - const mcContents = JSON.parse(result.stdout); - expect(mcContents).to.have.property('contents'); - expect(mcContents).to.have.property('mode'); - expect(mcContents).to.have.property('overwrite'); - const { - contents: { source }, - mode, - overwrite, - } = mcContents; - checkMachineConfigDetails(mode, overwrite, source); - }, - ); - }); - - it(`${MC_WITHOUT_CONFIG_FILES} does not display configuration files`, () => { - cy.visit(`${MC_DETAILS_PAGE_URL}${MC_WITHOUT_CONFIG_FILES}`); - detailsPage.titleShouldContain(`${MC_WITHOUT_CONFIG_FILES}`); - detailsPage.isLoaded(); - cy.byTestSectionHeading(MC_SECTION_HEADING).should('not.exist'); - cy.byTestID(MC_CONFIG_FILE_PATH_ID).should('not.exist'); - cy.get(MC_C2C).should('not.exist'); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/start-job-from-cronjob.cy.ts b/frontend/packages/integration-tests/tests/app/start-job-from-cronjob.cy.ts deleted file mode 100644 index 08efc0ae1c4..00000000000 --- a/frontend/packages/integration-tests/tests/app/start-job-from-cronjob.cy.ts +++ /dev/null @@ -1,76 +0,0 @@ -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 CRONJOB_NAME = 'cronjob1'; - -const cronJobReqPayload = `apiVersion: batch/v1 -kind: CronJob -metadata: - name: ${CRONJOB_NAME} - namespace: ${testName} -spec: - schedule: '@daily' - jobTemplate: - spec: - template: - spec: - containers: - - name: hello - image: busybox - args: - - /bin/sh - - '-c' - - date; echo Hello from the Openshift cluster - restartPolicy: OnFailure`; - -describe('Start a Job from a CronJob', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.visit('/'); - cy.deleteProjectWithCLI(testName); - }); - - it('verify "Start Job" on the CronJob details page', () => { - cy.visit(`/k8s/ns/${testName}/import`); - yamlEditor.isImportLoaded(); - yamlEditor.setEditorContent(cronJobReqPayload).then(() => { - yamlEditor.clickSaveCreateButton(); - detailsPage.sectionHeaderShouldExist('CronJob details'); - }); - detailsPage.clickPageActionFromDropdown('Start Job'); - detailsPage.isLoaded(); - detailsPage.sectionHeaderShouldExist('Job details'); - detailsPage.titleShouldContain(`${CRONJOB_NAME}`); - }); - - it('verify "Start Job" on the CronJob list page', () => { - cy.visit(`/k8s/ns/${testName}/cronjobs`); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickKebabAction(CRONJOB_NAME, 'Start Job'); - detailsPage.isLoaded(); - detailsPage.sectionHeaderShouldExist('Job details'); - detailsPage.titleShouldContain(`${CRONJOB_NAME}`); - }); - - it('verify the number of Jobs in CronJob > Jobs tab list page', () => { - cy.visit(`/k8s/ns/${testName}/cronjobs`); - listPage.dvRows.shouldBeLoaded(); - cy.visit(`/k8s/ns/${testName}/cronjobs/${CRONJOB_NAME}/jobs`); - listPage.dvRows.countShouldBe(2); - }); - - it('verify the number of events in CronJob > Events tab list page', () => { - cy.visit(`/k8s/ns/${testName}/cronjobs/${CRONJOB_NAME}/events`); - cy.byTestID('event-totals').should('have.text', 'Showing 2 events'); - }); -});