From 8810a34b54c33ff2243879eacc24ba43a33f81e9 Mon Sep 17 00:00:00 2001 From: stefanonardo Date: Tue, 12 May 2026 09:22:08 +0200 Subject: [PATCH] CONSOLE-5235: Migrate basic app Cypress e2e tests to Playwright Migrate 6 Cypress test files (21 tests) from packages/integration-tests/tests/app/ to Playwright: - masthead (6 tests) - overview (2 tests) - node-terminal (1 test) - resource-log (3 tests) - template (1 test) - filtering-and-searching (8 tests) New page objects: list-page, details-page, logs-page, catalog-page, masthead-page, overview-page. Extended KubernetesClient with createPod, deletePod, waitForPodReady, createDeployment, waitForDeploymentReady. Deleted Cypress files and their exclusive dependencies (views/logs.ts, views/catalogs.ts, views/overview.ts, fixture YAMLs) after validation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + frontend/e2e/clients/kubernetes-client.ts | 61 ++++++ frontend/e2e/pages/catalog-page.ts | 26 +++ frontend/e2e/pages/details-page.ts | 27 +++ frontend/e2e/pages/list-page.ts | 68 +++++++ frontend/e2e/pages/logs-page.ts | 58 ++++++ frontend/e2e/pages/masthead-page.ts | 52 +++++ frontend/e2e/pages/overview-page.ts | 44 +++++ .../app/filtering-and-searching.spec.ts | 146 ++++++++++++++ .../e2e/tests/console/app/masthead.spec.ts | 68 +++++++ .../tests/console/app/node-terminal.spec.ts | 32 +++ .../e2e/tests/console/app/overview.spec.ts | 155 +++++++++++++++ .../tests/console/app/resource-log.spec.ts | 157 +++++++++++++++ .../e2e/tests/console/app/template.spec.ts | 61 ++++++ .../fixtures/httpd-example-template.yaml | 182 ------------------ .../fixtures/pod-with-space.yaml | 48 ----- .../fixtures/pod-with-wrap-annotation.yaml | 23 --- .../tests/app/filtering-and-searching.cy.ts | 137 ------------- .../tests/app/masthead.cy.ts | 88 --------- .../tests/app/node-terminal.cy.ts | 30 --- .../tests/app/overview.cy.ts | 72 ------- .../tests/app/resource-log.cy.ts | 68 ------- .../tests/app/template.cy.ts | 28 --- .../integration-tests/views/catalogs.ts | 10 - .../packages/integration-tests/views/logs.ts | 37 ---- .../integration-tests/views/overview.ts | 23 --- 26 files changed, 956 insertions(+), 746 deletions(-) create mode 100644 frontend/e2e/pages/catalog-page.ts create mode 100644 frontend/e2e/pages/details-page.ts create mode 100644 frontend/e2e/pages/list-page.ts create mode 100644 frontend/e2e/pages/logs-page.ts create mode 100644 frontend/e2e/pages/masthead-page.ts create mode 100644 frontend/e2e/pages/overview-page.ts create mode 100644 frontend/e2e/tests/console/app/filtering-and-searching.spec.ts create mode 100644 frontend/e2e/tests/console/app/masthead.spec.ts create mode 100644 frontend/e2e/tests/console/app/node-terminal.spec.ts create mode 100644 frontend/e2e/tests/console/app/overview.spec.ts create mode 100644 frontend/e2e/tests/console/app/resource-log.spec.ts create mode 100644 frontend/e2e/tests/console/app/template.spec.ts delete mode 100644 frontend/packages/integration-tests/fixtures/httpd-example-template.yaml delete mode 100644 frontend/packages/integration-tests/fixtures/pod-with-space.yaml delete mode 100644 frontend/packages/integration-tests/fixtures/pod-with-wrap-annotation.yaml delete mode 100644 frontend/packages/integration-tests/tests/app/filtering-and-searching.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/masthead.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/node-terminal.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/overview.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/resource-log.cy.ts delete mode 100644 frontend/packages/integration-tests/tests/app/template.cy.ts delete mode 100644 frontend/packages/integration-tests/views/catalogs.ts delete mode 100644 frontend/packages/integration-tests/views/logs.ts delete mode 100644 frontend/packages/integration-tests/views/overview.ts diff --git a/.gitignore b/.gitignore index 7b9a9530880..50e8ec8b9c3 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/frontend/e2e/clients/kubernetes-client.ts b/frontend/e2e/clients/kubernetes-client.ts index a9255d43cf4..796d2c75b3b 100644 --- a/frontend/e2e/clients/kubernetes-client.ts +++ b/frontend/e2e/clients/kubernetes-client.ts @@ -471,4 +471,65 @@ export default class KubernetesClient { const response = await this.k8sApi.listNamespacedPod({ namespace }); return response.items || []; } + + async createPod(namespace: string, body: Partial): Promise { + await this.k8sApi.createNamespacedPod({ namespace, body: body as k8s.V1Pod }); + } + + async deletePod(name: string, namespace: string): Promise { + try { + await this.k8sApi.deleteNamespacedPod({ name, namespace }); + } catch (err) { + if (!isNotFound(err)) { + throw err; + } + } + } + + async waitForPodReady(name: string, namespace: string, timeoutMs = 120_000): Promise { + return pollUntil( + async () => { + try { + const pod = await this.k8sApi.readNamespacedPod({ name, namespace }); + if (pod?.status?.phase !== 'Running') { + return false; + } + const containers = pod?.status?.containerStatuses; + if (!containers || containers.length === 0) { + return false; + } + return containers.every((c) => c.ready === true); + } catch { + return false; + } + }, + timeoutMs, + 2_000, + ); + } + + async createDeployment(namespace: string, body: Partial): Promise { + await this.appsApi.createNamespacedDeployment({ namespace, body: body as k8s.V1Deployment }); + } + + async waitForDeploymentReady( + name: string, + namespace: string, + timeoutMs = 120_000, + ): Promise { + return pollUntil( + async () => { + try { + const dep = await this.appsApi.readNamespacedDeployment({ name, namespace }); + const ready = dep?.status?.readyReplicas ?? 0; + const desired = dep?.spec?.replicas ?? 1; + return ready >= desired; + } catch { + return false; + } + }, + timeoutMs, + 2_000, + ); + } } diff --git a/frontend/e2e/pages/catalog-page.ts b/frontend/e2e/pages/catalog-page.ts new file mode 100644 index 00000000000..68c299611b6 --- /dev/null +++ b/frontend/e2e/pages/catalog-page.ts @@ -0,0 +1,26 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class CatalogPage extends BasePage { + private readonly filterInput: Locator = this.page.locator( + 'input[placeholder*="Filter by keyword"]', + ); + + async navigateToCatalog(): Promise { + await this.goTo('/catalog/all-namespaces'); + await this.filterInput.waitFor({ state: 'visible', timeout: 60_000 }); + } + + async filterByKeyword(keyword: string): Promise { + await this.filterInput.fill(keyword); + } + + catalogItem(testId: string): Locator { + return this.page.getByTestId(testId); + } + + catalogItemIcon(testId: string): Locator { + return this.catalogItem(testId).locator('img.catalog-tile-pf-icon'); + } +} diff --git a/frontend/e2e/pages/details-page.ts b/frontend/e2e/pages/details-page.ts new file mode 100644 index 00000000000..ff622b4361c --- /dev/null +++ b/frontend/e2e/pages/details-page.ts @@ -0,0 +1,27 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class DetailsPage extends BasePage { + private readonly skeletonView: Locator = this.page.getByTestId('skeleton-detail-view'); + private readonly resourceTitle: Locator = this.page.locator('[data-test-id="resource-title"]'); + readonly nodeTerminalError: Locator = this.page.getByTestId('node-terminal-error'); + readonly xtermViewport: Locator = this.page.locator('.xterm-viewport'); + + get title(): Locator { + return this.resourceTitle; + } + + async waitForLoaded(): Promise { + await this.skeletonView.waitFor({ state: 'hidden', timeout: 30_000 }).catch(() => {}); + await this.resourceTitle.waitFor({ state: 'visible', timeout: 30_000 }); + } + + tab(name: string): Locator { + return this.page.locator(`[data-test-id="horizontal-link-${name}"]`); + } + + async selectTab(name: string): Promise { + await this.navigateToTab(this.tab(name)); + } +} diff --git a/frontend/e2e/pages/list-page.ts b/frontend/e2e/pages/list-page.ts new file mode 100644 index 00000000000..0ca67e2d9fd --- /dev/null +++ b/frontend/e2e/pages/list-page.ts @@ -0,0 +1,68 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class ListPage extends BasePage { + private readonly pageHeading: Locator = this.page.getByTestId('page-heading').locator('h1'); + private readonly dataViewTable: Locator = this.page.getByTestId('data-view-table'); + private readonly dataViewCells: Locator = this.page.locator('[data-test^="data-view-cell-"]'); + private readonly dataViewFilters: Locator = this.page.locator( + '[data-ouia-component-id="DataViewFilters"]', + ); + private readonly nameFilterInput: Locator = this.page.locator('[aria-label="Filter by name"]'); + private readonly singleFilterGroup: Locator = this.page.locator( + '.co-console-data-view-single-filter .pf-v6-c-toolbar__group.pf-m-filter-group', + ); + + get heading(): Locator { + return this.pageHeading; + } + + get table(): Locator { + return this.dataViewTable; + } + + get cells(): Locator { + return this.dataViewCells; + } + + get filterGroupToggles(): Locator { + return this.singleFilterGroup.locator('.pf-v6-c-menu-toggle'); + } + + cell(resourceName: string, cellName = 'name'): Locator { + return this.page.locator(`[data-test="data-view-cell-${resourceName}-${cellName}"]`); + } + + async waitForRows(): Promise { + await this.dataViewTable.waitFor({ state: 'visible', timeout: 30_000 }); + } + + async filterByName(name: string): Promise { + const filterToggle = this.dataViewFilters.locator('.pf-v6-c-menu-toggle').first(); + await this.robustClick(filterToggle); + await this.robustClick(this.page.locator('.pf-v6-c-menu__list-item', { hasText: 'Name' })); + await this.nameFilterInput.waitFor({ state: 'visible' }); + await this.nameFilterInput.fill(name); + } + + async clickFirstRowLink(): Promise { + const firstLink = this.dataViewCells.first().locator('a').first(); + await this.robustClick(firstLink); + } + + async clickFirstRowLinkMatching(pattern: RegExp): Promise { + const safeFlags = pattern.flags.replace(/[gy]/g, ''); + const safePattern = new RegExp(pattern.source, safeFlags); + const links = this.dataViewCells.locator('a'); + const count = await links.count(); + for (let i = 0; i < count; i++) { + const text = await links.nth(i).textContent(); + if (text && safePattern.test(text)) { + await this.robustClick(links.nth(i)); + return; + } + } + throw new Error(`No row link matching ${pattern} found`); + } +} diff --git a/frontend/e2e/pages/logs-page.ts b/frontend/e2e/pages/logs-page.ts new file mode 100644 index 00000000000..e90d9c998fb --- /dev/null +++ b/frontend/e2e/pages/logs-page.ts @@ -0,0 +1,58 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class LogsPage extends BasePage { + readonly lineCount: Locator = this.page.getByTestId('resource-log-no-lines'); + private readonly optionsToggle: Locator = this.page.getByTestId('resource-log-options-toggle'); + private readonly showFullLogOption: Locator = this.page.locator( + '[data-test-dropdown-menu="show-full-log"]', + ); + private readonly wrapLinesOption: Locator = this.page.locator( + '[data-test-dropdown-menu="wrap-lines"]', + ); + private readonly wrapCheckbox: Locator = this.wrapLinesOption.locator('input[type="checkbox"]'); + private readonly containerSelect: Locator = this.page.getByTestId('container-select'); + private readonly searchInput: Locator = this.page.locator('input[placeholder="Search logs"]'); + readonly searchMatches: Locator = this.page.locator('.pf-m-match'); + readonly logText: Locator = this.page.locator('span[class$="c-log-viewer__text"]'); + + async waitForLoaded(): Promise { + await this.optionsToggle.waitFor({ state: 'visible', timeout: 60_000 }); + } + + async toggleOptions(): Promise { + await this.robustClick(this.optionsToggle); + } + + async clickShowFullLog(): Promise { + await this.toggleOptions(); + await this.robustClick(this.showFullLogOption); + } + + async setWrap(enabled: boolean): Promise { + await this.toggleOptions(); + if (enabled) { + await this.wrapCheckbox.check(); + } else { + await this.wrapCheckbox.uncheck(); + } + await this.toggleOptions(); + } + + async isWrapChecked(): Promise { + await this.toggleOptions(); + const checked = await this.wrapCheckbox.isChecked(); + await this.toggleOptions(); + return checked; + } + + async selectContainer(name: string): Promise { + await this.robustClick(this.containerSelect); + await this.robustClick(this.page.locator(`[data-test-dropdown-menu="${name}"]`)); + } + + async searchLogs(text: string): Promise { + await this.searchInput.fill(text); + } +} diff --git a/frontend/e2e/pages/masthead-page.ts b/frontend/e2e/pages/masthead-page.ts new file mode 100644 index 00000000000..98161e42ed7 --- /dev/null +++ b/frontend/e2e/pages/masthead-page.ts @@ -0,0 +1,52 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class MastheadPage extends BasePage { + private readonly logo: Locator = this.page.getByTestId('masthead-logo'); + private readonly quickCreateToggle: Locator = this.page.getByTestId('quick-create-dropdown'); + private readonly userDropdownToggle: Locator = this.page.getByTestId('user-dropdown-toggle'); + private readonly copyLoginCommandLink: Locator = this.page + .getByTestId('copy-login-command') + .locator('a'); + private readonly logOutItem: Locator = this.page.getByTestId('log-out'); + readonly pageHeading: Locator = this.page.getByTestId('page-heading').locator('h1'); + + get logoLocator(): Locator { + return this.logo; + } + + async openQuickCreate(): Promise { + await this.quickCreateToggle.click(); + } + + async clickQuickCreateItem(testId: string): Promise { + const item = this.page.getByTestId(testId); + await item.waitFor({ state: 'visible' }); + await item.locator('a').click({ force: true }); + } + + async openUserDropdown(): Promise { + await this.userDropdownToggle.click(); + } + + async isAuthDisabled(): Promise { + return this.page.evaluate(() => { + const w = window as Window & { SERVER_FLAGS?: { authDisabled?: boolean } }; + return !!w.SERVER_FLAGS?.authDisabled; + }); + } + + async clickCopyLoginCommand(): Promise { + await this.copyLoginCommandLink.waitFor({ state: 'visible' }); + await this.copyLoginCommandLink.evaluate((el: HTMLAnchorElement) => + el.removeAttribute('target'), + ); + await this.copyLoginCommandLink.click(); + } + + async clickLogOut(): Promise { + await this.logOutItem.waitFor({ state: 'visible' }); + await this.logOutItem.click({ force: true }); + } +} diff --git a/frontend/e2e/pages/overview-page.ts b/frontend/e2e/pages/overview-page.ts new file mode 100644 index 00000000000..debd1f00fad --- /dev/null +++ b/frontend/e2e/pages/overview-page.ts @@ -0,0 +1,44 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class OverviewPage extends BasePage { + private readonly listView: Locator = this.page.locator('.odc-topology-list-view'); + private readonly itemRows: Locator = this.page.locator('.odc-topology-list-view__item-row'); + private readonly kindLabels: Locator = this.page.locator('.odc-topology-list-view__kind-label'); + private readonly labelCells: Locator = this.page.locator('.odc-topology-list-view__label-cell'); + private readonly sidebar: Locator = this.page.locator('.resource-overview'); + private readonly sidebarHeading: Locator = this.page.locator('.resource-overview__heading h1'); + + get listViewLocator(): Locator { + return this.listView; + } + + get itemRowsLocator(): Locator { + return this.itemRows; + } + + get sidebarLocator(): Locator { + return this.sidebar; + } + + get sidebarHeadingLocator(): Locator { + return this.sidebarHeading; + } + + kindLabel(label: string): Locator { + return this.kindLabels.filter({ hasText: label }); + } + + labelCell(name: string): Locator { + return this.labelCells.filter({ hasText: name }); + } + + async navigateToWorkloads(projectName: string): Promise { + await this.goTo(`/k8s/cluster/projects/${projectName}/workloads?view=list`); + } + + async clickListItem(name: string): Promise { + await this.labelCell(name).click(); + } +} diff --git a/frontend/e2e/tests/console/app/filtering-and-searching.spec.ts b/frontend/e2e/tests/console/app/filtering-and-searching.spec.ts new file mode 100644 index 00000000000..ed5b2163644 --- /dev/null +++ b/frontend/e2e/tests/console/app/filtering-and-searching.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from '../../../fixtures'; +import KubernetesClient from '../../../clients/kubernetes-client'; +import { ListPage } from '../../../pages/list-page'; + +const SEARCH_NAMESPACE = 'openshift-authentication-operator'; +const SEARCH_DEPLOYMENT_NAME = 'authentication-operator'; + +function createClient(): KubernetesClient { + return new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); +} + +test.describe('Filtering and Searching', { tag: ['@admin'] }, () => { + let ns: string; + let workloadName: string; + + test.beforeAll(async ({ browser }, workerInfo) => { + ns = `test-filter-${workerInfo.workerIndex}-${Date.now()}`; + workloadName = `filter-${ns.slice(-8)}`; + const client = createClient(); + + const page = await browser.newPage(); + await client.createNamespace(ns); + await client.createDeployment(ns, { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: workloadName, + labels: { 'lbl-filter': ns, app: 'name' }, + }, + spec: { + replicas: 3, + selector: { matchLabels: { app: 'name' } }, + template: { + metadata: { labels: { app: 'name' } }, + spec: { + securityContext: { runAsNonRoot: true, seccompProfile: { type: 'RuntimeDefault' } }, + containers: [ + { + name: 'httpd', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + securityContext: { + allowPrivilegeEscalation: false, + capabilities: { drop: ['ALL'] }, + }, + }, + ], + }, + }, + }, + }); + await client.waitForDeploymentReady(workloadName, ns); + await page.close(); + }); + + test.afterAll(async () => { + const client = createClient(); + await client.deleteNamespace(ns); + }); + + test('filters Pod from object detail', async ({ page }) => { + const listPage = new ListPage(page); + + await page.goto(`/k8s/ns/${ns}/deployments/${workloadName}/pods`); + await listPage.waitForRows(); + await listPage.filterByName(workloadName); + await expect(listPage.cells).toHaveCount(3, { timeout: 30_000 }); + }); + + test('filters invalid Pod from object detail', async ({ page }) => { + const listPage = new ListPage(page); + + await page.goto(`/k8s/ns/${ns}/deployments/${workloadName}/pods`); + await listPage.waitForRows(); + await listPage.filterByName('XYZ123'); + await expect(listPage.table.locator('.pf-v6-l-bullseye')).toContainText('No Pods found'); + }); + + test('filters from Pods list', async ({ page }) => { + const listPage = new ListPage(page); + + await page.goto('/k8s/all-namespaces/pods'); + await listPage.waitForRows(); + await listPage.filterByName(workloadName); + await expect(listPage.cells).toHaveCount(3, { timeout: 30_000 }); + }); + + test('displays namespace column in Search for All Namespaces', async ({ page }) => { + const listPage = new ListPage(page); + + await page.goto( + `/search/all-namespaces?kind=apps~v1~Deployment&page=1&perPage=50&name=${SEARCH_DEPLOYMENT_NAME}`, + ); + await listPage.waitForRows(); + await expect(listPage.cells).toHaveCount(1); + await expect(page.locator(`[data-test-id="${SEARCH_NAMESPACE}"]`)).toBeAttached(); + }); + + test('hides namespace column in Search for scoped namespace', async ({ page }) => { + const listPage = new ListPage(page); + + await page.goto( + `/search/ns/${SEARCH_NAMESPACE}?kind=apps~v1~Deployment&page=1&perPage=50&name=${SEARCH_DEPLOYMENT_NAME}`, + ); + await listPage.waitForRows(); + await expect(listPage.cells).toHaveCount(1); + await expect(page.locator(`[data-test-id="${SEARCH_NAMESPACE}"]`)).not.toBeAttached(); + }); + + test('searches for object by kind and label', async ({ page }) => { + const listPage = new ListPage(page); + + await page.goto(`/search/ns/${ns}?kind=Deployment&q=lbl-filter%3D${ns}`); + await listPage.waitForRows(); + await expect(listPage.cell(workloadName)).toBeAttached(); + }); + + test('searches for object by kind, label, and name', async ({ page }) => { + const listPage = new ListPage(page); + + await page.goto(`/search/all-namespaces?kind=Pod&q=app%3Dname&name=${workloadName}`); + await listPage.waitForRows(); + await expect(listPage.cells).toHaveCount(3, { timeout: 30_000 }); + }); + + test('hides filter category select when only one filter exists', async ({ page }) => { + const listPage = new ListPage(page); + + await test.step('Text-only filter page', async () => { + await page.goto('/settings/cluster/alertmanagerconfig?page=1&perPage=50'); + await listPage.table.waitFor({ state: 'visible', timeout: 60_000 }); + await expect(listPage.filterGroupToggles).toHaveCount(1); + await expect(listPage.filterGroupToggles).not.toBeVisible(); + }); + + await test.step('Select-only filter page', async () => { + await page.goto('/search/all-namespaces?page=1&perPage=50&kind=core~v1~Pod'); + await listPage.waitForRows(); + await expect(listPage.filterGroupToggles).toHaveCount(2); + await expect(listPage.filterGroupToggles.first()).not.toBeVisible(); + }); + }); +}); diff --git a/frontend/e2e/tests/console/app/masthead.spec.ts b/frontend/e2e/tests/console/app/masthead.spec.ts new file mode 100644 index 00000000000..94322688374 --- /dev/null +++ b/frontend/e2e/tests/console/app/masthead.spec.ts @@ -0,0 +1,68 @@ +import { test, expect } from '../../../fixtures'; +import { MastheadPage } from '../../../pages/masthead-page'; + +test.describe('Masthead', { tag: ['@smoke'] }, () => { + test('logo should be restricted to a max-height of 60px', async ({ page }) => { + const masthead = new MastheadPage(page); + await page.goto('/'); + + await expect(masthead.logoLocator).toBeVisible(); + await expect(masthead.logoLocator).toHaveCSS('max-height', '60px'); + + const height = await masthead.logoLocator.evaluate( + (el: HTMLElement) => el.getBoundingClientRect().height, + ); + expect(height).toBeLessThanOrEqual(60); + }); + + const quickCreateItems = [ + { testId: 'qc-import-yaml', heading: 'Import YAML' }, + { testId: 'qc-import-from-git', heading: 'Import from Git' }, + { testId: 'qc-container-images', heading: 'Deploy Image' }, + ] as const; + + for (const { testId, heading } of quickCreateItems) { + test(`quick create should open ${heading}`, async ({ page }) => { + const masthead = new MastheadPage(page); + await page.goto('/'); + + await test.step('Open quick create and click item', async () => { + await masthead.openQuickCreate(); + await masthead.clickQuickCreateItem(testId); + }); + + await test.step('Verify page heading', async () => { + await expect(masthead.pageHeading).toContainText(heading); + }); + }); + } + + test('should render the correct copy login command link', async ({ page }) => { + const masthead = new MastheadPage(page); + await page.goto('/'); + + const authDisabled = await masthead.isAuthDisabled(); + test.skip(authDisabled, 'Auth is disabled — skipping copy login command test'); + + await test.step('Open user dropdown and click copy login command', async () => { + await masthead.openUserDropdown(); + await masthead.clickCopyLoginCommand(); + }); + + await test.step('Verify token display page', async () => { + await expect(page).toHaveURL(/\/oauth\/token\/display/); + await expect(page.locator('body')).toContainText('Display Token'); + }); + }); + + test('should log the user out', async ({ page }) => { + const masthead = new MastheadPage(page); + await page.goto('/'); + + const authDisabled = await masthead.isAuthDisabled(); + test.skip(authDisabled, 'Auth is disabled — skipping logout test'); + + await masthead.openUserDropdown(); + await masthead.clickLogOut(); + }); +}); diff --git a/frontend/e2e/tests/console/app/node-terminal.spec.ts b/frontend/e2e/tests/console/app/node-terminal.spec.ts new file mode 100644 index 00000000000..7e6b40d3440 --- /dev/null +++ b/frontend/e2e/tests/console/app/node-terminal.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../../fixtures'; +import { DetailsPage } from '../../../pages/details-page'; +import { ListPage } from '../../../pages/list-page'; + +test.describe('Node terminal', { tag: ['@admin'] }, () => { + test('opens a debug terminal on a node', async ({ page }) => { + const listPage = new ListPage(page); + const detailsPage = new DetailsPage(page); + + await test.step('Navigate to Nodes list', async () => { + await page.goto('/k8s/cluster/nodes'); + await expect(listPage.heading).toContainText('Nodes'); + await listPage.waitForRows(); + }); + + await test.step('Open first node details', async () => { + await listPage.clickFirstRowLink(); + await detailsPage.waitForLoaded(); + }); + + await test.step('Verify terminal loads without errors', async () => { + await detailsPage.selectTab('Terminal'); + await expect(detailsPage.nodeTerminalError).not.toBeAttached(); + await expect(detailsPage.xtermViewport).toBeVisible({ timeout: 60_000 }); + }); + + // Navigate away from Terminal so the temporary debug namespace is deleted + await test.step('Navigate back to Overview', async () => { + await detailsPage.selectTab('Overview'); + }); + }); +}); diff --git a/frontend/e2e/tests/console/app/overview.spec.ts b/frontend/e2e/tests/console/app/overview.spec.ts new file mode 100644 index 00000000000..5466834635a --- /dev/null +++ b/frontend/e2e/tests/console/app/overview.spec.ts @@ -0,0 +1,155 @@ +import { test, expect } from '../../../fixtures'; +import { OverviewPage } from '../../../pages/overview-page'; + +const resourceModels = [ + { + kind: 'DaemonSet', + label: 'DaemonSet', + apiGroup: 'apps', + apiVersion: 'v1', + plural: 'daemonsets', + }, + { + kind: 'Deployment', + label: 'Deployment', + apiGroup: 'apps', + apiVersion: 'v1', + plural: 'deployments', + }, + { + kind: 'DeploymentConfig', + label: 'DeploymentConfig', + apiGroup: 'apps.openshift.io', + apiVersion: 'v1', + plural: 'deploymentconfigs', + }, + { + kind: 'StatefulSet', + label: 'StatefulSet', + apiGroup: 'apps', + apiVersion: 'v1', + plural: 'statefulsets', + }, +] as const; + +function buildResourceBody( + kind: string, + apiGroup: string, + apiVersion: string, + name: string, + namespace: string, +): Record { + const apiVersionField = apiGroup ? `${apiGroup}/${apiVersion}` : apiVersion; + const base = { + apiVersion: apiVersionField, + kind, + metadata: { name, namespace }, + spec: { + selector: { matchLabels: { app: name } }, + template: { + metadata: { labels: { app: name } }, + spec: { + containers: [ + { + name: 'test', + image: 'registry.access.redhat.com/ubi9/ubi-minimal:latest', + command: ['sleep', '3600'], + }, + ], + }, + }, + }, + }; + + if (kind === 'DaemonSet') { + return { + ...base, + spec: { + ...base.spec, + updateStrategy: { type: 'RollingUpdate' }, + }, + }; + } + + if (kind === 'StatefulSet') { + return { + ...base, + spec: { + ...base.spec, + serviceName: name, + }, + }; + } + + if (kind === 'DeploymentConfig') { + return { + ...base, + spec: { + ...base.spec, + selector: { app: name }, + replicas: 1, + }, + }; + } + + if (kind === 'Deployment') { + return { + ...base, + spec: { + ...base.spec, + replicas: 1, + }, + }; + } + + return base; +} + +test.describe('Overview page', { tag: ['@admin'] }, () => { + for (const model of resourceModels) { + test(`displays ${model.kind} in overview list and shows details sidebar`, async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-overview-${model.kind.toLowerCase()}-${Date.now()}`; + const resourceName = `test-${model.kind.toLowerCase()}`; + const overview = new OverviewPage(page); + + await test.step('Create namespace and resource via API', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + + const body = buildResourceBody( + model.kind, + model.apiGroup, + model.apiVersion, + resourceName, + ns, + ); + await k8sClient.createCustomResource( + model.apiGroup, + model.apiVersion, + ns, + model.plural, + body, + ); + }); + + await test.step(`Verify ${model.kind} appears in workloads list`, async () => { + await overview.navigateToWorkloads(ns); + await expect(overview.listViewLocator).toBeVisible(); + await expect(overview.itemRowsLocator.first()).toBeVisible(); + await expect(overview.kindLabel(model.label)).toBeVisible(); + await expect(overview.labelCell(resourceName)).toBeVisible(); + }); + + await test.step('Click resource and verify details sidebar', async () => { + await expect(overview.sidebarLocator).not.toBeAttached(); + await overview.clickListItem(resourceName); + await expect(overview.sidebarLocator).toBeAttached(); + await expect(overview.sidebarHeadingLocator).toContainText(resourceName); + }); + }); + } +}); diff --git a/frontend/e2e/tests/console/app/resource-log.spec.ts b/frontend/e2e/tests/console/app/resource-log.spec.ts new file mode 100644 index 00000000000..1fbb3121a0a --- /dev/null +++ b/frontend/e2e/tests/console/app/resource-log.spec.ts @@ -0,0 +1,157 @@ +import { test, expect } from '../../../fixtures'; +import { DetailsPage } from '../../../pages/details-page'; +import { ListPage } from '../../../pages/list-page'; +import { LogsPage } from '../../../pages/logs-page'; + +const examplePodSpec = { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'examplepod1', labels: { app: 'httpd' } }, + spec: { + securityContext: { runAsNonRoot: true, seccompProfile: { type: 'RuntimeDefault' } }, + containers: [ + { + name: 'container1', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + args: [ + '/bin/sh', + '-c', + 'i=0; while true; do echo "$i:Log TEST $(date)" >> /var/log/1.log; echo "$(date):Log INFO $i" >> /var/log/2.log; i=$((i+1)); sleep 1; done', + ], + volumeMounts: [{ name: 'varlog', mountPath: '/var/log' }], + securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ['ALL'] } }, + }, + { + name: 'container2', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + args: ['/bin/sh', '-c', 'tail -n+1 -f /var/log/1.log'], + volumeMounts: [{ name: 'varlog', mountPath: '/var/log' }], + securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ['ALL'] } }, + }, + ], + volumes: [{ name: 'varlog', emptyDir: {} }], + }, +}; + +const wrapPodSpec = { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + name: 'wraplogpod', + annotations: { 'console.openshift.io/wrap-log-lines': 'true' }, + labels: { app: 'hello-openshift' }, + }, + spec: { + securityContext: { runAsNonRoot: true, seccompProfile: { type: 'RuntimeDefault' } }, + containers: [ + { + name: 'hello-openshift', + image: + 'quay.io/openshifttest/hello-openshift@sha256:b6296396b632d15daf9b5e62cf26da20d76157161035fefddbd0e7f7749f4167', + ports: [{ containerPort: 80 }], + securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ['ALL'] } }, + }, + ], + }, +}; + +test.describe('Pod log viewer', { tag: ['@admin'] }, () => { + test('verifies default log buffer and full log', async ({ page }) => { + const listPage = new ListPage(page); + const detailsPage = new DetailsPage(page); + const logsPage = new LogsPage(page); + + await test.step('Navigate to kube-apiserver pods', async () => { + await page.goto('/k8s/ns/openshift-kube-apiserver/core~v1~Pod'); + await listPage.waitForRows(); + }); + + await test.step('Open first kube-apiserver pod', async () => { + await listPage.clickFirstRowLinkMatching(/^kube-apiserver-(?!.*guard)/); + await detailsPage.waitForLoaded(); + }); + + await test.step('Verify default log buffer size', async () => { + await detailsPage.selectTab('Logs'); + await expect(logsPage.lineCount).toContainText('1000 lines'); + }); + + await test.step('Verify full log exceeds default buffer', async () => { + await logsPage.clickShowFullLog(); + await expect(logsPage.lineCount).not.toContainText('1000 lines', { timeout: 30_000 }); + }); + }); + + test('supports whitespace retention and log search', async ({ page, k8sClient, cleanup }) => { + const logsPage = new LogsPage(page); + const ns = `test-log-${Date.now()}`; + + await test.step('Create namespace and pod', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createPod(ns, examplePodSpec); + await k8sClient.waitForPodReady('examplepod1', ns); + }); + + await test.step('Navigate to pod logs and select container2', async () => { + await page.goto(`/k8s/ns/${ns}/pods/examplepod1/logs`); + await logsPage.selectContainer('container2'); + }); + + await test.step('Verify whitespace preserved with wrap enabled', async () => { + await logsPage.setWrap(true); + await expect(logsPage.logText.first()).toContainText('Log TEST', { timeout: 60_000 }); + }); + + await test.step('Verify whitespace preserved with wrap disabled', async () => { + await logsPage.setWrap(false); + await expect(logsPage.logText.first()).toContainText('Log TEST'); + }); + + await test.step('Verify log search finds matches', async () => { + await logsPage.searchLogs('test'); + await expect(logsPage.searchMatches.first()).toBeAttached({ timeout: 10_000 }); + expect(await logsPage.searchMatches.count()).toBeGreaterThan(0); + }); + }); + + test('respects wrap-log-lines pod annotation', async ({ page, k8sClient, cleanup }) => { + test.setTimeout(240_000); + const logsPage = new LogsPage(page); + const ns = `test-wrap-${Date.now()}`; + + await test.step('Create namespace and pods', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createPod(ns, examplePodSpec); + await k8sClient.createPod(ns, wrapPodSpec); + await Promise.all([ + k8sClient.waitForPodReady('examplepod1', ns), + k8sClient.waitForPodReady('wraplogpod', ns), + ]); + }); + + await test.step('Verify non-annotated pod has wrap disabled', async () => { + await page.goto(`/k8s/ns/${ns}/pods/examplepod1/logs`); + await logsPage.waitForLoaded(); + await logsPage.setWrap(false); + expect(await logsPage.isWrapChecked()).toBe(false); + }); + + await test.step('Verify annotated pod has wrap enabled by default', async () => { + await page.goto(`/k8s/ns/${ns}/pods/wraplogpod/logs`); + await logsPage.waitForLoaded(); + expect(await logsPage.isWrapChecked()).toBe(true); + }); + + await test.step('Verify annotation re-asserts wrap after navigation', async () => { + await logsPage.setWrap(false); + await page.goto(`/k8s/ns/${ns}/pods/examplepod1/logs`); + await logsPage.waitForLoaded(); + expect(await logsPage.isWrapChecked()).toBe(false); + await page.goto(`/k8s/ns/${ns}/pods/wraplogpod/logs`); + await logsPage.waitForLoaded(); + expect(await logsPage.isWrapChecked()).toBe(true); + }); + }); +}); diff --git a/frontend/e2e/tests/console/app/template.spec.ts b/frontend/e2e/tests/console/app/template.spec.ts new file mode 100644 index 00000000000..d3fa8a4fd4e --- /dev/null +++ b/frontend/e2e/tests/console/app/template.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from '../../../fixtures'; +import { CatalogPage } from '../../../pages/catalog-page'; + +const TEMPLATE_ICON = 'https://example.com/icon/logo.png'; + +function buildTemplateBody(name: string) { + return { + apiVersion: 'template.openshift.io/v1', + kind: 'Template', + metadata: { + name, + namespace: 'openshift', + annotations: { + iconClass: TEMPLATE_ICON, + 'openshift.io/display-name': 'Test Apache HTTP Server', + description: 'An example Apache HTTP Server Test.', + }, + labels: { + 'samples.operator.openshift.io/managed': 'true', + }, + }, + objects: [], + parameters: [], + }; +} + +test.describe('Template feature', { tag: ['@admin'] }, () => { + test('allows custom icon using template annotation', async ({ page, k8sClient, cleanup }) => { + const catalogPage = new CatalogPage(page); + const templateName = `httpd-test-${Date.now()}`; + + await test.step('Create template with custom icon', async () => { + await k8sClient.createCustomResource( + 'template.openshift.io', + 'v1', + 'openshift', + 'templates', + buildTemplateBody(templateName), + ); + cleanup.trackCustomResource( + templateName, + 'openshift', + 'template.openshift.io', + 'v1', + 'templates', + ); + }); + + await test.step('Navigate to catalog and filter for template', async () => { + await catalogPage.navigateToCatalog(); + await catalogPage.filterByKeyword('test apache'); + }); + + await test.step('Verify template displays custom icon', async () => { + const item = catalogPage.catalogItem('Template-Test Apache HTTP Server'); + await expect(item).toBeVisible(); + const icon = catalogPage.catalogItemIcon('Template-Test Apache HTTP Server'); + await expect(icon).toHaveAttribute('src', TEMPLATE_ICON); + }); + }); +}); diff --git a/frontend/packages/integration-tests/fixtures/httpd-example-template.yaml b/frontend/packages/integration-tests/fixtures/httpd-example-template.yaml deleted file mode 100644 index ebd358509a7..00000000000 --- a/frontend/packages/integration-tests/fixtures/httpd-example-template.yaml +++ /dev/null @@ -1,182 +0,0 @@ -apiVersion: template.openshift.io/v1 -kind: Template -labels: - app: httpd-example-test - app.openshift.io/runtime: apache - template: httpd-example-test -message: |- - The following service(s) have been created in your project: ${NAME}. - - For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/httpd-ex/blob/master/README.md. -metadata: - annotations: - description: An example Apache HTTP Server Test. See https://github.com/sclorg/httpd-ex/blob/master/README.md. - iconClass: https://example.com/icon/logo.png - openshift.io/display-name: Test Apache HTTP Server - openshift.io/documentation-url: https://github.com/sclorg/httpd-ex - openshift.io/long-description: Test! This template defines resources needed to develop - a static application served by Apache HTTP Server (httpd), including a build - configuration and application deployment configuration. - openshift.io/provider-display-name: Red Hat, Inc. - openshift.io/support-url: https://access.redhat.com - samples.operator.openshift.io/version: 4.20.0-0-2025-07-11-041818-test-ci-ln-cnh2ypk-latest - template.openshift.io/bindable: "false" - labels: - samples.operator.openshift.io/managed: "true" - name: httpd-example-test - namespace: openshift -objects: -- apiVersion: v1 - kind: Service - metadata: - annotations: - description: Exposes and load balances the application pods - name: ${NAME} - spec: - ports: - - name: web - port: 8080 - targetPort: 8080 - selector: - name: ${NAME} -- apiVersion: route.openshift.io/v1 - kind: Route - metadata: - name: ${NAME} - spec: - host: ${APPLICATION_DOMAIN} - to: - kind: Service - name: ${NAME} -- apiVersion: image.openshift.io/v1 - kind: ImageStream - metadata: - annotations: - description: Keeps track of changes in the application image - name: ${NAME} -- apiVersion: build.openshift.io/v1 - kind: BuildConfig - metadata: - annotations: - description: Defines how to build the application - template.alpha.openshift.io/wait-for-ready: "true" - name: ${NAME} - spec: - output: - to: - kind: ImageStreamTag - name: ${NAME}:latest - source: - contextDir: ${CONTEXT_DIR} - git: - ref: ${SOURCE_REPOSITORY_REF} - uri: ${SOURCE_REPOSITORY_URL} - type: Git - strategy: - sourceStrategy: - from: - kind: ImageStreamTag - name: httpd:${HTTPD_VERSION} - namespace: ${NAMESPACE} - type: Source - triggers: - - type: ImageChange - - type: ConfigChange - - github: - secret: ${GITHUB_WEBHOOK_SECRET} - type: GitHub - - generic: - secret: ${GENERIC_WEBHOOK_SECRET} - type: Generic -- apiVersion: apps/v1 - kind: Deployment - metadata: - annotations: - description: Defines how to deploy the application server - image.openshift.io/triggers: '[{"from":{"kind":"ImageStreamTag","name":"${NAME}:latest"},"fieldPath": - "spec.template.spec.containers[0].image"}]' - template.alpha.openshift.io/wait-for-ready: "true" - name: ${NAME} - spec: - replicas: 1 - selector: - matchLabels: - name: ${NAME} - strategy: - type: RollingUpdate - template: - metadata: - labels: - name: ${NAME} - name: ${NAME} - spec: - containers: - - env: [] - image: ' ' - livenessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 30 - timeoutSeconds: 3 - name: httpd-example - ports: - - containerPort: 8080 - readinessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 3 - timeoutSeconds: 3 - resources: - limits: - memory: ${MEMORY_LIMIT} -parameters: -- description: The name assigned to all of the frontend objects defined in this template. - displayName: Name - name: NAME - required: true - value: httpd-example -- description: The OpenShift Namespace where the ImageStream resides. - displayName: Namespace - name: NAMESPACE - required: true - value: openshift -- description: Version of HTTPD image to be used (2.4-el8 by default). - displayName: HTTPD Version - name: HTTPD_VERSION - required: true - value: 2.4-el8 -- description: Maximum amount of memory the container can use. - displayName: Memory Limit - name: MEMORY_LIMIT - required: true - value: 512Mi -- description: The URL of the repository with your application source code. - displayName: Git Repository URL - name: SOURCE_REPOSITORY_URL - required: true - value: https://github.com/sclorg/httpd-ex.git -- description: Set this to a branch name, tag or other ref of your repository if you - are not using the default branch. - displayName: Git Reference - name: SOURCE_REPOSITORY_REF -- description: Set this to the relative path to your project if it is not in the root - of your repository. - displayName: Context Directory - name: CONTEXT_DIR -- description: The exposed hostname that will route to the httpd service, if left - blank a value will be defaulted. - displayName: Application Hostname - name: APPLICATION_DOMAIN -- description: Github trigger secret. A difficult to guess string encoded as part - of the webhook URL. Not encrypted. - displayName: GitHub Webhook Secret - from: '[a-zA-Z0-9]{40}' - generate: expression - name: GITHUB_WEBHOOK_SECRET -- description: A secret string used to configure the Generic webhook. - displayName: Generic Webhook Secret - from: '[a-zA-Z0-9]{40}' - generate: expression - name: GENERIC_WEBHOOK_SECRET \ No newline at end of file diff --git a/frontend/packages/integration-tests/fixtures/pod-with-space.yaml b/frontend/packages/integration-tests/fixtures/pod-with-space.yaml deleted file mode 100644 index 4fd96aa5bf2..00000000000 --- a/frontend/packages/integration-tests/fixtures/pod-with-space.yaml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: examplepod1 - labels: - app: httpd -spec: - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - name: container1 - image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest' - args: - - /bin/sh - - -c - - > - i=0; - while true; - do - echo "$i:Log TEST $(date)" >> /var/log/1.log; - echo "$(date):Log INFO $i" >> /var/log/2.log; - i=$((i+1)); - sleep 1; - done - volumeMounts: - - name: varlog - mountPath: /var/log - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - - name: container2 - image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest' - args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log'] - volumeMounts: - - name: varlog - mountPath: /var/log - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - volumes: - - name: varlog - emptyDir: {} diff --git a/frontend/packages/integration-tests/fixtures/pod-with-wrap-annotation.yaml b/frontend/packages/integration-tests/fixtures/pod-with-wrap-annotation.yaml deleted file mode 100644 index 72d299b5e87..00000000000 --- a/frontend/packages/integration-tests/fixtures/pod-with-wrap-annotation.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: wraplogpod - annotations: - console.openshift.io/wrap-log-lines: 'true' - labels: - app: hello-openshift -spec: - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - name: hello-openshift - image: quay.io/openshifttest/hello-openshift@sha256:b6296396b632d15daf9b5e62cf26da20d76157161035fefddbd0e7f7749f4167 - ports: - - containerPort: 80 - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL diff --git a/frontend/packages/integration-tests/tests/app/filtering-and-searching.cy.ts b/frontend/packages/integration-tests/tests/app/filtering-and-searching.cy.ts deleted file mode 100644 index c745fc1c325..00000000000 --- a/frontend/packages/integration-tests/tests/app/filtering-and-searching.cy.ts +++ /dev/null @@ -1,137 +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 SEARCH_NAMESPACE = 'openshift-authentication-operator'; -const SEARCH_DEPLOYMENT_NAME = 'authentication-operator'; -const SINGLE_FILTER_GROUP_SELECTOR = - '.co-console-data-view-single-filter .pf-v6-c-toolbar__group.pf-m-filter-group'; - -const verifySingleFilterCategoryHidden = (expectedToggles: number) => { - cy.get('[data-test="data-view-table"]').should('exist'); - cy.get(SINGLE_FILTER_GROUP_SELECTOR) - .find('.pf-v6-c-menu-toggle') - .should('have.length', expectedToggles); - - if (expectedToggles === 1) { - cy.get(SINGLE_FILTER_GROUP_SELECTOR).find('.pf-v6-c-menu-toggle').should('not.be.visible'); - } else { - cy.get(SINGLE_FILTER_GROUP_SELECTOR) - .find('.pf-v6-c-menu-toggle') - .first() - .should('not.be.visible'); - } -}; - -describe('Filtering and Searching', () => { - let WORKLOAD_NAME; - let WORKLOAD_LABEL; - - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - cy.visit(`/k8s/ns/${testName}/deployments`); - listPage.clickCreateYAMLbutton(); - cy.byTestID('yaml-view-input').click(); - - WORKLOAD_NAME = `filter-${testName}`; - WORKLOAD_LABEL = `lbl-filter=${testName}`; - - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep( - {}, - { metadata: { name: WORKLOAD_NAME, labels: { 'lbl-filter': testName } } }, - safeLoad(content), - ); - yamlEditor.setEditorContent(safeDump(newContent)).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - detailsPage.sectionHeaderShouldExist('Deployment details'); - }); - }); - }); - - 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('filters Pod from object detail', () => { - cy.visit(`/k8s/ns/${testName}/deployments`); - listPage.dvRows.shouldExist(WORKLOAD_NAME); - cy.visit(`/k8s/ns/${testName}/deployments/${WORKLOAD_NAME}/pods`); - listPage.dvRows.shouldBeLoaded(); - listPage.dvFilter.byName(WORKLOAD_NAME); - listPage.dvRows.countShouldBe(3); - }); - - it('filters invalid Pod from object detail', () => { - cy.visit(`/k8s/ns/${testName}/deployments/${WORKLOAD_NAME}/pods`); - listPage.dvRows.shouldBeLoaded(); - listPage.dvFilter.byName('XYZ123'); - cy.get('[data-test="data-view-table"]').within(() => { - cy.get('.pf-v6-l-bullseye').should('contain', 'No Pods found'); - }); - }); - - it('filters from Pods list', () => { - cy.visit(`/k8s/all-namespaces/pods`); - listPage.dvRows.shouldBeLoaded(); - listPage.dvFilter.byName(WORKLOAD_NAME); - listPage.dvRows.countShouldBe(3); - }); - - it('displays namespace on Search when All Namespaces is selected', () => { - cy.visit( - `/search/all-namespaces?kind=apps~v1~Deployment&page=1&perPage=50&name=${SEARCH_DEPLOYMENT_NAME}`, - ); - listPage.dvRows.countShouldBe(1); - cy.get(`[data-test-id="${SEARCH_NAMESPACE}"]`).should('exist'); - }); - - it('does not display namespace on Search when namespace is selected', () => { - cy.visit( - `/search/ns/${SEARCH_NAMESPACE}?kind=apps~v1~Deployment&page=1&perPage=50&name=${SEARCH_DEPLOYMENT_NAME}`, - ); - listPage.dvRows.countShouldBe(1); - cy.get(`[data-test-id="${SEARCH_NAMESPACE}"]`).should('not.exist'); - }); - - it('searches for object by kind and label', () => { - cy.visit(`/search/ns/${testName}`, { qs: { kind: 'Deployment', q: WORKLOAD_LABEL } }); - listPage.dvRows.shouldExist(WORKLOAD_NAME); - }); - - it('searches for object by kind, label, and name', () => { - cy.visit(`/search/all-namespaces`, { - qs: { kind: 'Pod', q: 'app=name', name: WORKLOAD_NAME }, - }); - listPage.dvRows.countShouldBe(3); - }); - - it('ConsoleDataView filter toolbar should not display a filter select', () => { - cy.log('when a text filter is the only filter'); - cy.visit('/settings/cluster/alertmanagerconfig?page=1&perPage=50'); - verifySingleFilterCategoryHidden(1); - - cy.log('when a select filter is the only filter'); - cy.visit('/search/all-namespaces?page=1&perPage=50&kind=core~v1~Pod'); - listPage.dvRows.shouldBeLoaded(); - verifySingleFilterCategoryHidden(2); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/masthead.cy.ts b/frontend/packages/integration-tests/tests/app/masthead.cy.ts deleted file mode 100644 index bee0ac0ad0a..00000000000 --- a/frontend/packages/integration-tests/tests/app/masthead.cy.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { checkErrors } from '../../support'; -import { isLocalDevEnvironment } from '../../views/common'; -import { masthead } from '../../views/masthead'; - -describe('Masthead', () => { - before(() => { - // clear any existing sessions - Cypress.session.clearAllSavedSessions(); - cy.login(); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.visit('/'); - }); - - describe('Logo', () => { - it('should be restricted to a max-height of 60px', () => { - cy.byTestID('masthead-logo').should('be.visible'); - cy.byTestID('masthead-logo').should('have.css', 'max-height', '60px'); - cy.byTestID('masthead-logo').invoke('height').should('be.lte', 60); - }); - }); - - describe('Quick create', () => { - it('should open Import YAML', () => { - masthead.quickCreateDropdown().click(); - cy.byTestID('qc-import-yaml').should('be.visible'); - cy.get('[data-test="qc-import-yaml"] a').click({ force: true }); - cy.get('[data-test="page-heading"] h1').should('include.text', 'Import YAML'); - }); - it('should open Import from Git', () => { - masthead.quickCreateDropdown().click(); - cy.byTestID('qc-import-from-git').should('be.visible'); - cy.get('[data-test="qc-import-from-git"] a').click({ force: true }); - cy.get('[data-test="page-heading"] h1').should('include.text', 'Import from Git'); - }); - it('should open Deploy Image', () => { - masthead.quickCreateDropdown().click(); - cy.byTestID('qc-container-images').should('be.visible'); - cy.get('[data-test="qc-container-images"] a').click({ force: true }); - cy.get('[data-test="page-heading"] h1').should('include.text', 'Deploy Image'); - }); - }); - - describe('User dropdown', () => { - it('should render the correct copy login command link', () => { - cy.window().then((win: any) => { - if (win.SERVER_FLAGS?.authDisabled) { - cy.log('Skipping test, auth is disabled'); - } else { - masthead.userDropdown().click(); - masthead.copyLoginCommand().should('be.visible').invoke('removeAttr', 'target'); - masthead.copyLoginCommand().click(); - if (isLocalDevEnvironment) { - cy.origin(Cypress.expose('OAUTH_BASE_ADDRESS'), () => { - // note required duplication in else below due to limitations of cy.origin - cy.url().should('include', '/oauth/token/display'); - cy.get('body').should('include.text', 'Display Token'); - }); - } else { - // note required duplication in if above due to limitations of cy.origin - cy.url().should('include', '/oauth/token/display'); - cy.get('body').should('include.text', 'Display Token'); - } - cy.visit('/'); - } - }); - }); - it('should log the user out', () => { - // Check if auth is disabled (for a local development environment). - cy.window().then((win: any) => { - if (win.SERVER_FLAGS?.authDisabled) { - cy.task('log', ' skipping logout, console is running with auth disabled'); - return; - } - cy.task('log', ' Logging out'); - cy.byTestID('user-dropdown-toggle').click(); - cy.byTestID('log-out').should('be.visible'); - cy.byTestID('log-out').click({ force: true }); - cy.visit('/'); - }); - }); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/node-terminal.cy.ts b/frontend/packages/integration-tests/tests/app/node-terminal.cy.ts deleted file mode 100644 index 6ab2052dd97..00000000000 --- a/frontend/packages/integration-tests/tests/app/node-terminal.cy.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { checkErrors } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; - -describe('Node terminal', () => { - before(() => { - cy.login(); - }); - - beforeEach(() => { - cy.initAdmin(); - }); - - afterEach(() => { - checkErrors(); - }); - - it('Opens a debug terminal', () => { - cy.visit(`/k8s/cluster/nodes`); - listPage.titleShouldHaveText('Nodes'); - listPage.dvRows.shouldBeLoaded(); - listPage.dvRows.clickFirstLinkInFirstRow(); - detailsPage.isLoaded(); - detailsPage.selectTab('Terminal'); - cy.byTestID('node-terminal-error').should('not.exist'); - cy.get('[class="xterm-viewport"]').should('exist'); - // navigate back to Overview tab so the temporary namespace is deleted - cy.get('a[data-test-id="horizontal-link-Overview"]').should('exist').click(); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/overview.cy.ts b/frontend/packages/integration-tests/tests/app/overview.cy.ts deleted file mode 100644 index dabdaebd4f9..00000000000 --- a/frontend/packages/integration-tests/tests/app/overview.cy.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Set as ImmutableSet } from 'immutable'; -import { safeLoad, safeDump } from 'js-yaml'; -import * as _ from 'lodash'; -import { - DeploymentModel, - StatefulSetModel, - DeploymentConfigModel, - DaemonSetModel, -} from '@console/internal/models'; -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { masthead } from '../../views/masthead'; -import { overviewPage } from '../../views/overview'; -import * as yamlEditor from '../../views/yaml-editor'; - -const overviewResources = ImmutableSet([ - DaemonSetModel, - DeploymentModel, - DeploymentConfigModel, - StatefulSetModel, -]); - -describe('Visiting Overview page', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - afterEach(() => { - checkErrors(); - }); - - overviewResources.forEach((kindModel) => { - describe(kindModel.labelPlural, () => { - const resourceName = `${testName}-${kindModel.kind.toLowerCase()}`; - - before(() => { - cy.visit(`k8s/ns/${testName}/${kindModel.plural}/~new`); - masthead.username.shouldBeVisible(); - yamlEditor.isLoaded(); - yamlEditor.getEditorContent().then((content) => { - const newContent = _.defaultsDeep( - {}, - { metadata: { name: resourceName, labels: { automatedTestName: testName } } }, - safeLoad(content), - ); - yamlEditor.setEditorContent(safeDump(newContent)).then(() => { - yamlEditor.clickSaveCreateButton(); - cy.byTestID('yaml-error').should('not.exist'); - detailsPage.sectionHeaderShouldExist(`${kindModel.label} details`); - }); - }); - }); - - it(`displays a ${kindModel.id} in the overview list page`, () => { - cy.visit(`/k8s/cluster/projects/${testName}/workloads?view=list`); - overviewPage.projectOverviewShouldBeVisible(); - overviewPage.itemsShouldBeVisible(); - overviewPage.groupLabelItemContains(kindModel); - overviewPage.projectOverviewListItemContains(resourceName); - }); - - it(`shows ${kindModel.id} details sidebar when item is clicked`, () => { - cy.visit(`/k8s/cluster/projects/${testName}/workloads?view=list`); - overviewPage.detailsSidebarShouldExist(false); - overviewPage.clickProjectOverviewListItem(resourceName); - overviewPage.detailsSidebarShouldExist(true); - overviewPage.detailsSidebarHeadingContains(resourceName); - }); - }); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/resource-log.cy.ts b/frontend/packages/integration-tests/tests/app/resource-log.cy.ts deleted file mode 100644 index 914f5c9e164..00000000000 --- a/frontend/packages/integration-tests/tests/app/resource-log.cy.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { checkErrors, testName } from '../../support'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import { logs } from '../../views/logs'; - -describe('Pod log viewer tab', () => { - const POD_NAME = 'examplepod1'; - const POD_ANNO_NAME = 'wraplogpod'; - const examplepodFilename = 'pod-with-space.yaml'; - const podAnnoFilename = 'pod-with-wrap-annotation.yaml'; - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - }); - - afterEach(() => { - checkErrors(); - }); - - it('Open logs from pod details page tab and verify the log buffer sizes', () => { - cy.visit( - `/k8s/ns/openshift-kube-apiserver/core~v1~Pod?name=kube-apiserver-ip-&status=Running&orderBy=asc&sortBy=Owner`, - ); - listPage.dvRows.clickFirstLinkInFirstRow(); - detailsPage.isLoaded(); - detailsPage.selectTab('Logs'); - detailsPage.isLoaded(); - // Verify the default log buffer size - cy.byTestID('resource-log-no-lines').contains('1000 lines'); - // Verify the log exceeds the default log buffer size - cy.byTestID('resource-log-options-toggle').click(); - cy.byTestDropDownMenu('show-full-log').click(); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(5000); - cy.byTestID('resource-log-no-lines').should('not.contain', '1000 lines'); - }); - - it('Enable white space retain in resource logs', () => { - cy.exec(`oc create -f ./fixtures/${examplepodFilename} -n ${testName}`).then((result) => { - expect(result.stdout).includes('created'); - }); - cy.visit(`/k8s/ns/${testName}/pods/${POD_NAME}/logs`); - cy.byTestID('container-select').click(); - cy.byTestDropDownMenu('container2').click(); - logs.setLogWrap(true); - cy.get('span[class$=c-log-viewer__text]').as('log-text').should('contain', 'Log TEST'); - logs.setLogWrap(false); - cy.get('@log-text').should('contain', 'Log TEST'); - logs.searchLogs('test'); - cy.get('.pf-m-match').its('length').should('be.greaterThan', 0); - }); - - it('Pod annotation could change default behavior for Wrap lines', () => { - cy.exec(`oc create -f ./fixtures/${podAnnoFilename} -n ${testName}`); - cy.visit(`/k8s/ns/${testName}/pods/${POD_NAME}/logs`); - logs.setLogWrap(false); - logs.checkLogWraped(false); - cy.visit(`/k8s/ns/${testName}/pods/${POD_ANNO_NAME}/logs`); - logs.checkLogWraped(true); - logs.setLogWrap(false); - cy.visit(`/k8s/ns/${testName}/pods/${POD_NAME}/logs`); - logs.checkLogWraped(false); - cy.visit(`/k8s/ns/${testName}/pods/${POD_ANNO_NAME}/logs`); - logs.checkLogWraped(true); - cy.pause(); - logs.setLogWrap(false); - }); -}); diff --git a/frontend/packages/integration-tests/tests/app/template.cy.ts b/frontend/packages/integration-tests/tests/app/template.cy.ts deleted file mode 100644 index d58c8ec69fe..00000000000 --- a/frontend/packages/integration-tests/tests/app/template.cy.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { catalog } from '../../views/catalogs'; - -describe('template feature', () => { - before(() => { - cy.log('create template:'); - cy.exec(`oc create -f ./fixtures/httpd-example-template.yaml -n openshift`).then((result) => { - expect(result.stdout).to.include('created'); - }); - cy.login(); - }); - - after(() => { - cy.exec('oc delete template httpd-example-test -n openshift'); - }); - it('Allow custom icon using template annotation', () => { - cy.clickNavLink(['Ecosystem', 'Software Catalog']); - cy.get('.loading-box__loaded', { timeout: 50000 }).should('exist'); - catalog.filterByKeyword('test apach'); - cy.byTestID('Template-Test Apache HTTP Server').click(); - cy.exec( - `oc get template httpd-example-test -n openshift -o jsonpath='{.metadata.annotations.iconClass}'`, - ).then((output) => { - const iconClass = output.stdout; - cy.log(`1. icon url: ${iconClass}`); - catalog.checkItemImage(`${iconClass}`); - }); - }); -}); diff --git a/frontend/packages/integration-tests/views/catalogs.ts b/frontend/packages/integration-tests/views/catalogs.ts deleted file mode 100644 index 4916da22a85..00000000000 --- a/frontend/packages/integration-tests/views/catalogs.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const catalog = { - filterByKeyword: (keyword: string) => { - cy.get(`input[placeholder*="Filter by keyword"]`).clear().type(`${keyword}`); - }, - checkItemImage: (srcText) => { - cy.get('img.catalog-item-header-pf-icon') - .should('have.attr', 'src') - .and('contain', `${srcText}`); - }, -}; diff --git a/frontend/packages/integration-tests/views/logs.ts b/frontend/packages/integration-tests/views/logs.ts deleted file mode 100644 index f98dc965c11..00000000000 --- a/frontend/packages/integration-tests/views/logs.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const logs = { - toggleOptions: () => { - // click configuration dropdown - cy.byTestID('resource-log-options-toggle').click(); - }, - checkLogWraped: (flag: boolean) => { - // open options - logs.toggleOptions(); - if (flag) { - cy.byTestDropDownMenu('wrap-lines').within(() => { - cy.get('input').should('be.checked'); - }); - } else { - cy.byTestDropDownMenu('wrap-lines').within(() => { - cy.get('input').should('not.be.checked'); - }); - } - // close options - logs.toggleOptions(); - }, - setLogWrap: (flag: boolean) => { - logs.toggleOptions(); - if (flag) { - cy.byTestDropDownMenu('wrap-lines').within(() => { - cy.get('input').check(); - }); - } else { - cy.byTestDropDownMenu('wrap-lines').within(() => { - cy.get('input').uncheck(); - }); - } - logs.toggleOptions(); - }, - searchLogs: (text: string) => { - cy.get('input[placeholder="Search logs"]').clear().type(text); - }, -}; diff --git a/frontend/packages/integration-tests/views/overview.ts b/frontend/packages/integration-tests/views/overview.ts deleted file mode 100644 index aac8a6518b3..00000000000 --- a/frontend/packages/integration-tests/views/overview.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const overviewPage = { - projectOverviewShouldBeVisible: () => { - cy.get(`.odc-topology-list-view`).should('be.visible'); - }, - itemsShouldBeVisible: () => { - cy.get(`.odc-topology-list-view__item-row`).should('be.visible'); - }, - groupLabelItemContains: (kind) => { - cy.get('.odc-topology-list-view__kind-label').contains(kind.label); - }, - projectOverviewListItemContains: (name) => { - cy.get('.odc-topology-list-view__label-cell').contains(name); - }, - clickProjectOverviewListItem: (name) => { - cy.get('.odc-topology-list-view__label-cell').contains(name).click(); - }, - detailsSidebarShouldExist: (exist = true) => { - cy.get('.resource-overview').should(exist ? 'exist' : 'not.exist'); - }, - detailsSidebarHeadingContains: (name) => { - cy.get('.resource-overview__heading h1').contains(name); - }, -};