diff --git a/frontend/e2e/clients/kubernetes-client.ts b/frontend/e2e/clients/kubernetes-client.ts index 789f87b29d3..33d7ffdb06a 100644 --- a/frontend/e2e/clients/kubernetes-client.ts +++ b/frontend/e2e/clients/kubernetes-client.ts @@ -375,10 +375,9 @@ export default class KubernetesClient { const existing = await this.k8sApi.readNamespacedConfigMap({ name, namespace }); const existingData = (existing as any)?.data || {}; const mergedData = { ...existingData, ...patchData }; - await this.mergePatchResource( - `/api/v1/namespaces/${namespace}/configmaps/${name}`, - { data: mergedData }, - ); + await this.mergePatchResource(`/api/v1/namespaces/${namespace}/configmaps/${name}`, { + data: mergedData, + }); } async createConfigMap( @@ -455,10 +454,9 @@ export default class KubernetesClient { namespace: string, annotations: Record, ): Promise { - await this.mergePatchResource( - `/api/v1/namespaces/${namespace}/configmaps/${name}`, - { metadata: { annotations } }, - ); + await this.mergePatchResource(`/api/v1/namespaces/${namespace}/configmaps/${name}`, { + metadata: { annotations }, + }); } async labelConfigMap( @@ -466,10 +464,9 @@ export default class KubernetesClient { namespace: string, labels: Record, ): Promise { - await this.mergePatchResource( - `/api/v1/namespaces/${namespace}/configmaps/${name}`, - { metadata: { labels } }, - ); + await this.mergePatchResource(`/api/v1/namespaces/${namespace}/configmaps/${name}`, { + metadata: { labels }, + }); } async deleteConfigMap(name: string, namespace: string): Promise { @@ -640,11 +637,7 @@ export default class KubernetesClient { } } - async patchDeployment( - name: string, - namespace: string, - patch: object, - ): Promise { + async patchDeployment(name: string, namespace: string, patch: object): Promise { return this.appsApi.patchNamespacedDeployment({ name, namespace, @@ -687,7 +680,6 @@ export default class KubernetesClient { }); } - async waitForDeploymentReady( name: string, namespace: string, @@ -702,9 +694,7 @@ export default class KubernetesClient { return ( status?.availableReplicas === desired && status?.updatedReplicas === desired && - (status?.conditions ?? []).some( - (c) => c.type === 'Available' && c.status === 'True', - ) + (status?.conditions ?? []).some((c) => c.type === 'Available' && c.status === 'True') ); } catch { return false; @@ -748,9 +738,11 @@ export default class KubernetesClient { const state = cs.state?.waiting ? `Waiting: ${cs.state.waiting.reason} - ${cs.state.waiting.message ?? ''}` : cs.state?.terminated - ? `Terminated: ${cs.state.terminated.reason}` - : 'Running'; - lines.push(` container ${cs.name}: ready=${cs.ready}, restarts=${cs.restartCount}, ${state}`); + ? `Terminated: ${cs.state.terminated.reason}` + : 'Running'; + lines.push( + ` container ${cs.name}: ready=${cs.ready}, restarts=${cs.restartCount}, ${state}`, + ); } try { const events = await this.k8sApi.listNamespacedEvent({ @@ -760,8 +752,7 @@ export default class KubernetesClient { const recent = events.items .sort( (a, b) => - new Date(b.lastTimestamp ?? 0).getTime() - - new Date(a.lastTimestamp ?? 0).getTime(), + new Date(b.lastTimestamp ?? 0).getTime() - new Date(a.lastTimestamp ?? 0).getTime(), ) .slice(0, 10); for (const ev of recent) { diff --git a/frontend/e2e/fixtures/index.ts b/frontend/e2e/fixtures/index.ts index 0ae703794ab..27acba18697 100644 --- a/frontend/e2e/fixtures/index.ts +++ b/frontend/e2e/fixtures/index.ts @@ -4,10 +4,16 @@ import * as path from 'path'; import { test as base, expect } from '@playwright/test'; import KubernetesClient from '../clients/kubernetes-client'; +import { loginFromEnv } from '../setup/login-helper'; import type { CleanupFixture } from './cleanup-fixture'; import { createCleanupFixture } from './cleanup-fixture'; +// URLs the console redirects to when a shared storageState session expires or is +// invalidated (e.g. by a console rollout in another spec). Matches the OAuth +// server and the console's own login route. +const OAUTH_REDIRECT_RE = /\/oauth\/|oauth-openshift|\/auth\/login\b/; + export interface SharedTestConfig { testNamespace: string; authToken?: string; @@ -24,6 +30,69 @@ type WorkerFixtures = { }; export const test = base.extend({ + // Override the built-in `page` fixture to self-heal lost sessions. When any + // navigation is bounced to the OAuth login page — during warmup or mid-test — + // re-authenticate the current persona and retry the original target so the + // caller transparently lands on the page it asked for. loginFromEnv returns + // quickly when the OAuth SSO cookie is still valid (the flow auto-completes) + // and resubmits credentials when it isn't. Persona is derived from the project + // name, matching the storageState mapping in playwright.config.ts. + // + // Tests that assert on session/auth behavior directly (e.g. session + // persistence across pod restarts) must opt out with a + // `{ type: 'no-auto-reauth' }` annotation, otherwise transparent recovery + // would mask the very failure they check for. + page: async ({ page }, use, testInfo) => { + if (testInfo.annotations.some((a) => a.type === 'no-auto-reauth')) { + await use(page); + return; + } + const persona = testInfo.project.name.endsWith('-developer') ? 'developer' : 'admin'; + const originalGoto = page.goto.bind(page); + let recovering = false; + + const recoverIfRedirectedToLogin = async (): Promise => { + // Guard against re-entrancy: loginFromEnv navigates internally, and those + // navigations flow back through this override. + if (recovering || !OAUTH_REDIRECT_RE.test(page.url())) { + return false; + } + recovering = true; + try { + await loginFromEnv(page, persona); + } finally { + recovering = false; + } + return true; + }; + + page.goto = async (url, options) => { + const response = await originalGoto(url, options); + // The console redirects to the OAuth login page client-side, a beat after + // the initial document loads, so `page.url()` can still read the target + // right after goto resolves. Wait for auth to settle before deciding: the + // console boots with a `co-auth-pending` class on and removes it + // once its authenticated bootstrap fetch succeeds (see public/components/ + // app.tsx); a 401 instead redirects to OAuth. Race that class dropping + // against the OAuth redirect so we neither miss the redirect nor stall the + // happy path. + if (!recovering) { + // eslint-disable-next-line no-restricted-syntax -- waiting for state, no action follows + const authSettled = page + .locator('html:not(.co-auth-pending)') + .waitFor({ state: 'attached', timeout: 30_000 }); + const redirectedToLogin = page.waitForURL(OAUTH_REDIRECT_RE, { timeout: 30_000 }); + await Promise.race([authSettled.catch(() => {}), redirectedToLogin.catch(() => {})]); + } + if (await recoverIfRedirectedToLogin()) { + return originalGoto(url, options); + } + return response; + }; + + await use(page); + }, + testConfig: [ async ({}, use) => { const configPath = path.resolve(import.meta.dirname, '..', '.test-config.json'); diff --git a/frontend/e2e/mocks/operator-lifecycle.ts b/frontend/e2e/mocks/operator-lifecycle.ts index e24696e64fd..b45dc1127df 100644 --- a/frontend/e2e/mocks/operator-lifecycle.ts +++ b/frontend/e2e/mocks/operator-lifecycle.ts @@ -20,8 +20,16 @@ const activePhases = (): { name: string; startDate: string; endDate: string }[] extendedStart.setDate(extendedStart.getDate() + 1); const extendedEnd = new Date(now.getFullYear() + 3, 11, 31); return [ - { name: 'Maintenance support', startDate: toDateStr(maintenanceStart), endDate: toDateStr(maintenanceEnd) }, - { name: 'Extended life cycle support', startDate: toDateStr(extendedStart), endDate: toDateStr(extendedEnd) }, + { + name: 'Maintenance support', + startDate: toDateStr(maintenanceStart), + endDate: toDateStr(maintenanceEnd), + }, + { + name: 'Extended life cycle support', + startDate: toDateStr(extendedStart), + endDate: toDateStr(extendedEnd), + }, ]; }; @@ -33,8 +41,16 @@ const expiredPhases = (): { name: string; startDate: string; endDate: string }[] extendedStart.setDate(extendedStart.getDate() + 1); const extendedEnd = new Date(now.getFullYear() - 1, 11, 31); return [ - { name: 'Maintenance support', startDate: toDateStr(maintenanceStart), endDate: toDateStr(maintenanceEnd) }, - { name: 'Extended life cycle support', startDate: toDateStr(extendedStart), endDate: toDateStr(extendedEnd) }, + { + name: 'Maintenance support', + startDate: toDateStr(maintenanceStart), + endDate: toDateStr(maintenanceEnd), + }, + { + name: 'Extended life cycle support', + startDate: toDateStr(extendedStart), + endDate: toDateStr(extendedEnd), + }, ]; }; @@ -70,10 +86,7 @@ export const makeLifecycleSelfSupport = ( ], }); -export const makeLifecycleIncompatible = ( - packageName: string, - version: string, -): LifecycleData => ({ +export const makeLifecycleIncompatible = (packageName: string, version: string): LifecycleData => ({ package: packageName, schema: LIFECYCLE_SCHEMA, versions: [ diff --git a/frontend/e2e/mocks/storage.ts b/frontend/e2e/mocks/storage.ts index 65da86f423b..a369d65fb59 100644 --- a/frontend/e2e/mocks/storage.ts +++ b/frontend/e2e/mocks/storage.ts @@ -105,8 +105,18 @@ export const provisionersMap: Record = { { name: 'Availability zone', id: 'availability', values: 'lalitpur' }, ], 'kubernetes.io/azure-file': [ - { name: 'SKU name', id: 'skuName', hintText: 'Azure storage account SKU tier', values: 'sample-name' }, - { name: 'Location', id: 'location', hintText: 'Azure storage account name', values: 'bhaktapur' }, + { + name: 'SKU name', + id: 'skuName', + hintText: 'Azure storage account SKU tier', + values: 'sample-name', + }, + { + name: 'Location', + id: 'location', + hintText: 'Azure storage account name', + values: 'bhaktapur', + }, { name: 'Azure storage account name', id: 'storageAccount', @@ -115,7 +125,12 @@ export const provisionersMap: Record = { }, ], 'kubernetes.io/azure-disk': [ - { name: 'Storage account type', id: 'storageaccounttype', hintText: 'Storage account type', values: 'tester' }, + { + name: 'Storage account type', + id: 'storageaccounttype', + hintText: 'Storage account type', + values: 'tester', + }, { name: 'Account kind', id: 'kind', values: ['shared', 'dedicated', 'managed'] }, ], 'kubernetes.io/quobyte': [ @@ -128,7 +143,11 @@ export const provisionersMap: Record = { { name: 'Quobyte tenant', id: 'quobyteTenant', values: 'tester' }, ], 'kubernetes.io/vsphere-volume': [ - { name: 'Disk format', id: 'diskformat', values: ['thin', 'zeroed thick', 'eager zeroed thick'] }, + { + name: 'Disk format', + id: 'diskformat', + values: ['thin', 'zeroed thick', 'eager zeroed thick'], + }, { name: 'Datastore', id: 'datastore', values: 'store-thin' }, ], 'kubernetes.io/portworx-volume': [ diff --git a/frontend/e2e/pages/alertmanager-page.ts b/frontend/e2e/pages/alertmanager-page.ts index de3c55d289c..ad2f4b158d4 100644 --- a/frontend/e2e/pages/alertmanager-page.ts +++ b/frontend/e2e/pages/alertmanager-page.ts @@ -105,7 +105,9 @@ export function getGlobalsAndReceiverConfig( } { const parsed = yaml.load(yamlContent); const config: AlertmanagerConfig = - typeof parsed === 'object' && parsed !== null ? (parsed as AlertmanagerConfig) : ({} as AlertmanagerConfig); + typeof parsed === 'object' && parsed !== null + ? (parsed as AlertmanagerConfig) + : ({} as AlertmanagerConfig); const receiver: AlertmanagerReceiver | undefined = config.receivers?.find( (r) => r.name === receiverName, ); diff --git a/frontend/e2e/pages/base-page.ts b/frontend/e2e/pages/base-page.ts index 83467a30f24..7412b360e26 100644 --- a/frontend/e2e/pages/base-page.ts +++ b/frontend/e2e/pages/base-page.ts @@ -21,12 +21,25 @@ export async function setEditorContent(page: Page, content: string): Promise (window as any).monaco?.editor?.getModels()?.[0], { timeout: 10_000, }); - await page.evaluate((text) => { - (window as any).monaco.editor.getModels()[0].setValue(text); - }, content); + // Monaco can swap its model during initialisation, silently dropping an early + // setValue and leaving the editor empty — which then submits an empty + // definition. Set and verify with retries so the content is guaranteed to + // stick before the caller proceeds. + await expect(async () => { + await page.evaluate((text) => { + (window as any).monaco.editor.getModels()[0].setValue(text); + }, content); + const value = await page.evaluate(() => + (window as any).monaco.editor.getModels()[0].getValue(), + ); + expect(value.trim()).toBe(content.trim()); + }).toPass({ timeout: 15_000, intervals: [300, 700, 1500] }); } export async function warmupSPA(page: Page): Promise { + // Session recovery on OAuth redirect is handled by the guarded `page` fixture + // (e2e/fixtures/index.ts), which re-authenticates on any navigation — during + // warmup or mid-test — that gets bounced to the login page. await expect(async () => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60_000 }); await expect(page.locator('#page-sidebar')).toBeVisible({ timeout: 30_000 }); @@ -51,8 +64,7 @@ export async function ensureDeveloperPerspective( ): Promise { const toggle = page.getByTestId('perspective-switcher-toggle'); await expect(toggle).toBeVisible(); - const isSinglePerspective = - (await toggle.getAttribute('id')) === 'only-one-perspective'; + const isSinglePerspective = (await toggle.getAttribute('id')) === 'only-one-perspective'; if (isSinglePerspective) { await k8sClient.customObjectsApi.patchClusterCustomObject({ group: 'operator.openshift.io', @@ -187,10 +199,9 @@ export default abstract class BasePage { } async waitForEditorReady(): Promise { - await this.page.waitForFunction( - () => !!(window as any).monaco?.editor?.getModels()?.[0], - { timeout: 30_000 }, - ); + await this.page.waitForFunction(() => !!(window as any).monaco?.editor?.getModels()?.[0], { + timeout: 30_000, + }); } async getEditorContent(): Promise { diff --git a/frontend/e2e/pages/cluster-dashboard-page.ts b/frontend/e2e/pages/cluster-dashboard-page.ts index 0bde71f2434..eb2249a0882 100644 --- a/frontend/e2e/pages/cluster-dashboard-page.ts +++ b/frontend/e2e/pages/cluster-dashboard-page.ts @@ -16,12 +16,8 @@ export class ClusterDashboardPage extends BasePage { private readonly utilizationItem = this.page.getByTestId('utilization-item'); private readonly utilizationItemTitle = this.page.getByTestId('utilization-item-title'); private readonly durationSelect = this.page.getByTestId('duration-select'); - private readonly insightsHealthItem = this.page.locator( - '[data-item-id="Insights-health-item"]', - ); - private readonly insightsButton = this.page - .getByTestId('Insights') - .locator('button'); + private readonly insightsHealthItem = this.page.locator('[data-item-id="Insights-health-item"]'); + private readonly insightsButton = this.page.getByTestId('Insights').locator('button'); private readonly popover = this.page.locator('.pf-v6-c-popover'); async navigateToDashboard(): Promise { @@ -32,9 +28,12 @@ export class ClusterDashboardPage extends BasePage { async waitForStatusCardLoaded(): Promise { await expect(this.statusCard).toBeVisible({ timeout: 30_000 }); // eslint-disable-next-line no-restricted-syntax - await this.statusCard.locator('.skeleton-health').waitFor({ state: 'hidden', timeout: 30_000 }).catch(() => { - // Skeletons may have already disappeared - }); + await this.statusCard + .locator('.skeleton-health') + .waitFor({ state: 'hidden', timeout: 30_000 }) + .catch(() => { + // Skeletons may have already disappeared + }); } getDetailsCard(): Locator { @@ -107,10 +106,23 @@ export class ClusterDashboardPage extends BasePage { const timeout = 30_000; /* eslint-disable no-restricted-syntax */ const result = await Promise.race([ - popover.getByText('Temporarily unavailable.').waitFor({ state: 'visible', timeout }).then(() => 'no-data' as const), - popover.getByText('Waiting for results.').waitFor({ state: 'visible', timeout }).then(() => 'no-data' as const), - popover.getByText('Disabled.').waitFor({ state: 'visible', timeout }).then(() => 'no-data' as const), - popover.locator('a[href*="console.redhat.com/openshift/insights/advisor"]').first().waitFor({ state: 'visible', timeout }).then(() => 'data' as const), + popover + .getByText('Temporarily unavailable.') + .waitFor({ state: 'visible', timeout }) + .then(() => 'no-data' as const), + popover + .getByText('Waiting for results.') + .waitFor({ state: 'visible', timeout }) + .then(() => 'no-data' as const), + popover + .getByText('Disabled.') + .waitFor({ state: 'visible', timeout }) + .then(() => 'no-data' as const), + popover + .locator('a[href*="console.redhat.com/openshift/insights/advisor"]') + .first() + .waitFor({ state: 'visible', timeout }) + .then(() => 'data' as const), ]).catch(() => 'no-data' as const); /* eslint-enable no-restricted-syntax */ return result === 'data'; diff --git a/frontend/e2e/pages/console-plugin-page.ts b/frontend/e2e/pages/console-plugin-page.ts index 2fb7c9c15ba..926475dbabf 100644 --- a/frontend/e2e/pages/console-plugin-page.ts +++ b/frontend/e2e/pages/console-plugin-page.ts @@ -7,15 +7,11 @@ export class ConsolePluginPage extends BasePage { private readonly pfCodeEditor = this.page.locator('.pf-v6-c-code-editor'); async navigateToConsolePlugins(): Promise { - await this.goTo( - '/k8s/cluster/operator.openshift.io~v1~Console/cluster/console-plugins', - ); + await this.goTo('/k8s/cluster/operator.openshift.io~v1~Console/cluster/console-plugins'); } async navigateToPluginDetails(pluginName: string): Promise { - await this.goTo( - `/k8s/cluster/console.openshift.io~v1~ConsolePlugin/${pluginName}`, - ); + await this.goTo(`/k8s/cluster/console.openshift.io~v1~ConsolePlugin/${pluginName}`); } async navigateToPluginManifest(pluginName: string): Promise { diff --git a/frontend/e2e/pages/details-page.ts b/frontend/e2e/pages/details-page.ts index 112b6d14398..4c1c75d1367 100644 --- a/frontend/e2e/pages/details-page.ts +++ b/frontend/e2e/pages/details-page.ts @@ -87,8 +87,6 @@ export class DetailsPage extends BasePage { } async confirmDelete(): Promise { - await this.robustClick( - this.page.getByRole('button', { name: 'Delete', exact: true }), - ); + await this.robustClick(this.page.getByRole('button', { name: 'Delete', exact: true })); } } diff --git a/frontend/e2e/pages/dev-console/add-page.ts b/frontend/e2e/pages/dev-console/add-page.ts index 0e47d358487..a4c82a2b8d8 100644 --- a/frontend/e2e/pages/dev-console/add-page.ts +++ b/frontend/e2e/pages/dev-console/add-page.ts @@ -146,9 +146,7 @@ export class AddPage extends BasePage { } getPinnedResource(name: string): Locator { - return this.page - .getByRole('region', { name: 'Pinned resources' }) - .getByRole('link', { name }); + return this.page.getByRole('region', { name: 'Pinned resources' }).getByRole('link', { name }); } } export class ImportFromGitPage extends BasePage { @@ -157,8 +155,14 @@ export class ImportFromGitPage extends BasePage { }); private readonly appNameInput: Locator = this.page.getByTestId('application-form-app-input'); private readonly nameInput: Locator = this.page.getByTestId('application-form-app-name'); - private readonly createButton: Locator = this.page.getByRole('button', { name: 'Create', exact: true }); - private readonly cancelButton: Locator = this.page.getByRole('button', { name: 'Cancel', exact: true }); + private readonly createButton: Locator = this.page.getByRole('button', { + name: 'Create', + exact: true, + }); + private readonly cancelButton: Locator = this.page.getByRole('button', { + name: 'Cancel', + exact: true, + }); async navigateToImportFromGit(namespace: string): Promise { await this.goTo(`/import/ns/${namespace}`); @@ -294,12 +298,20 @@ export class DeployImagePage extends BasePage { private readonly imageNameInput: Locator = this.page.getByRole('textbox', { name: 'Image name' }); private readonly nameInput: Locator = this.page.getByTestId('application-form-app-name'); private readonly appNameInput: Locator = this.page.getByTestId('application-form-app-input'); - private readonly createButton: Locator = this.page.getByRole('button', { name: 'Create', exact: true }); - private readonly cancelButton: Locator = this.page.getByRole('button', { name: 'Cancel', exact: true }); + private readonly createButton: Locator = this.page.getByRole('button', { + name: 'Create', + exact: true, + }); + private readonly cancelButton: Locator = this.page.getByRole('button', { + name: 'Cancel', + exact: true, + }); async navigateToDeployImage(namespace: string): Promise { await this.goTo(`/deploy-image/ns/${namespace}`); - await expect(this.imageNameInput.or(this.page.getByTestId('internal-view-input')).first()).toBeVisible({ timeout: 60_000 }); + await expect( + this.imageNameInput.or(this.page.getByTestId('internal-view-input')).first(), + ).toBeVisible({ timeout: 60_000 }); } async enterExternalRegistryImage(imageName: string): Promise { @@ -341,7 +353,9 @@ export class DeployImagePage extends BasePage { } async selectRuntimeIcon(iconName: string): Promise { - const iconToggle = this.page.locator('.odc-icon-dropdown').getByTestId('console-select-menu-toggle'); + const iconToggle = this.page + .locator('.odc-icon-dropdown') + .getByTestId('console-select-menu-toggle'); await this.robustClick(iconToggle, { timeout: 30_000 }); const option = this.page.getByRole('option', { name: iconName }); await this.robustClick(option); @@ -391,8 +405,14 @@ export class DeployImagePage extends BasePage { } export class ImportYAMLPage extends BasePage { - private readonly submitButton: Locator = this.page.getByRole('button', { name: 'Create', exact: true }); - private readonly cancelButton: Locator = this.page.getByRole('button', { name: 'Cancel', exact: true }); + private readonly submitButton: Locator = this.page.getByRole('button', { + name: 'Create', + exact: true, + }); + private readonly cancelButton: Locator = this.page.getByRole('button', { + name: 'Cancel', + exact: true, + }); getSubmitButton(): Locator { return this.submitButton; @@ -410,4 +430,3 @@ export class ImportYAMLPage extends BasePage { await this.robustClick(this.cancelButton); } } - diff --git a/frontend/e2e/pages/dev-console/vulnerability-page.ts b/frontend/e2e/pages/dev-console/vulnerability-page.ts index abce7ce9328..df9bc0b83a3 100644 --- a/frontend/e2e/pages/dev-console/vulnerability-page.ts +++ b/frontend/e2e/pages/dev-console/vulnerability-page.ts @@ -3,9 +3,7 @@ import type { Locator } from '@playwright/test'; import BasePage from '../base-page'; export class VulnerabilityPage extends BasePage { - private readonly vulnerabilitiesTab = this.page.getByTestId( - 'horizontal-link-Vulnerabilities', - ); + private readonly vulnerabilitiesTab = this.page.getByTestId('horizontal-link-Vulnerabilities'); private readonly detailsTab = this.page.getByTestId('horizontal-link-Details'); private readonly yamlTab = this.page.getByTestId('horizontal-link-YAML'); private readonly affectedPodsTab = this.page.getByTestId('horizontal-link-Affected Pods'); diff --git a/frontend/e2e/pages/knative/add-flow-page.ts b/frontend/e2e/pages/knative/add-flow-page.ts index f76b09f3f18..a6238521158 100644 --- a/frontend/e2e/pages/knative/add-flow-page.ts +++ b/frontend/e2e/pages/knative/add-flow-page.ts @@ -8,20 +8,14 @@ export class AddFlowPage extends BasePage { private readonly componentNameInput = this.page.locator('#form-input-name-field'); private readonly appNameInput = this.page.locator('#form-input-application-name-field'); private readonly createButton = this.page.getByTestId('save-changes'); - private readonly resourcesDropdown = this.page.locator( - '#form-select-input-resources-field', - ); - private readonly knativeResourceOption = this.page.locator( - '#select-option-resources-knative', - ); + private readonly resourcesDropdown = this.page.locator('#form-select-input-resources-field'); + private readonly knativeResourceOption = this.page.locator('#select-option-resources-knative'); private readonly importStrategyEditButton = this.page.getByTestId('import-strategy-button'); private readonly dockerfileStrategy = this.page.getByTestId('import-strategy-Dockerfile'); private readonly dockerfilePathInput = this.page.locator( '#form-input-docker-dockerfilePath-field', ); - private readonly externalRegistryInput = this.page.locator( - '#form-input-searchTerm-field', - ); + private readonly externalRegistryInput = this.page.locator('#form-input-searchTerm-field'); private readonly pageHeading = this.page.getByTestId('page-heading').locator('h1'); async navigateToAddPage(namespace: string): Promise { @@ -48,7 +42,10 @@ export class AddFlowPage extends BasePage { await this.gitUrlInput.clear(); await this.gitUrlInput.fill(url); await expect( - this.page.locator('.pf-v6-c-helper-text').filter({ hasText: /Validated|Rate limit/ }).first(), + this.page + .locator('.pf-v6-c-helper-text') + .filter({ hasText: /Validated|Rate limit/ }) + .first(), ).toBeVisible({ timeout: 60_000 }); // If rate limited, auto-detection fails — manually select Builder Image strategy and Node.js @@ -99,7 +96,10 @@ export class AddFlowPage extends BasePage { await this.externalRegistryInput.clear(); await this.externalRegistryInput.fill(imageName); await expect( - this.page.locator('.pf-v6-c-helper-text').filter({ hasText: /Validated|Loading/ }).first(), + this.page + .locator('.pf-v6-c-helper-text') + .filter({ hasText: /Validated|Loading/ }) + .first(), ).toBeVisible({ timeout: 60_000 }); await expect(this.componentNameInput).not.toHaveValue('', { timeout: 30_000 }); } diff --git a/frontend/e2e/pages/knative/admin-eventing-page.ts b/frontend/e2e/pages/knative/admin-eventing-page.ts index 040a673eafa..6fb17c36451 100644 --- a/frontend/e2e/pages/knative/admin-eventing-page.ts +++ b/frontend/e2e/pages/knative/admin-eventing-page.ts @@ -73,9 +73,11 @@ export class AdminEventingPage extends BasePage { async createBroker(name: string): Promise { await this.page.locator('#form-radiobutton-editorType-form-field').click(); - const nameField = this.page.locator( - '[data-test="application-form-app-name"], [data-test-id="application-form-app-name"]', - ).first(); + const nameField = this.page + .locator( + '[data-test="application-form-app-name"], [data-test-id="application-form-app-name"]', + ) + .first(); await nameField.clear(); await nameField.fill(name); await this.robustClick(this.page.getByTestId('save-changes')); diff --git a/frontend/e2e/pages/knative/topology-knative-page.ts b/frontend/e2e/pages/knative/topology-knative-page.ts index 81e70db232e..eba1ab4a9eb 100644 --- a/frontend/e2e/pages/knative/topology-knative-page.ts +++ b/frontend/e2e/pages/knative/topology-knative-page.ts @@ -11,7 +11,9 @@ export class TopologyKnativePage extends BasePage { private readonly highlightedNode = this.page.locator('.is-filtered').first(); private readonly knativeServiceNode = this.page.locator('[data-type="knative-service"]'); private readonly sidePane = this.page.getByTestId('topology-sidepane'); - private readonly sidePaneClose = this.page.getByTestId('topology-sidepane').locator('button[aria-label="Close"]'); + private readonly sidePaneClose = this.page + .getByTestId('topology-sidepane') + .locator('button[aria-label="Close"]'); private readonly editAnnotationsLink = this.page.getByTestId('edit-annotations'); private readonly modalTitle = this.page.getByTestId('modal-title'); private readonly modalCancel = this.page.getByTestId('modal-cancel-action'); @@ -79,9 +81,7 @@ export class TopologyKnativePage extends BasePage { } async selectContextMenuAction(action: string): Promise { - const menuItem = this.page.locator( - `[data-test="${action}"], [data-test-action="${action}"]`, - ); + const menuItem = this.page.locator(`[data-test="${action}"], [data-test-action="${action}"]`); await this.robustClick(menuItem.first(), { timeout: 10_000 }); } @@ -193,7 +193,9 @@ export class TopologyKnativePage extends BasePage { } async verifyKnativeRevisionVisible(timeout = 60_000): Promise { - await expect(this.page.locator('[data-type="knative-revision"]').first()).toBeAttached({ timeout }); + await expect(this.page.locator('[data-type="knative-revision"]').first()).toBeAttached({ + timeout, + }); } async getRevisionCount(): Promise { @@ -201,9 +203,9 @@ export class TopologyKnativePage extends BasePage { } async verifyRevisionCount(expected: number, timeout = 60_000): Promise { - await expect( - this.page.getByTestId('revision-list').locator('li'), - ).toHaveCount(expected, { timeout }); + await expect(this.page.getByTestId('revision-list').locator('li')).toHaveCount(expected, { + timeout, + }); } async openServiceAction( @@ -213,13 +215,13 @@ export class TopologyKnativePage extends BasePage { kind = 'serving.knative.dev~v1~Service', ): Promise { await this.goTo(`/k8s/ns/${namespace}/${kind}/${resourceName}`); - const actionsButton = this.page.locator( - '[data-test="actions-menu-button"], [data-test-id="actions-menu-button"]', - ).first(); + const actionsButton = this.page + .locator('[data-test="actions-menu-button"], [data-test-id="actions-menu-button"]') + .first(); await this.robustClick(actionsButton, { timeout: 30_000 }); - const actionItem = this.page.locator( - `[data-test="${action}"], [data-test-action="${action}"]`, - ).first(); + const actionItem = this.page + .locator(`[data-test="${action}"], [data-test-action="${action}"]`) + .first(); await this.robustClick(actionItem, { timeout: 10_000 }); } @@ -238,9 +240,12 @@ export class TopologyKnativePage extends BasePage { const appInput = this.page.getByTestId('application-form-app-input'); if ((await appDropdown.count()) > 0) { await appDropdown.click(); - await this.page.locator( - '[data-test="#CREATE_APPLICATION_KEY#"], [data-test-dropdown-menu="#CREATE_APPLICATION_KEY#"]', - ).first().click(); + await this.page + .locator( + '[data-test="#CREATE_APPLICATION_KEY#"], [data-test-dropdown-menu="#CREATE_APPLICATION_KEY#"]', + ) + .first() + .click(); } await appInput.clear(); await appInput.fill(appName); diff --git a/frontend/e2e/pages/list-page.ts b/frontend/e2e/pages/list-page.ts index 5a101645f45..3bf7dad9023 100644 --- a/frontend/e2e/pages/list-page.ts +++ b/frontend/e2e/pages/list-page.ts @@ -120,9 +120,7 @@ export class ListPage extends BasePage { } async clickResourceRowKebabAction(resourceName: string, actionName: string): Promise { - const row = this.resourceRows - .filter({ hasText: resourceName }) - .first(); + const row = this.resourceRows.filter({ hasText: resourceName }).first(); const kebab = row.getByTestId('kebab-button'); await this.robustClick(kebab); await this.robustClick(this.page.getByTestId(actionName)); @@ -136,9 +134,7 @@ export class ListPage extends BasePage { if (await this.dataViewFilters.isVisible()) { await this.page.locator('.pf-v6-c-menu__list-item', { hasText: filterName }).click(); - const checkboxFilter = this.page.locator( - '[data-ouia-component-id="DataViewCheckboxFilter"]', - ); + const checkboxFilter = this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]'); await this.robustClick(checkboxFilter); const filterItem = this.page.locator( `[data-ouia-component-id="DataViewCheckboxFilter-filter-item-${checkboxLabel}"]`, diff --git a/frontend/e2e/pages/topology-page.ts b/frontend/e2e/pages/topology-page.ts index 5ddb215a7d7..d342d0d689d 100644 --- a/frontend/e2e/pages/topology-page.ts +++ b/frontend/e2e/pages/topology-page.ts @@ -9,7 +9,9 @@ export class TopologyPage extends BasePage { private readonly noResourcesFound = this.page.getByTestId('no-resources-found'); private readonly startBuildingLink = this.page.getByTestId('start-building-your-application'); private readonly addPageLink = this.page.getByTestId('add-page'); - private readonly filterByResourceDropdown = this.page.getByTestId('filter-by-resource').getByRole('button'); + private readonly filterByResourceDropdown = this.page + .getByTestId('filter-by-resource') + .getByRole('button'); private readonly displayOptionsButton = this.page .getByRole('button') .filter({ hasText: 'Display options' }); @@ -25,7 +27,9 @@ export class TopologyPage extends BasePage { private readonly workloadNameField = this.page.getByTestId('application-form-app-name'); private readonly resourceTypeField = this.page.getByTestId('form-select-input-resources-field'); private readonly saveChangesButton = this.page.getByTestId('save-changes'); - private readonly applicationDropdown = this.page.getByTestId('form-dropdown-application-name-field'); + private readonly applicationDropdown = this.page.getByTestId( + 'form-dropdown-application-name-field', + ); private readonly sidebarCloseButton = this.page.getByTestId('sidebar-close-button'); async navigateToTopology(namespace?: string): Promise { @@ -35,9 +39,7 @@ export class TopologyPage extends BasePage { } async navigateToTopologyGraph(namespace?: string): Promise { - const url = namespace - ? `/topology/ns/${namespace}?view=graph` - : '/topology?view=graph'; + const url = namespace ? `/topology/ns/${namespace}?view=graph` : '/topology?view=graph'; await this.goTo(url); await this.waitForLoadingComplete(10_000); } @@ -105,9 +107,7 @@ export class TopologyPage extends BasePage { // PF Topology internal class — no data-test available; may break on PF upgrades getNode(nodeName: string): Locator { - return this.page - .locator('g[class$="topology__node__label"]') - .filter({ hasText: nodeName }); + return this.page.locator('g[class$="topology__node__label"]').filter({ hasText: nodeName }); } async ensureGraphView(): Promise { @@ -139,7 +139,7 @@ export class TopologyPage extends BasePage { } async selectContextMenuAction(action: string): Promise { - const actionButton = this.page.getByRole('menuitem', { name: action }) + const actionButton = this.page.getByRole('menuitem', { name: action }); await expect(actionButton).toBeVisible({ timeout: 10_000 }); await this.robustClick(actionButton); } @@ -195,7 +195,10 @@ export class TopologyPage extends BasePage { async fillApplicationName(appName: string): Promise { // eslint-disable-next-line no-restricted-syntax - const hasDropdown = await this.applicationDropdown.waitFor({ state: 'visible', timeout: 2_000 }).then(() => true).catch(() => false); + const hasDropdown = await this.applicationDropdown + .waitFor({ state: 'visible', timeout: 2_000 }) + .then(() => true) + .catch(() => false); if (hasDropdown) { await this.applicationDropdown.click(); await this.page.getByRole('option', { name: 'Create application' }).click(); @@ -218,11 +221,7 @@ export class TopologyPage extends BasePage { async selectBuilderImageFromList(pattern: RegExp): Promise { await expect(this.page.getByRole('progressbar')).not.toBeAttached({ timeout: 60_000 }); - await this.page - .getByRole('listitem') - .filter({ hasText: pattern }) - .first() - .click(); + await this.page.getByRole('listitem').filter({ hasText: pattern }).first().click(); } async clickCreateButton(): Promise { @@ -234,9 +233,10 @@ export class TopologyPage extends BasePage { } getWorkload(name: string): Locator { - return this.page.locator(`[data-id="${name}"] text`).first().or( - this.page.locator('.pf-topology-content').getByText(name, { exact: true }), - ); + return this.page + .locator(`[data-id="${name}"] text`) + .first() + .or(this.page.locator('.pf-topology-content').getByText(name, { exact: true })); } async clickWorkload(name: string): Promise { diff --git a/frontend/e2e/pages/web-terminal-page.ts b/frontend/e2e/pages/web-terminal-page.ts index b810bd8e04e..af6319d2fd2 100644 --- a/frontend/e2e/pages/web-terminal-page.ts +++ b/frontend/e2e/pages/web-terminal-page.ts @@ -160,5 +160,4 @@ export class WebTerminalPage extends BasePage { await this.goTo(`/k8s/ns/${namespace}/workspace.devfile.io~v1alpha2~DevWorkspace/${name}/yaml`); await this.waitForLoadingComplete(30_000); } - } diff --git a/frontend/e2e/pages/yaml-editor-page.ts b/frontend/e2e/pages/yaml-editor-page.ts index ab1110da14d..c9b9aa90bfb 100644 --- a/frontend/e2e/pages/yaml-editor-page.ts +++ b/frontend/e2e/pages/yaml-editor-page.ts @@ -89,9 +89,7 @@ export class YamlEditorPage extends BasePage { } async closeSettingsModal(): Promise { - await this.robustClick( - this.getSettingsModal().locator('button[aria-label="Close"]'), - ); + await this.robustClick(this.getSettingsModal().locator('button[aria-label="Close"]')); } async selectTheme(themeName: 'Dark' | 'Light' | 'Use theme setting'): Promise { diff --git a/frontend/e2e/reporters/prow-junit-reporter.ts b/frontend/e2e/reporters/prow-junit-reporter.ts index 5704a287737..2458ceb82ed 100644 --- a/frontend/e2e/reporters/prow-junit-reporter.ts +++ b/frontend/e2e/reporters/prow-junit-reporter.ts @@ -180,8 +180,10 @@ class ProwJUnitReporter implements Reporter { for (const projectSuite of this.suite.suites) { for (const fileSuite of projectSuite.suites) { - const { entry, tests, failures, errors, skipped, flaky, failed } = - this._buildTestSuite(projectSuite.title, fileSuite); + const { entry, tests, failures, errors, skipped, flaky, failed } = this._buildTestSuite( + projectSuite.title, + fileSuite, + ); suiteEntries.push(entry); totalTests += tests; totalFailures += failures; @@ -230,7 +232,14 @@ class ProwJUnitReporter implements Reporter { await fs.promises.writeFile(linkFile, html); } - this._printSummary(totalTests, failedTests.length, totalSkipped, flakyTests, failedTests, result); + this._printSummary( + totalTests, + failedTests.length, + totalSkipped, + flakyTests, + failedTests, + result, + ); } private _buildTestSuite( diff --git a/frontend/e2e/setup/admin-auth.setup.ts b/frontend/e2e/setup/admin-auth.setup.ts index 54f64063583..08baf91ab79 100644 --- a/frontend/e2e/setup/admin-auth.setup.ts +++ b/frontend/e2e/setup/admin-auth.setup.ts @@ -1,18 +1,10 @@ -import * as path from 'path'; - import { test as setup } from '@playwright/test'; -import { performLogin, saveStorageState } from './login-helper'; - -const adminStorageState = path.resolve(import.meta.dirname, '..', '.auth', 'kubeadmin.json'); +import { adminStorageState, loginFromEnv, saveStorageState } from './login-helper'; setup('login as kubeadmin', async ({ page }) => { setup.skip(process.env.SKIP_GLOBAL_SETUP === 'true', 'SKIP_GLOBAL_SETUP is set'); - const baseURL = process.env.WEB_CONSOLE_URL || 'http://localhost:9000'; - const username = process.env.OPENSHIFT_USERNAME || 'kubeadmin'; - const password = process.env.BRIDGE_KUBEADMIN_PASSWORD || ''; - - await performLogin(page, baseURL, username, password, 'kube:admin'); + await loginFromEnv(page, 'admin'); await saveStorageState(page, adminStorageState); }); diff --git a/frontend/e2e/setup/developer-auth.setup.ts b/frontend/e2e/setup/developer-auth.setup.ts index 328139b4b74..38c6326162e 100644 --- a/frontend/e2e/setup/developer-auth.setup.ts +++ b/frontend/e2e/setup/developer-auth.setup.ts @@ -1,22 +1,14 @@ -import * as path from 'path'; - import { test as setup } from '@playwright/test'; -import { performLogin, saveStorageState } from './login-helper'; - -const developerStorageState = path.resolve(import.meta.dirname, '..', '.auth', 'developer.json'); +import { developerStorageState, loginFromEnv, saveStorageState } from './login-helper'; setup('login as developer', async ({ page }) => { setup.skip(process.env.SKIP_GLOBAL_SETUP === 'true', 'SKIP_GLOBAL_SETUP is set'); + setup.skip( + !process.env.BRIDGE_HTPASSWD_USERNAME || !process.env.BRIDGE_HTPASSWD_PASSWORD, + 'No developer credentials configured', + ); - const htpasswdUser = process.env.BRIDGE_HTPASSWD_USERNAME; - const htpasswdPass = process.env.BRIDGE_HTPASSWD_PASSWORD; - - setup.skip(!htpasswdUser || !htpasswdPass, 'No developer credentials configured'); - - const baseURL = process.env.WEB_CONSOLE_URL || 'http://localhost:9000'; - const htpasswdIdp = process.env.BRIDGE_HTPASSWD_IDP || htpasswdUser!; - - await performLogin(page, baseURL, htpasswdUser!, htpasswdPass!, htpasswdIdp); + await loginFromEnv(page, 'developer'); await saveStorageState(page, developerStorageState); }); diff --git a/frontend/e2e/setup/knative.setup.ts b/frontend/e2e/setup/knative.setup.ts index 521554a503a..e5e8b947a18 100644 --- a/frontend/e2e/setup/knative.setup.ts +++ b/frontend/e2e/setup/knative.setup.ts @@ -10,15 +10,9 @@ const SUBSCRIPTION_YAML = path.resolve( '../mocks/knative/serverlessOperatorSubscription.yaml', ); -const SERVING_YAML = path.resolve( - import.meta.dirname, - '../mocks/knative/knative-serving.yaml', -); +const SERVING_YAML = path.resolve(import.meta.dirname, '../mocks/knative/knative-serving.yaml'); -const EVENTING_YAML = path.resolve( - import.meta.dirname, - '../mocks/knative/knative-eventing.yaml', -); +const EVENTING_YAML = path.resolve(import.meta.dirname, '../mocks/knative/knative-eventing.yaml'); const isRetryableError = (err: unknown): boolean => { const msg = err instanceof Error ? err.message : String(err); @@ -37,7 +31,7 @@ const isRetryableError = (err: unknown): boolean => { setup.describe.configure({ mode: 'serial' }); -setup('install OpenShift Serverless operator if not present', async ({ }) => { +setup('install OpenShift Serverless operator if not present', async ({}) => { setup.setTimeout(600_000); const k8sClient = new KubernetesClient( @@ -115,11 +109,13 @@ setup('install OpenShift Serverless operator if not present', async ({ }) => { await new Promise((r) => setTimeout(r, 10_000)); } if (!csvSucceeded) { - throw new Error(`Serverless operator CSV did not reach Succeeded phase after ${Math.round((Date.now() - startTime) / 1000)}s`); + throw new Error( + `Serverless operator CSV did not reach Succeeded phase after ${Math.round((Date.now() - startTime) / 1000)}s`, + ); } }); -setup('create KnativeServing and KnativeEventing instances', async ({ }) => { +setup('create KnativeServing and KnativeEventing instances', async ({}) => { setup.setTimeout(600_000); const k8sClient = new KubernetesClient( diff --git a/frontend/e2e/setup/login-helper.ts b/frontend/e2e/setup/login-helper.ts index a05875dc50a..1e6676b8351 100644 --- a/frontend/e2e/setup/login-helper.ts +++ b/frontend/e2e/setup/login-helper.ts @@ -6,6 +6,9 @@ import { expect } from '@playwright/test'; const STORAGE_STATE_DIR = path.resolve(import.meta.dirname, '..', '.auth'); +export const adminStorageState = path.join(STORAGE_STATE_DIR, 'kubeadmin.json'); +export const developerStorageState = path.join(STORAGE_STATE_DIR, 'developer.json'); + export async function performLogin( page: Page, baseURL: string, @@ -23,22 +26,60 @@ export async function performLogin( return; } - await expect( - page.locator('[data-test-id="login"]').or(page.locator('#inputUsername')).first(), - ).toBeVisible({ timeout: 30_000 }); + const userMenu = page.getByTestId('user-dropdown-toggle'); + const loginForm = page.locator('[data-test-id="login"]').or(page.locator('#inputUsername')); + + // The context may already be authenticated (e.g. a reused storageState). In that + // case the OAuth flow completes automatically and lands back on the console + // without ever rendering a login form, so wait for whichever appears first. + await expect(userMenu.or(loginForm).first()).toBeVisible({ timeout: 60_000 }); + if (await userMenu.isVisible().catch(() => false)) { + return; + } if (idpName) { - const providerButton = page.getByText(idpName, { exact: true }); - if ((await providerButton.count()) > 0) { + const providerButton = page.getByText(idpName).first(); + if (await providerButton.isVisible().catch(() => false)) { await providerButton.click(); } } + await expect(page.locator('#inputUsername')).toBeVisible({ timeout: 30_000 }); await page.locator('#inputUsername').fill(username); await page.locator('#inputPassword').fill(password); await page.locator('button[type="submit"]').click(); - await expect(page.getByTestId('user-dropdown-toggle')).toBeVisible({ timeout: 60_000 }); + await expect(userMenu).toBeVisible({ timeout: 60_000 }); +} + +/** + * Log in using the credentials configured via environment variables for the + * given persona. Admin uses the kubeadmin / kube:admin identity provider; + * developer uses the htpasswd identity provider. Used both by the auth setup + * projects and as a re-authentication fallback for specs whose shared + * storageState session has expired or been invalidated mid-run. + */ +export async function loginFromEnv( + page: Page, + persona: 'admin' | 'developer', + baseURL: string = process.env.WEB_CONSOLE_URL || 'http://localhost:9000', +): Promise { + if (persona === 'developer') { + const username = process.env.BRIDGE_HTPASSWD_USERNAME; + const password = process.env.BRIDGE_HTPASSWD_PASSWORD; + if (!username || !password) { + throw new Error( + 'Developer credentials (BRIDGE_HTPASSWD_USERNAME/PASSWORD) are not configured', + ); + } + const idpName = process.env.BRIDGE_HTPASSWD_IDP || username; + await performLogin(page, baseURL, username, password, idpName); + return; + } + + const username = process.env.OPENSHIFT_USERNAME || 'kubeadmin'; + const password = process.env.BRIDGE_KUBEADMIN_PASSWORD || ''; + await performLogin(page, baseURL, username, password, 'kube:admin'); } export async function saveStorageState(page: Page, storagePath: string): Promise { 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 index ed99f954f02..bb05b12ac6c 100644 --- a/frontend/e2e/tests/console/app/admission-webhook-warning-notifications.spec.ts +++ b/frontend/e2e/tests/console/app/admission-webhook-warning-notifications.spec.ts @@ -68,9 +68,7 @@ spec: await expect(page.getByTestId('section-heading-Pod details')).toBeVisible({ timeout: 30_000 }); const warning = page.getByTestId(WARNING_ID); await expect(warning).toContainText('Admission Webhook Warning', { timeout: 10_000 }); - await expect(warning).toContainText( - `Pod ${POD_NAME}-a violates policy ${WARNING_FOO}`, - ); + await expect(warning).toContainText(`Pod ${POD_NAME}-a violates policy ${WARNING_FOO}`); await expect(page.getByTestId(LEARN_MORE_ID)).toContainText('Learn more'); await page.getByTestId(LEARN_MORE_ID).click(); }); diff --git a/frontend/e2e/tests/console/app/debug-pod.spec.ts b/frontend/e2e/tests/console/app/debug-pod.spec.ts index 28fa1794bd7..fdbd2d420d1 100644 --- a/frontend/e2e/tests/console/app/debug-pod.spec.ts +++ b/frontend/e2e/tests/console/app/debug-pod.spec.ts @@ -83,7 +83,10 @@ test.describe('Debug pod', () => { page, k8sClient, }) => { - test.setTimeout(300_000); + // This test is image-pull and reconcile heavy: it waits for a pod to + // CrashLoopBackOff and then spins up three separate debug pods. On a cold or + // slow CI cluster the default 300s is not enough, so allow more headroom. + test.setTimeout(480_000); const detailsPage = new DetailsPage(page); const listPage = new ListPage(page); @@ -95,9 +98,9 @@ test.describe('Debug pod', () => { await yamlEditorPage.setEditorContent(podYaml); await yamlEditorPage.clickSave(); await expect(yamlEditorPage.getYamlError()).not.toBeAttached(); - await expect( - page.getByTestId('section-heading-Pod details'), - ).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId('section-heading-Pod details')).toBeVisible({ + timeout: 30_000, + }); }); await test.step('Wait for pod to enter CrashLoopBackOff', async () => { diff --git a/frontend/e2e/tests/console/app/demo-dynamic-plugin.spec.ts b/frontend/e2e/tests/console/app/demo-dynamic-plugin.spec.ts index 86e0103b4cc..63a2a751180 100644 --- a/frontend/e2e/tests/console/app/demo-dynamic-plugin.spec.ts +++ b/frontend/e2e/tests/console/app/demo-dynamic-plugin.spec.ts @@ -17,12 +17,10 @@ const SHOULD_DEPLOY_PLUGIN = !IS_LOCAL_DEV; async function skipIfModelUnavailable(page: import('@playwright/test').Page): Promise { const errorHeading = page.getByRole('heading', { name: /Error loading/ }); // eslint-disable-next-line no-restricted-syntax - const hasError = await errorHeading - .waitFor({ state: 'visible', timeout: 10_000 }) - .then( - () => true, - () => false, - ); + const hasError = await errorHeading.waitFor({ state: 'visible', timeout: 10_000 }).then( + () => true, + () => false, + ); if (hasError) { test.skip(true, 'ConsolePlugin model not available in this environment'); } @@ -35,350 +33,332 @@ interface ManifestResource { } ['webpack', 'rspack'].forEach((bundler) => { - test.describe( - `Demo dynamic plugin [${bundler}]`, - { tag: ['@admin', '@dynamic-plugin'] }, - () => { - test.describe.configure({ mode: 'serial' }); - - let consolePluginPage: ConsolePluginPage; - let detailsPage: DetailsPage; - let listPage: ListPage; - let modalPage: ModalPage; - - test.beforeAll(async ({ k8sClient }) => { - test.setTimeout(180_000); - if (SHOULD_DEPLOY_PLUGIN) { - const manifestPath = path.resolve( - import.meta.dirname, - '../../../../../dynamic-demo-plugin/oc-manifest.yaml', + test.describe(`Demo dynamic plugin [${bundler}]`, { tag: ['@admin', '@dynamic-plugin'] }, () => { + test.describe.configure({ mode: 'serial' }); + + let consolePluginPage: ConsolePluginPage; + let detailsPage: DetailsPage; + let listPage: ListPage; + let modalPage: ModalPage; + + test.beforeAll(async ({ k8sClient }) => { + test.setTimeout(180_000); + if (SHOULD_DEPLOY_PLUGIN) { + const manifestPath = path.resolve( + import.meta.dirname, + '../../../../../dynamic-demo-plugin/oc-manifest.yaml', + ); + const textManifest = fs.readFileSync(manifestPath, 'utf-8'); + const yamlManifest = yaml.loadAll(textManifest) as ManifestResource[]; + + const deployment = yamlManifest.find(({ kind }) => kind === 'Deployment'); + const service = yamlManifest.find(({ kind }) => kind === 'Service'); + const consolePlugin = yamlManifest.find(({ kind }) => kind === 'ConsolePlugin'); + + if (!deployment || !service || !consolePlugin) { + throw new Error( + 'oc-manifest.yaml is missing required resources: Deployment, Service, or ConsolePlugin', ); - const textManifest = fs.readFileSync(manifestPath, 'utf-8'); - const yamlManifest = yaml.loadAll(textManifest) as ManifestResource[]; - - const deployment = yamlManifest.find(({ kind }) => kind === 'Deployment'); - const service = yamlManifest.find(({ kind }) => kind === 'Service'); - const consolePlugin = yamlManifest.find(({ kind }) => kind === 'ConsolePlugin'); - - if (!deployment || !service || !consolePlugin) { - throw new Error( - 'oc-manifest.yaml is missing required resources: Deployment, Service, or ConsolePlugin', - ); - } - - if (PLUGIN_PULL_SPEC && deployment.spec) { - const templateSpec = ( - (deployment.spec as Record).template as Record - ).spec as Record; - const containers = templateSpec.containers as Array>; - templateSpec.containers = containers.map((container, idx) => - idx === 0 ? { ...container, image: PLUGIN_PULL_SPEC } : container, - ); - } + } + if (PLUGIN_PULL_SPEC && deployment.spec) { const templateSpec = ( (deployment.spec as Record).template as Record ).spec as Record; - const deploymentContainers = templateSpec.containers as Array>; - deploymentContainers[0].env = [ - ...((deploymentContainers[0].env as Array>) ?? []), - { name: 'BUNDLER', value: bundler }, - ]; - - // eslint-disable-next-line no-console - console.log( - `Deploying ${PLUGIN_NAME} [${bundler}] with image: ${deploymentContainers[0]?.image ?? 'unknown'}`, - ); - - await k8sClient.createNamespace(PLUGIN_NAME); - - // Create the Service first so that the serving-cert-secret-name - // annotation triggers creation of the TLS secret before the - // Deployment pod tries to mount it. - await k8sClient.coreV1Api.createNamespacedService({ - namespace: PLUGIN_NAME, - body: service as unknown as Record, - }); - - await k8sClient.appsV1Api.createNamespacedDeployment({ - namespace: PLUGIN_NAME, - body: deployment as unknown as Record, - }); - - await k8sClient.waitForDeploymentReady(PLUGIN_NAME, PLUGIN_NAME); - - await k8sClient.createClusterCustomResource( - 'console.openshift.io', - 'v1', - 'consoleplugins', - consolePlugin as unknown as Record, + const containers = templateSpec.containers as Array>; + templateSpec.containers = containers.map((container, idx) => + idx === 0 ? { ...container, image: PLUGIN_PULL_SPEC } : container, ); } - }); - - test.beforeEach(async ({ page }) => { - consolePluginPage = new ConsolePluginPage(page); - detailsPage = new DetailsPage(page); - listPage = new ListPage(page); - modalPage = new ModalPage(page); - }); - test.afterAll(async ({ k8sClient }) => { - if (SHOULD_DEPLOY_PLUGIN) { - await k8sClient - .deleteClusterCustomResource( - 'console.openshift.io', - 'v1', - 'consoleplugins', - PLUGIN_NAME, - ) - .catch(() => { - // May already be deleted by the UI test - }); - await k8sClient.deleteNamespace(PLUGIN_NAME); - await k8sClient.waitForNamespaceDeleted(PLUGIN_NAME); - } - }); + const templateSpec = ( + (deployment.spec as Record).template as Record + ).spec as Record; + const deploymentContainers = templateSpec.containers as Array>; + deploymentContainers[0].env = [ + ...((deploymentContainers[0].env as Array>) ?? []), + { name: 'BUNDLER', value: bundler }, + ]; + + // eslint-disable-next-line no-console + console.log( + `Deploying ${PLUGIN_NAME} [${bundler}] with image: ${deploymentContainers[0]?.image ?? 'unknown'}`, + ); - test('enables the demo plugin and verifies it loads', async ({ page }) => { - test.setTimeout(600_000); - test.skip(IS_LOCAL_DEV, 'Plugin enablement is only tested on CI'); + await k8sClient.createNamespace(PLUGIN_NAME); - await test.step('Navigate to console plugins tab', async () => { - await consolePluginPage.navigateToConsolePlugins(); - await expect(consolePluginPage.getPluginNameCell(PLUGIN_NAME)).toBeVisible(); + // Create the Service first so that the serving-cert-secret-name + // annotation triggers creation of the TLS secret before the + // Deployment pod tries to mount it. + await k8sClient.coreV1Api.createNamespacedService({ + namespace: PLUGIN_NAME, + body: service as unknown as Record, }); - await test.step('Enable the plugin if not already enabled', async () => { - const enabledCell = page.getByTestId(`${PLUGIN_NAME}-enabled`); - const alreadyEnabled = (await enabledCell.textContent())?.includes('Enabled'); - if (alreadyEnabled) { - return; - } - await consolePluginPage.clickEditPluginButton(PLUGIN_NAME); - await modalPage.waitForOpen(); - await expect(modalPage.getModalTitle()).toContainText('Console plugin enablement'); - await page.getByTestId('Enable-radio-input').click(); - await modalPage.submit(); - await modalPage.waitForClosed(); - await expect(enabledCell).toContainText('Enabled'); + await k8sClient.appsV1Api.createNamespacedDeployment({ + namespace: PLUGIN_NAME, + body: deployment as unknown as Record, }); - await test.step('Verify plugin status is Loaded', async () => { - // After enablement the console-operator reconciles the ConsolePlugin - // and restarts the console-server pods. The restart invalidates the - // session (CSRF cookie), so we must navigate (not just API-call) to - // get fresh cookies from the new pod. Reload the console plugins page - // until the server has picked up the updated config and reports the - // plugin as Loaded. - await expect(async () => { - await consolePluginPage.navigateToConsolePlugins(); - await expect(page.getByTestId(`data-view-cell-${PLUGIN_NAME}-name`)).toBeVisible(); - await expect(page.getByTestId(`${PLUGIN_NAME}-status`)).toContainText('Loaded'); - }).toPass({ timeout: 300_000, intervals: [15_000] }); - }); - }); + await k8sClient.waitForDeploymentReady(PLUGIN_NAME, PLUGIN_NAME); - test('verifies Dashboard Card nav item', async ({ page }) => { - await consolePluginPage.navigateToOverview(); - const demoDashboardTab = page.getByTestId('horizontal-link-Demo Dashboard'); - await expect(demoDashboardTab).toHaveText('Demo Dashboard'); - await demoDashboardTab.click(); - await expect(page.getByTestId('demo-plugin-dashboard-card')).toContainText( - 'Metrics Dashboard Card example', + await k8sClient.createClusterCustomResource( + 'console.openshift.io', + 'v1', + 'consoleplugins', + consolePlugin as unknown as Record, ); - await expect(page.locator('div.graph-wrapper')).toBeAttached(); - }); - - test('verifies Dynamic Nav items', async ({ page }) => { - for (const navID of ['1', '2']) { - await test.step(`Dynamic Nav ${navID}`, async () => { - await consolePluginPage.navigateToDynamicRoute(navID); - await expect(page.getByTestId('title')).toContainText(`Dynamic Page ${navID}`); - await expect(page.getByTestId('alert-info')).toContainText('Example info alert'); - await expect(page.getByTestId('alert-warning')).toContainText('Example warning alert'); - await expect(page.getByTestId('hint')).toContainText('Example hint'); - await expect(page.getByTestId('card').first()).toContainText('Example card'); + } + }); + + test.beforeEach(async ({ page }) => { + consolePluginPage = new ConsolePluginPage(page); + detailsPage = new DetailsPage(page); + listPage = new ListPage(page); + modalPage = new ModalPage(page); + }); + + test.afterAll(async ({ k8sClient }) => { + if (SHOULD_DEPLOY_PLUGIN) { + await k8sClient + .deleteClusterCustomResource('console.openshift.io', 'v1', 'consoleplugins', PLUGIN_NAME) + .catch(() => { + // May already be deleted by the UI test }); - } - }); - - test('verifies Test Utilities nav item', async ({ page }) => { - await consolePluginPage.navigateToTestUtilities(); - await expect( - page.getByRole('heading', { name: 'Utilities from Dynamic Plugin SDK' }), - ).toBeVisible(); - await expect(page.getByText('Utility: consoleFetchJSON')).toBeVisible(); - await expect(page.getByText('Utility: useToast')).toBeVisible(); + await k8sClient.deleteNamespace(PLUGIN_NAME); + await k8sClient.waitForNamespaceDeleted(PLUGIN_NAME); + } + }); + + test('enables the demo plugin and verifies it loads', async ({ page }) => { + test.setTimeout(600_000); + test.skip(IS_LOCAL_DEV, 'Plugin enablement is only tested on CI'); + + await test.step('Navigate to console plugins tab', async () => { + await consolePluginPage.navigateToConsolePlugins(); + await expect(consolePluginPage.getPluginNameCell(PLUGIN_NAME)).toBeVisible(); }); - test('verifies List Page nav item', async ({ page }) => { - const podName = 'openshift-state-metrics'; - await consolePluginPage.navigateToDemoListPage(); - await expect(page.getByTestId('page-heading').locator('h1')).toContainText( - 'OpenShift Pods List Page', - ); - await listPage.filterByNameInput(podName); - await expect(page.getByTestId('resource-row').filter({ hasText: podName })).toBeVisible(); - }); - - test('verifies K8s API nav item', async ({ page }) => { - const apiIDs = ['k8sCreate', 'k8sGet', 'k8sPatch', 'k8sUpdate', 'k8sList', 'k8sDelete']; - await consolePluginPage.navigateToK8sApi(); - await expect( - page.getByRole('heading', { name: 'K8s API from Dynamic Plugin SDK' }), - ).toBeVisible(); - for (const apiID of apiIDs) { - await test.step(`K8s API: ${apiID}`, async () => { - await expect( - page.getByRole('button', { name: apiID, exact: true }), - ).toBeVisible(); - }); + await test.step('Enable the plugin if not already enabled', async () => { + const enabledCell = page.getByTestId(`${PLUGIN_NAME}-enabled`); + const alreadyEnabled = (await enabledCell.textContent())?.includes('Enabled'); + if (alreadyEnabled) { + return; } + await consolePluginPage.clickEditPluginButton(PLUGIN_NAME); + await modalPage.waitForOpen(); + await expect(modalPage.getModalTitle()).toContainText('Console plugin enablement'); + await page.getByTestId('Enable-radio-input').click(); + await modalPage.submit(); + await modalPage.waitForClosed(); + await expect(enabledCell).toContainText('Enabled'); }); - test('shows Dynamic Plugins in Cluster Overview Status card', async ({ page }) => { - await consolePluginPage.navigateToOverview(); - await page.getByRole('button', { name: 'Dynamic Plugins' }).click(); - await expect(page.getByText('Loaded plugins')).toBeVisible(); - const popover = page.locator('.pf-v6-c-popover'); - await expect( - popover.locator('a', { hasText: 'View all' }), - ).toHaveAttribute( - 'href', - '/k8s/cluster/operator.openshift.io~v1~Console/cluster/console-plugins', - ); - }); - - test('shows Dynamic Plugins in About modal', async ({ page }) => { - await consolePluginPage.navigateToOverview(); - await page.getByTestId('help-dropdown-toggle').click(); - await page.getByText('About', { exact: true }).click(); - await expect(page.locator('dt', { hasText: 'Dynamic plugins' })).toBeVisible(); - await expect(page.getByText('console-demo-plugin (0.0.0)')).toBeVisible(); - await page.getByRole('button', { name: 'Close Dialog' }).click(); - }); - - test('verifies extension point for customized create project modal', async ({ page }) => { - await consolePluginPage.navigateToProjects(); - await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible(); - await page.getByRole('button', { name: 'Create Project' }).click(); - await expect( - page.getByText('This modal is created with an extension'), - ).toBeVisible(); - await page.getByRole('button', { name: 'Cancel' }).click(); + await test.step('Verify plugin status is Loaded', async () => { + // After enablement the console-operator reconciles the ConsolePlugin + // and restarts the console-server pods. The restart invalidates the + // session (CSRF cookie), so we must navigate (not just API-call) to + // get fresh cookies from the new pod. Reload the console plugins page + // until the server has picked up the updated config and reports the + // plugin as Loaded. + await expect(async () => { + await consolePluginPage.navigateToConsolePlugins(); + await expect(page.getByTestId(`data-view-cell-${PLUGIN_NAME}-name`)).toBeVisible(); + await expect(page.getByTestId(`${PLUGIN_NAME}-status`)).toContainText('Loaded'); + }).toPass({ timeout: 300_000, intervals: [15_000] }); }); - - test('displays manifest tab in ConsolePlugin details page', async ({ page }) => { - await consolePluginPage.navigateToPluginDetails(PLUGIN_NAME); - await skipIfModelUnavailable(page); - await expect(detailsPage.getPageHeading()).toContainText(PLUGIN_NAME); - await expect( - page.getByTestId('horizontal-link-Plugin manifest'), - ).toBeVisible(); + }); + + test('verifies Dashboard Card nav item', async ({ page }) => { + await consolePluginPage.navigateToOverview(); + const demoDashboardTab = page.getByTestId('horizontal-link-Demo Dashboard'); + await expect(demoDashboardTab).toHaveText('Demo Dashboard'); + await demoDashboardTab.click(); + await expect(page.getByTestId('demo-plugin-dashboard-card')).toContainText( + 'Metrics Dashboard Card example', + ); + await expect(page.locator('div.graph-wrapper')).toBeAttached(); + }); + + test('verifies Dynamic Nav items', async ({ page }) => { + for (const navID of ['1', '2']) { + await test.step(`Dynamic Nav ${navID}`, async () => { + await consolePluginPage.navigateToDynamicRoute(navID); + await expect(page.getByTestId('title')).toContainText(`Dynamic Page ${navID}`); + await expect(page.getByTestId('alert-info')).toContainText('Example info alert'); + await expect(page.getByTestId('alert-warning')).toContainText('Example warning alert'); + await expect(page.getByTestId('hint')).toContainText('Example hint'); + await expect(page.getByTestId('card').first()).toContainText('Example card'); + }); + } + }); + + test('verifies Test Utilities nav item', async ({ page }) => { + await consolePluginPage.navigateToTestUtilities(); + await expect( + page.getByRole('heading', { name: 'Utilities from Dynamic Plugin SDK' }), + ).toBeVisible(); + await expect(page.getByText('Utility: consoleFetchJSON')).toBeVisible(); + await expect(page.getByText('Utility: useToast')).toBeVisible(); + }); + + test('verifies List Page nav item', async ({ page }) => { + const podName = 'openshift-state-metrics'; + await consolePluginPage.navigateToDemoListPage(); + await expect(page.getByTestId('page-heading').locator('h1')).toContainText( + 'OpenShift Pods List Page', + ); + await listPage.filterByNameInput(podName); + await expect(page.getByTestId('resource-row').filter({ hasText: podName })).toBeVisible(); + }); + + test('verifies K8s API nav item', async ({ page }) => { + const apiIDs = ['k8sCreate', 'k8sGet', 'k8sPatch', 'k8sUpdate', 'k8sList', 'k8sDelete']; + await consolePluginPage.navigateToK8sApi(); + await expect( + page.getByRole('heading', { name: 'K8s API from Dynamic Plugin SDK' }), + ).toBeVisible(); + for (const apiID of apiIDs) { + await test.step(`K8s API: ${apiID}`, async () => { + await expect(page.getByRole('button', { name: apiID, exact: true })).toBeVisible(); + }); + } + }); + + test('shows Dynamic Plugins in Cluster Overview Status card', async ({ page }) => { + await consolePluginPage.navigateToOverview(); + await page.getByRole('button', { name: 'Dynamic Plugins' }).click(); + await expect(page.getByText('Loaded plugins')).toBeVisible(); + const popover = page.locator('.pf-v6-c-popover'); + await expect(popover.locator('a', { hasText: 'View all' })).toHaveAttribute( + 'href', + '/k8s/cluster/operator.openshift.io~v1~Console/cluster/console-plugins', + ); + }); + + test('shows Dynamic Plugins in About modal', async ({ page }) => { + await consolePluginPage.navigateToOverview(); + await page.getByTestId('help-dropdown-toggle').click(); + await page.getByText('About', { exact: true }).click(); + await expect(page.locator('dt', { hasText: 'Dynamic plugins' })).toBeVisible(); + await expect(page.getByText('console-demo-plugin (0.0.0)')).toBeVisible(); + await page.getByRole('button', { name: 'Close Dialog' }).click(); + }); + + test('verifies extension point for customized create project modal', async ({ page }) => { + await consolePluginPage.navigateToProjects(); + await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible(); + await page.getByRole('button', { name: 'Create Project' }).click(); + await expect(page.getByText('This modal is created with an extension')).toBeVisible(); + await page.getByRole('button', { name: 'Cancel' }).click(); + }); + + test('displays manifest tab in ConsolePlugin details page', async ({ page }) => { + await consolePluginPage.navigateToPluginDetails(PLUGIN_NAME); + await skipIfModelUnavailable(page); + await expect(detailsPage.getPageHeading()).toContainText(PLUGIN_NAME); + await expect(page.getByTestId('horizontal-link-Plugin manifest')).toBeVisible(); + }); + + test('navigates to manifest tab and displays read-only editor with JSON', async ({ page }) => { + await consolePluginPage.navigateToPluginManifest(PLUGIN_NAME); + await expect(page).toHaveURL(/\/plugin-manifest/); + await skipIfModelUnavailable(page); + + await expect(page.getByTestId('horizontal-link-Plugin manifest')).toHaveAttribute( + 'aria-selected', + 'true', + ); + + const codeEditor = consolePluginPage.getCodeEditor(); + const emptyBox = consolePluginPage.getEmptyBox(); + const heading = detailsPage.getPageHeading(); + await expect(codeEditor.or(emptyBox).or(heading).first()).toBeVisible(); + }); + + test('manifest tab shows read-only editor when manifest is available', async ({ page }) => { + await consolePluginPage.navigateToPluginManifest(PLUGIN_NAME); + await skipIfModelUnavailable(page); + + const codeEditor = consolePluginPage.getCodeEditor(); + // eslint-disable-next-line no-restricted-syntax + const hasEditor = await codeEditor.waitFor({ state: 'visible', timeout: 5_000 }).then( + () => true, + () => false, + ); + test.skip(!hasEditor, 'Code editor not present — manifest not available'); + + await expect(consolePluginPage.getReadOnlyCodeEditor()).toHaveClass(/pf-m-read-only/); + const content = await getEditorContent(page); + expect(content).toContain('"name"'); + }); + + test('console plugin proxy copies plugin service response status code', async ({ page }) => { + test.skip(IS_LOCAL_DEV, 'Proxy test is only run on CI'); + + await consolePluginPage.navigateToPluginDetails(PLUGIN_NAME); + const pluginResponse = await page.request.get( + `/api/plugins/${PLUGIN_NAME}/plugin-manifest.json`, + ); + expect(pluginResponse.status()).toBe(200); + }); + + test('allows disabling dynamic plugins through a query parameter', async ({ page }) => { + await test.step('Disable non-existing plugin makes no changes', async () => { + await consolePluginPage.navigateWithQueryParam('disable-plugins=foo,bar'); + await expect(page.locator('#page-sidebar')).toContainText('Dynamic Nav'); }); - test('navigates to manifest tab and displays read-only editor with JSON', async ({ page }) => { - await consolePluginPage.navigateToPluginManifest(PLUGIN_NAME); - await expect(page).toHaveURL(/\/plugin-manifest/); - await skipIfModelUnavailable(page); - - await expect( - page.getByTestId('horizontal-link-Plugin manifest'), - ).toHaveAttribute('aria-selected', 'true'); - - const codeEditor = consolePluginPage.getCodeEditor(); - const emptyBox = consolePluginPage.getEmptyBox(); - const heading = detailsPage.getPageHeading(); - await expect(codeEditor.or(emptyBox).or(heading).first()).toBeVisible(); + await test.step('Disable one plugin', async () => { + await consolePluginPage.navigateWithQueryParam('disable-plugins=console-demo-plugin'); + await expect(page.locator('#page-sidebar')).not.toContainText('Dynamic Nav'); }); - test('manifest tab shows read-only editor when manifest is available', async ({ page }) => { - await consolePluginPage.navigateToPluginManifest(PLUGIN_NAME); - await skipIfModelUnavailable(page); - - const codeEditor = consolePluginPage.getCodeEditor(); - // eslint-disable-next-line no-restricted-syntax - const hasEditor = await codeEditor - .waitFor({ state: 'visible', timeout: 5_000 }) - .then( - () => true, - () => false, - ); - test.skip(!hasEditor, 'Code editor not present — manifest not available'); - - await expect(consolePluginPage.getReadOnlyCodeEditor()).toHaveClass(/pf-m-read-only/); - const content = await getEditorContent(page); - expect(content).toContain('"name"'); + await test.step('Disable all plugins', async () => { + await consolePluginPage.navigateWithQueryParam('disable-plugins'); + await expect(page.locator('#page-sidebar')).not.toContainText('Dynamic Nav'); }); + }); - test('console plugin proxy copies plugin service response status code', async ({ page }) => { - test.skip(IS_LOCAL_DEV, 'Proxy test is only run on CI'); + test('disables the demo plugin and deletes it', async ({ page }) => { + test.setTimeout(600_000); + test.skip(IS_LOCAL_DEV, 'Plugin disablement is only tested on CI'); - await consolePluginPage.navigateToPluginDetails(PLUGIN_NAME); - const pluginResponse = await page.request.get( - `/api/plugins/${PLUGIN_NAME}/plugin-manifest.json`, - ); - expect(pluginResponse.status()).toBe(200); + await test.step('Navigate to console plugins tab', async () => { + await consolePluginPage.navigateToConsolePlugins(); + await expect(consolePluginPage.getPluginNameCell(PLUGIN_NAME)).toBeVisible(); }); - test('allows disabling dynamic plugins through a query parameter', async ({ page }) => { - await test.step('Disable non-existing plugin makes no changes', async () => { - await consolePluginPage.navigateWithQueryParam('disable-plugins=foo,bar'); - await expect(page.locator('#page-sidebar')).toContainText('Dynamic Nav'); - }); - - await test.step('Disable one plugin', async () => { - await consolePluginPage.navigateWithQueryParam('disable-plugins=console-demo-plugin'); - await expect(page.locator('#page-sidebar')).not.toContainText('Dynamic Nav'); - }); - - await test.step('Disable all plugins', async () => { - await consolePluginPage.navigateWithQueryParam('disable-plugins'); - await expect(page.locator('#page-sidebar')).not.toContainText('Dynamic Nav'); - }); + await test.step('Disable the plugin', async () => { + await consolePluginPage.clickEditPluginButton(PLUGIN_NAME); + await modalPage.waitForOpen(); + await page.getByTestId('Disable-radio-input').click(); + await modalPage.submit(); + await modalPage.waitForClosed(); }); - test('disables the demo plugin and deletes it', async ({ page }) => { - test.setTimeout(600_000); - test.skip(IS_LOCAL_DEV, 'Plugin disablement is only tested on CI'); - - await test.step('Navigate to console plugins tab', async () => { + await test.step('Verify plugin is disabled', async () => { + // Disabling a plugin triggers a console-operator reconciliation that + // restarts console-server pods, similar to enablement. Navigate on + // each retry to get fresh cookies from the new pod. + await expect(async () => { await consolePluginPage.navigateToConsolePlugins(); - await expect(consolePluginPage.getPluginNameCell(PLUGIN_NAME)).toBeVisible(); - }); - - await test.step('Disable the plugin', async () => { - await consolePluginPage.clickEditPluginButton(PLUGIN_NAME); - await modalPage.waitForOpen(); - await page.getByTestId('Disable-radio-input').click(); - await modalPage.submit(); - await modalPage.waitForClosed(); - }); - - await test.step('Verify plugin is disabled', async () => { - // Disabling a plugin triggers a console-operator reconciliation that - // restarts console-server pods, similar to enablement. Navigate on - // each retry to get fresh cookies from the new pod. - await expect(async () => { - await consolePluginPage.navigateToConsolePlugins(); - await expect(page.getByTestId(`data-view-cell-${PLUGIN_NAME}-name`)).toBeVisible(); - const row = consolePluginPage.getPluginNameCell(PLUGIN_NAME).locator('xpath=ancestor::tr'); - await expect(row.getByTestId('edit-console-plugin')).toContainText('Disabled'); - await expect( - consolePluginPage.getPluginStatusCell(PLUGIN_NAME), - ).toContainText('-'); - }).toPass({ timeout: 300_000, intervals: [15_000] }); - }); + await expect(page.getByTestId(`data-view-cell-${PLUGIN_NAME}-name`)).toBeVisible(); + const row = consolePluginPage + .getPluginNameCell(PLUGIN_NAME) + .locator('xpath=ancestor::tr'); + await expect(row.getByTestId('edit-console-plugin')).toContainText('Disabled'); + await expect(consolePluginPage.getPluginStatusCell(PLUGIN_NAME)).toContainText('-'); + }).toPass({ timeout: 300_000, intervals: [15_000] }); + }); - await test.step('Delete the ConsolePlugin', async () => { - await consolePluginPage.getPluginNameCell(PLUGIN_NAME).locator('a').click(); - await expect(detailsPage.getPageHeading()).toContainText(PLUGIN_NAME); - await detailsPage.clickPageAction('Delete ConsolePlugin'); - await modalPage.waitForOpen(); - await modalPage.submit(); - }); + await test.step('Delete the ConsolePlugin', async () => { + await consolePluginPage.getPluginNameCell(PLUGIN_NAME).locator('a').click(); + await expect(detailsPage.getPageHeading()).toContainText(PLUGIN_NAME); + await detailsPage.clickPageAction('Delete ConsolePlugin'); + await modalPage.waitForOpen(); + await modalPage.submit(); }); - }, - ); + }); + }); }); diff --git a/frontend/e2e/tests/console/app/machine-config.spec.ts b/frontend/e2e/tests/console/app/machine-config.spec.ts index 47da761bee5..3017f4c29bb 100644 --- a/frontend/e2e/tests/console/app/machine-config.spec.ts +++ b/frontend/e2e/tests/console/app/machine-config.spec.ts @@ -24,7 +24,15 @@ test.describe('MachineConfig resource details page', () => { 'v1', 'machineconfigs', MC_WITH_CONFIG_FILES, - )) as { spec?: { config?: { storage?: { files?: Array<{ contents?: { source?: string }; mode?: number; overwrite?: boolean }> } } } }; + )) as { + spec?: { + config?: { + storage?: { + files?: Array<{ contents?: { source?: string }; mode?: number; overwrite?: boolean }>; + }; + }; + }; + }; const file = mc.spec?.config?.storage?.files?.[0]; expect(file).toBeDefined(); @@ -39,9 +47,9 @@ test.describe('MachineConfig resource details page', () => { await expect(descriptionList.getByText(String(file!.mode), { exact: true })).toBeVisible({ timeout: 10_000, }); - await expect( - descriptionList.getByText(String(file!.overwrite), { exact: true }), - ).toBeVisible({ timeout: 10_000 }); + await expect(descriptionList.getByText(String(file!.overwrite), { exact: true })).toBeVisible({ + timeout: 10_000, + }); const decodedContent = decodeURIComponent(file!.contents!.source!) .replace(/^(data:,)/, '') diff --git a/frontend/e2e/tests/console/app/poll-console-updates.spec.ts b/frontend/e2e/tests/console/app/poll-console-updates.spec.ts index 4c4ccc418cf..eeadc2a52e7 100644 --- a/frontend/e2e/tests/console/app/poll-console-updates.spec.ts +++ b/frontend/e2e/tests/console/app/poll-console-updates.spec.ts @@ -83,9 +83,7 @@ test.describe('PollConsoleUpdates', { tag: ['@admin'] }, () => { test.setTimeout(300_000); test('triggers the console update toast when consoleCommit changes', async ({ page }) => { - const updates = createMutableHandler((route) => - route.fulfill({ json: UPDATES_DEFAULT }), - ); + const updates = createMutableHandler((route) => route.fulfill({ json: UPDATES_DEFAULT })); await page.route(CHECK_UPDATES_URL, updates.handler); await navigateAndWaitForInit(page); @@ -99,9 +97,7 @@ test.describe('PollConsoleUpdates', { tag: ['@admin'] }, () => { }); test('triggers the console update toast when a plugin is added', async ({ page }) => { - const updates = createMutableHandler((route) => - route.fulfill({ json: UPDATES_DEFAULT }), - ); + const updates = createMutableHandler((route) => route.fulfill({ json: UPDATES_DEFAULT })); const manifest = createMutableHandler((route) => route.abort()); await page.route(CHECK_UPDATES_URL, updates.handler); await page.route(PLUGIN_MANIFEST_URL, manifest.handler); @@ -125,9 +121,7 @@ test.describe('PollConsoleUpdates', { tag: ['@admin'] }, () => { test('triggers the console update toast when a plugin is added and a different plugin endpoint is erroring', async ({ page, }) => { - const updates = createMutableHandler((route) => - route.fulfill({ json: UPDATES_NEW_PLUGIN }), - ); + const updates = createMutableHandler((route) => route.fulfill({ json: UPDATES_NEW_PLUGIN })); const manifest1 = createMutableHandler((route) => route.abort()); const manifest2 = createMutableHandler((route) => route.abort()); await page.route(PLUGIN_MANIFEST_URL, manifest1.handler); @@ -157,9 +151,7 @@ test.describe('PollConsoleUpdates', { tag: ['@admin'] }, () => { }); test('triggers the console update toast when a plugin is removed', async ({ page }) => { - const updates = createMutableHandler((route) => - route.fulfill({ json: UPDATES_NEW_PLUGIN }), - ); + const updates = createMutableHandler((route) => route.fulfill({ json: UPDATES_NEW_PLUGIN })); await page.route(CHECK_UPDATES_URL, updates.handler); await page.route(PLUGIN_MANIFEST_URL, (route) => route.fulfill({ json: PLUGIN_MANIFEST_DEFAULT }), @@ -176,9 +168,7 @@ test.describe('PollConsoleUpdates', { tag: ['@admin'] }, () => { const manifest = createMutableHandler((route) => route.fulfill({ json: PLUGIN_MANIFEST_DEFAULT }), ); - await page.route(CHECK_UPDATES_URL, (route) => - route.fulfill({ json: UPDATES_NEW_PLUGIN }), - ); + await page.route(CHECK_UPDATES_URL, (route) => route.fulfill({ json: UPDATES_NEW_PLUGIN })); await page.route(PLUGIN_MANIFEST_URL, manifest.handler); await navigateAndWaitForInit(page); diff --git a/frontend/e2e/tests/console/app/resource-log.spec.ts b/frontend/e2e/tests/console/app/resource-log.spec.ts index 401be68adf2..c5421405b53 100644 --- a/frontend/e2e/tests/console/app/resource-log.spec.ts +++ b/frontend/e2e/tests/console/app/resource-log.spec.ts @@ -89,7 +89,10 @@ test.describe('Pod log viewer', { tag: ['@admin'] }, () => { await test.step('Create namespace and pod', async () => { await k8sClient.createNamespace(ns); cleanup.trackNamespace(ns); - await k8sClient.createPod({ ...examplePodSpec, metadata: { ...examplePodSpec.metadata, namespace: ns } } as any); + await k8sClient.createPod({ + ...examplePodSpec, + metadata: { ...examplePodSpec.metadata, namespace: ns }, + } as any); await k8sClient.waitForPodReady('examplepod1', ns); }); @@ -124,8 +127,14 @@ test.describe('Pod log viewer', { tag: ['@admin'] }, () => { await test.step('Create namespace and pods', async () => { await k8sClient.createNamespace(ns); cleanup.trackNamespace(ns); - await k8sClient.createPod({ ...examplePodSpec, metadata: { ...examplePodSpec.metadata, namespace: ns } } as any); - await k8sClient.createPod({ ...wrapPodSpec, metadata: { ...wrapPodSpec.metadata, namespace: ns } } as any); + await k8sClient.createPod({ + ...examplePodSpec, + metadata: { ...examplePodSpec.metadata, namespace: ns }, + } as any); + await k8sClient.createPod({ + ...wrapPodSpec, + metadata: { ...wrapPodSpec.metadata, namespace: ns }, + } as any); await Promise.all([ k8sClient.waitForPodReady('examplepod1', ns), k8sClient.waitForPodReady('wraplogpod', ns), 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 index 78565708b8c..68a9889f274 100644 --- a/frontend/e2e/tests/console/app/start-job-from-cronjob.spec.ts +++ b/frontend/e2e/tests/console/app/start-job-from-cronjob.spec.ts @@ -48,18 +48,18 @@ spec: await yamlEditorPage.waitForEditorReady(); await yamlEditorPage.setEditorContent(cronJobYaml); await yamlEditorPage.clickSave(); - await expect( - page.getByTestId('section-heading-CronJob details'), - ).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId('section-heading-CronJob details')).toBeVisible({ + timeout: 30_000, + }); }); await test.step('Start Job from CronJob details page', async () => { await detailsPage.clickActionsMenuAction('Start Job'); await detailsPage.waitForPageLoad(); await retryOnModelNotFound(page); - await expect( - page.getByTestId('section-heading-Job details'), - ).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId('section-heading-Job details')).toBeVisible({ + timeout: 30_000, + }); await expect(detailsPage.title).toContainText(CRONJOB_NAME, { timeout: 30_000 }); }); @@ -92,9 +92,9 @@ spec: await detailsPage.waitForPageLoad(); await retryOnModelNotFound(page); - await expect( - page.getByTestId('section-heading-Job details'), - ).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId('section-heading-Job details')).toBeVisible({ + timeout: 30_000, + }); await expect(detailsPage.title).toContainText(CRONJOB_NAME, { timeout: 30_000 }); }); diff --git a/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager.spec.ts b/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager.spec.ts index 7856ac7f518..f0e908897a1 100644 --- a/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager.spec.ts +++ b/frontend/e2e/tests/console/cluster-settings/alertmanager/alertmanager.spec.ts @@ -1,9 +1,6 @@ import { test, expect } from '../../../../fixtures'; import jsYaml from 'js-yaml'; -import { - AlertmanagerPage, - getGlobalsAndReceiverConfig, -} from '../../../../pages/alertmanager-page'; +import { AlertmanagerPage, getGlobalsAndReceiverConfig } from '../../../../pages/alertmanager-page'; import KubernetesClient from '../../../../clients/kubernetes-client'; import { resetAlertmanagerConfig } from './alertmanager-test-utils'; @@ -727,19 +724,16 @@ test.describe('Alertmanager Receiver Forms', { tag: ['@admin'] }, () => { await alertmanager.save(); }); - await test.step( - 'Verify pagerduty_url saved with Receiver and global still exists', - async () => { - await expect(async () => { - await alertmanager.navigateToYAMLPage(); - const yamlContent = await alertmanager.getYAMLContent(); - const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); + await test.step('Verify pagerduty_url saved with Receiver and global still exists', async () => { + await expect(async () => { + await alertmanager.navigateToYAMLPage(); + const yamlContent = await alertmanager.getYAMLContent(); + const configs = getGlobalsAndReceiverConfig(receiverName, configName, yamlContent); - expect(configs.globals.pagerduty_url).toBe(pagerDutyURL2); - expect(configs.receiverConfig.url).toBe(pagerDutyURL3); - }).toPass({ intervals: [2_000, 3_000, 5_000], timeout: 30_000 }); - }, - ); + expect(configs.globals.pagerduty_url).toBe(pagerDutyURL2); + expect(configs.receiverConfig.url).toBe(pagerDutyURL3); + }).toPass({ intervals: [2_000, 3_000, 5_000], timeout: 30_000 }); + }); await test.step('Update advanced configuration fields', async () => { await alertmanager.navigateToEditReceiver(receiverName); diff --git a/frontend/e2e/tests/console/cluster-settings/channel-modal.spec.ts b/frontend/e2e/tests/console/cluster-settings/channel-modal.spec.ts index cda597ef54f..bdc10e6f019 100644 --- a/frontend/e2e/tests/console/cluster-settings/channel-modal.spec.ts +++ b/frontend/e2e/tests/console/cluster-settings/channel-modal.spec.ts @@ -47,9 +47,7 @@ test.describe('Cluster Settings channel modal', { tag: ['@admin', '@smoke'] }, ( await expect(clusterSettings.getModalTitle()).toContainText('Select channel'); - const dropdown = clusterSettings - .getChannelModal() - .getByTestId('console-select-menu-toggle'); + const dropdown = clusterSettings.getChannelModal().getByTestId('console-select-menu-toggle'); await expect(dropdown).toBeVisible(); await clusterSettings.page.keyboard.press('Escape'); diff --git a/frontend/e2e/tests/console/cluster-settings/managed-control-plane.spec.ts b/frontend/e2e/tests/console/cluster-settings/managed-control-plane.spec.ts index 027a1fbd2a3..8b1d2bb0d41 100644 --- a/frontend/e2e/tests/console/cluster-settings/managed-control-plane.spec.ts +++ b/frontend/e2e/tests/console/cluster-settings/managed-control-plane.spec.ts @@ -31,29 +31,26 @@ test.describe('Cluster Settings when control plane is managed', { tag: ['@admin' }); }); - await test.step( - 'Verify Details tab shows hosted alert and hides/disables update controls', - async () => { - await clusterSettings.navigateToDetails(); + await test.step('Verify Details tab shows hosted alert and hides/disables update controls', async () => { + await clusterSettings.navigateToDetails(); - // Hosted cluster alert should be visible - await expect(clusterSettings.getHostedAlert()).toBeVisible(); + // Hosted cluster alert should be visible + await expect(clusterSettings.getHostedAlert()).toBeVisible(); - // Update-related controls should be hidden or disabled - // Some are removed from DOM, others are disabled - check both states - await expect(clusterSettings.getCurrentChannelLink()).not.toBeAttached(); - await expect(clusterSettings.getUpdateButton()).not.toBeAttached(); - // Upstream server URL button is disabled but still in DOM - await expect(clusterSettings.getUpstreamServerUrl()).toBeDisabled(); - await expect(clusterSettings.getAutoscalerLink()).not.toBeAttached(); + // Update-related controls should be hidden or disabled + // Some are removed from DOM, others are disabled - check both states + await expect(clusterSettings.getCurrentChannelLink()).not.toBeAttached(); + await expect(clusterSettings.getUpdateButton()).not.toBeAttached(); + // Upstream server URL button is disabled but still in DOM + await expect(clusterSettings.getUpstreamServerUrl()).toBeDisabled(); + await expect(clusterSettings.getAutoscalerLink()).not.toBeAttached(); - // Temporary admin user messages should not exist - const tempAdminMessage = page.getByText(/logged in as a temporary administrative user/i); - await expect(tempAdminMessage).toBeHidden(); - const allowOthersMessage = page.getByText(/allow others to log in/i); - await expect(allowOthersMessage).toBeHidden(); - }, - ); + // Temporary admin user messages should not exist + const tempAdminMessage = page.getByText(/logged in as a temporary administrative user/i); + await expect(tempAdminMessage).toBeHidden(); + const allowOthersMessage = page.getByText(/allow others to log in/i); + await expect(allowOthersMessage).toBeHidden(); + }); await test.step('Verify Configuration tab hides cluster-level config resources', async () => { await clusterSettings.navigateToConfigurationTab(); diff --git a/frontend/e2e/tests/console/cluster-settings/update-modal.spec.ts b/frontend/e2e/tests/console/cluster-settings/update-modal.spec.ts index cb827c8cf56..9351ec1c8ed 100644 --- a/frontend/e2e/tests/console/cluster-settings/update-modal.spec.ts +++ b/frontend/e2e/tests/console/cluster-settings/update-modal.spec.ts @@ -50,9 +50,7 @@ test.describe('Cluster Settings cluster update modal', { tag: ['@admin'] }, () = await clusterSettings.openUpdateModal(); // Verify irreversibility notice is always shown - const irreversibilityNotice = page.getByTestId( - 'update-cluster-modal-irreversibility-notice', - ); + const irreversibilityNotice = page.getByTestId('update-cluster-modal-irreversibility-notice'); await expect(irreversibilityNotice).toBeVisible(); await clusterSettings.openUpdateDropdown(); diff --git a/frontend/e2e/tests/console/crd-extensions/console-yaml-sample.spec.ts b/frontend/e2e/tests/console/crd-extensions/console-yaml-sample.spec.ts index 049fd51ec48..8eee9f33e34 100644 --- a/frontend/e2e/tests/console/crd-extensions/console-yaml-sample.spec.ts +++ b/frontend/e2e/tests/console/crd-extensions/console-yaml-sample.spec.ts @@ -71,9 +71,7 @@ spec: await page.goto(`/k8s/cluster/console.openshift.io~v1~${crd}`); // Additional printer columns should not exist for this CRD - await expect( - page.getByTestId(/^additional-printer-column-header-/).first(), - ).toBeHidden(); + await expect(page.getByTestId(/^additional-printer-column-header-/).first()).toBeHidden(); // Created column should exist since Age does not await expect(page.getByTestId('column-header-Created')).toBeVisible(); diff --git a/frontend/e2e/tests/console/crud/annotations.spec.ts b/frontend/e2e/tests/console/crud/annotations.spec.ts index 828ee8002fb..467a9ae7987 100644 --- a/frontend/e2e/tests/console/crud/annotations.spec.ts +++ b/frontend/e2e/tests/console/crud/annotations.spec.ts @@ -23,11 +23,7 @@ function getRow(modal: ModalPage, index: number) { } test.describe('Annotations', { tag: ['@admin'] }, () => { - test('creates, edits, updates, and deletes annotations', async ({ - page, - k8sClient, - cleanup, - }) => { + test('creates, edits, updates, and deletes annotations', async ({ page, k8sClient, cleanup }) => { const namespace = `${generateTestName()}-ann`; await k8sClient.createNamespace(namespace); await k8sClient.waitForNamespaceReady(namespace); @@ -165,11 +161,7 @@ test.describe('Annotations', { tag: ['@admin'] }, () => { }); }); - test('disables Save when annotations change externally', async ({ - page, - k8sClient, - cleanup, - }) => { + test('disables Save when annotations change externally', async ({ page, k8sClient, cleanup }) => { const namespace = `${generateTestName()}-ann`; await k8sClient.createNamespace(namespace); await k8sClient.waitForNamespaceReady(namespace); diff --git a/frontend/e2e/tests/console/crud/customresourcedefinition.spec.ts b/frontend/e2e/tests/console/crud/customresourcedefinition.spec.ts index 57206b6d1b8..4ad1f5c44eb 100644 --- a/frontend/e2e/tests/console/crud/customresourcedefinition.spec.ts +++ b/frontend/e2e/tests/console/crud/customresourcedefinition.spec.ts @@ -121,9 +121,7 @@ test.describe('CustomResourceDefinitions', { tag: ['@admin'] }, () => { await setEditorContent(page, yaml.dump(merged, { sortKeys: true })); await yamlEditor.clickSave(); await expect(yamlEditor.getYamlError()).not.toBeAttached(); - await expect(page).toHaveURL( - new RegExp(`/k8s/cluster/customresourcedefinitions/${crdName}`), - ); + await expect(page).toHaveURL(new RegExp(`/k8s/cluster/customresourcedefinitions/${crdName}`)); }); await test.step('Verify CRD in list and navigate to instances', async () => { diff --git a/frontend/e2e/tests/console/crud/other-routes.spec.ts b/frontend/e2e/tests/console/crud/other-routes.spec.ts index 308ab86e1b3..1fd7d3bbb5e 100644 --- a/frontend/e2e/tests/console/crud/other-routes.spec.ts +++ b/frontend/e2e/tests/console/crud/other-routes.spec.ts @@ -73,9 +73,7 @@ const routes: RouteConfig[] = [ { path: '/api-resource/ns/default/core~v1~Pod/access', assertLoaded: async (page) => { - await expect( - page.locator('[data-ouia-component-type$="TableRow"]').first(), - ).toBeVisible(); + await expect(page.locator('[data-ouia-component-type$="TableRow"]').first()).toBeVisible(); }, }, { diff --git a/frontend/e2e/tests/console/crud/resource-crud.spec.ts b/frontend/e2e/tests/console/crud/resource-crud.spec.ts index 6d82c825f40..59214b19fa6 100644 --- a/frontend/e2e/tests/console/crud/resource-crud.spec.ts +++ b/frontend/e2e/tests/console/crud/resource-crud.spec.ts @@ -47,37 +47,212 @@ const RESOURCES_WITH_SYNCED_EDITOR = new Set([ ]); const k8sResources: ResourceDefinition[] = [ - { resource: 'pods', kind: 'Pod', namespaced: true, humanizeKind: true, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'serviceaccounts', kind: 'ServiceAccount', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'secrets', kind: 'Secret', namespaced: true, humanizeKind: true, skipYamlReloadTest: true, skipYamlSaveTest: false }, - { resource: 'persistentvolumes', kind: 'PersistentVolume', namespaced: false, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'storageclasses', kind: 'StorageClass', namespaced: false, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'cronjobs', kind: 'CronJob', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'jobs', kind: 'Job', namespaced: true, humanizeKind: true, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'daemonsets', kind: 'DaemonSet', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'deployments', kind: 'Deployment', namespaced: true, humanizeKind: true, skipYamlReloadTest: true, skipYamlSaveTest: true }, - { resource: 'replicasets', kind: 'ReplicaSet', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'replicationcontrollers', kind: 'ReplicationController', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'persistentvolumeclaims', kind: 'PersistentVolumeClaim', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'statefulsets', kind: 'StatefulSet', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'resourcequotas', kind: 'ResourceQuota', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'limitranges', kind: 'LimitRange', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'horizontalpodautoscalers', kind: 'HorizontalPodAutoscaler', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'roles', kind: 'Role', namespaced: true, humanizeKind: true, skipYamlReloadTest: false, skipYamlSaveTest: false }, + { + resource: 'pods', + kind: 'Pod', + namespaced: true, + humanizeKind: true, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'serviceaccounts', + kind: 'ServiceAccount', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'secrets', + kind: 'Secret', + namespaced: true, + humanizeKind: true, + skipYamlReloadTest: true, + skipYamlSaveTest: false, + }, + { + resource: 'persistentvolumes', + kind: 'PersistentVolume', + namespaced: false, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'storageclasses', + kind: 'StorageClass', + namespaced: false, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'cronjobs', + kind: 'CronJob', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'jobs', + kind: 'Job', + namespaced: true, + humanizeKind: true, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'daemonsets', + kind: 'DaemonSet', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'deployments', + kind: 'Deployment', + namespaced: true, + humanizeKind: true, + skipYamlReloadTest: true, + skipYamlSaveTest: true, + }, + { + resource: 'replicasets', + kind: 'ReplicaSet', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'replicationcontrollers', + kind: 'ReplicationController', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'persistentvolumeclaims', + kind: 'PersistentVolumeClaim', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'statefulsets', + kind: 'StatefulSet', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'resourcequotas', + kind: 'ResourceQuota', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'limitranges', + kind: 'LimitRange', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'horizontalpodautoscalers', + kind: 'HorizontalPodAutoscaler', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'roles', + kind: 'Role', + namespaced: true, + humanizeKind: true, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, ]; const openshiftResources: ResourceDefinition[] = [ - { resource: 'deploymentconfigs', kind: 'DeploymentConfig', namespaced: true, humanizeKind: false, skipYamlReloadTest: true, skipYamlSaveTest: true }, - { resource: 'buildconfigs', kind: 'BuildConfig', namespaced: true, humanizeKind: false, skipYamlReloadTest: true, skipYamlSaveTest: true }, - { resource: 'imagestreams', kind: 'ImageStream', namespaced: true, humanizeKind: false, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'user.openshift.io~v1~Group', kind: 'user.openshift.io~v1~Group', namespaced: false, humanizeKind: true, skipYamlReloadTest: false, skipYamlSaveTest: false }, + { + resource: 'deploymentconfigs', + kind: 'DeploymentConfig', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: true, + skipYamlSaveTest: true, + }, + { + resource: 'buildconfigs', + kind: 'BuildConfig', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: true, + skipYamlSaveTest: true, + }, + { + resource: 'imagestreams', + kind: 'ImageStream', + namespaced: true, + humanizeKind: false, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'user.openshift.io~v1~Group', + kind: 'user.openshift.io~v1~Group', + namespaced: false, + humanizeKind: true, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, ]; const snapshotResources: ResourceDefinition[] = [ - { resource: 'snapshot.storage.k8s.io~v1~VolumeSnapshot', kind: 'snapshot.storage.k8s.io~v1~VolumeSnapshot', namespaced: true, humanizeKind: true, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'snapshot.storage.k8s.io~v1~VolumeSnapshotClass', kind: 'snapshot.storage.k8s.io~v1~VolumeSnapshotClass', namespaced: false, humanizeKind: true, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'snapshot.storage.k8s.io~v1~VolumeSnapshotContent', kind: 'snapshot.storage.k8s.io~v1~VolumeSnapshotContent', namespaced: false, humanizeKind: true, skipYamlReloadTest: false, skipYamlSaveTest: false }, - { resource: 'storage.k8s.io~v1~VolumeAttributesClass', kind: 'storage.k8s.io~v1~VolumeAttributesClass', namespaced: false, humanizeKind: true, skipYamlReloadTest: false, skipYamlSaveTest: false }, + { + resource: 'snapshot.storage.k8s.io~v1~VolumeSnapshot', + kind: 'snapshot.storage.k8s.io~v1~VolumeSnapshot', + namespaced: true, + humanizeKind: true, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'snapshot.storage.k8s.io~v1~VolumeSnapshotClass', + kind: 'snapshot.storage.k8s.io~v1~VolumeSnapshotClass', + namespaced: false, + humanizeKind: true, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'snapshot.storage.k8s.io~v1~VolumeSnapshotContent', + kind: 'snapshot.storage.k8s.io~v1~VolumeSnapshotContent', + namespaced: false, + humanizeKind: true, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, + { + resource: 'storage.k8s.io~v1~VolumeAttributesClass', + kind: 'storage.k8s.io~v1~VolumeAttributesClass', + namespaced: false, + humanizeKind: true, + skipYamlReloadTest: false, + skipYamlSaveTest: false, + }, ]; function buildResourceList(): ResourceDefinition[] { @@ -188,14 +363,8 @@ test.describe('Kubernetes resource CRUD operations', { tag: ['@admin'] }, () => const allResources = buildResourceList(); for (const resourceDef of allResources) { - const { - resource, - kind, - namespaced, - humanizeKind, - skipYamlReloadTest, - skipYamlSaveTest, - } = resourceDef; + const { resource, kind, namespaced, humanizeKind, skipYamlReloadTest, skipYamlSaveTest } = + resourceDef; test(`${kind} CRUD lifecycle`, async ({ page, k8sClient, cleanup }) => { const testName = generateTestName(); @@ -256,11 +425,7 @@ test.describe('Kubernetes resource CRUD operations', { tag: ['@admin'] }, () => await page.goto(`${basePath}/${resource}/${name}`); await expect(detailsPage.getPageHeading()).toContainText(name); await testA11y(page, `Details page for ${kind}: ${name}`); - await testI18n(page, [ - '.pf-v6-c-tabs__item', - '[data-test-section-heading]', - 'dt', - ]); + await testI18n(page, ['.pf-v6-c-tabs__item', '[data-test-section-heading]', 'dt']); }); await test.step('View list page', async () => { diff --git a/frontend/e2e/tests/console/crud/roles-rolebindings.spec.ts b/frontend/e2e/tests/console/crud/roles-rolebindings.spec.ts index 099c9685fde..6ad5b8aad89 100644 --- a/frontend/e2e/tests/console/crud/roles-rolebindings.spec.ts +++ b/frontend/e2e/tests/console/crud/roles-rolebindings.spec.ts @@ -124,9 +124,7 @@ test.describe('Roles and RoleBindings', { tag: ['@admin'] }, () => { await expect(page.locator('th', { hasText: 'Actions' })).not.toBeAttached(); }); - test('displays Resource names and Verbs columns in ClusterRole rules table', async ({ - page, - }) => { + test('displays Resource names and Verbs columns in ClusterRole rules table', async ({ page }) => { const listPage = new ListPage(page); await page.goto('/k8s/all-namespaces/roles'); @@ -151,10 +149,7 @@ test.describe('Roles and RoleBindings', { tag: ['@admin'] }, () => { await page.goto(`/k8s/ns/${namespace}/${resource}`); await listPage.selectProject(namespace); await expect(namespaceDropdown).toContainText(namespace); - await listPage.filterByCheckbox( - rolesOrBindings === 'Roles' ? 'Role' : 'Kind', - 'namespace', - ); + await listPage.filterByCheckbox(rolesOrBindings === 'Roles' ? 'Role' : 'Kind', 'namespace'); await listPage.filterByName(name); await listPage.clickRowByName(name); @@ -184,10 +179,7 @@ test.describe('Roles and RoleBindings', { tag: ['@admin'] }, () => { await page.goto(`/k8s/all-namespaces/${resource}`); await listPage.selectAllProjects(); await expect(namespaceDropdown).toContainText('All Projects'); - await listPage.filterByCheckbox( - rolesOrBindings === 'Roles' ? 'Role' : 'Kind', - 'cluster', - ); + await listPage.filterByCheckbox(rolesOrBindings === 'Roles' ? 'Role' : 'Kind', 'cluster'); await listPage.filterByName(clusterName); await listPage.clickRowByName(clusterName); @@ -208,10 +200,7 @@ test.describe('Roles and RoleBindings', { tag: ['@admin'] }, () => { await page.goto(`/k8s/ns/${namespace}/${resource}`); await listPage.selectProject(namespace); await expect(namespaceDropdown).toContainText(namespace); - await listPage.filterByCheckbox( - rolesOrBindings === 'Roles' ? 'Role' : 'Kind', - 'cluster', - ); + await listPage.filterByCheckbox(rolesOrBindings === 'Roles' ? 'Role' : 'Kind', 'cluster'); await listPage.filterByName(clusterName); await listPage.clickRowByName(clusterName); diff --git a/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts b/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts index f9517aa4f26..5496f5cd90f 100644 --- a/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts +++ b/frontend/e2e/tests/console/crud/secrets/add-to-workload.spec.ts @@ -27,8 +27,7 @@ test.describe('Add Secret to Workloads', () => { containers: [ { name: 'httpd', - image: - 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', }, ], }, diff --git a/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts b/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts index 056ef40e88a..7f6b7c09002 100644 --- a/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts +++ b/frontend/e2e/tests/console/crud/secrets/image-pull.spec.ts @@ -78,8 +78,20 @@ test.describe('Image pull secrets', () => { await expect(secretsPage.getPageHeading()).toContainText('Create image pull secret'); await secretsPage.enterSecretName(secretName); await secretsPage.clickAddCredentials(); - await secretsPage.fillCredentialEntry(0, `${address}0`, `${username}0`, `${password}0`, `${mail}0`); - await secretsPage.fillCredentialEntry(1, `${address}1`, `${username}1`, `${password}1`, `${mail}1`); + await secretsPage.fillCredentialEntry( + 0, + `${address}0`, + `${username}0`, + `${password}0`, + `${mail}0`, + ); + await secretsPage.fillCredentialEntry( + 1, + `${address}1`, + `${username}1`, + `${password}1`, + `${mail}1`, + ); await secretsPage.save(); }); diff --git a/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts b/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts index 48afa820abb..7b839d25e9b 100644 --- a/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts +++ b/frontend/e2e/tests/console/crud/secrets/key-value.spec.ts @@ -43,10 +43,7 @@ test.describe('Create key/value secrets', () => { await k8sClient.deleteNamespace(namespace); }); - test('creates and edits a key/value secret with a binary file', async ({ - page, - k8sClient, - }) => { + test('creates and edits a key/value secret with a binary file', async ({ page, k8sClient }) => { const secretName = `kv-binary-${Date.now()}`; const secretsPage = new SecretsPage(page); const detailsPage = new DetailsPage(page); diff --git a/frontend/e2e/tests/console/dashboards/insights-popup.spec.ts b/frontend/e2e/tests/console/dashboards/insights-popup.spec.ts index 8948d2d1e3f..a445a808cea 100644 --- a/frontend/e2e/tests/console/dashboards/insights-popup.spec.ts +++ b/frontend/e2e/tests/console/dashboards/insights-popup.spec.ts @@ -32,9 +32,7 @@ test.describe('Insights Popup on Cluster Dashboard', { tag: ['@admin'] }, () => await dashboard.openInsightsPopup(); await expect( - dashboard - .getPopover() - .getByText('Red Hat Lightspeed Advisor identifies and prioritizes'), + dashboard.getPopover().getByText('Red Hat Lightspeed Advisor identifies and prioritizes'), ).toBeVisible(); }); @@ -71,7 +69,9 @@ test.describe('Insights Popup on Cluster Dashboard', { tag: ['@admin'] }, () => test.skip(!dataAvailable, 'Insights data is not available on this cluster'); const popover = dashboard.getPopover(); - const advisorLink = popover.getByText(/View (all recommendations|more) in Red Hat Lightspeed Advisor/); + const advisorLink = popover.getByText( + /View (all recommendations|more) in Red Hat Lightspeed Advisor/, + ); await expect(advisorLink).toBeVisible(); }); }); diff --git a/frontend/e2e/tests/console/dashboards/project-dashboard.spec.ts b/frontend/e2e/tests/console/dashboards/project-dashboard.spec.ts index 553d728128f..f7ea25c29aa 100644 --- a/frontend/e2e/tests/console/dashboards/project-dashboard.spec.ts +++ b/frontend/e2e/tests/console/dashboards/project-dashboard.spec.ts @@ -109,22 +109,17 @@ test.describe('Project Dashboard', { tag: ['@admin'] }, () => { test('is displayed when ConsoleLink CR exists', async ({ k8sClient, cleanup }) => { await test.step('Create ConsoleLink', async () => { - await k8sClient.createClusterCustomResource( - 'console.openshift.io', - 'v1', - 'consolelinks', - { - apiVersion: 'console.openshift.io/v1', - kind: 'ConsoleLink', - metadata: { name: consoleLinkName }, - spec: { - href: 'https://www.example.com/', - location: 'NamespaceDashboard', - namespaceDashboard: { namespaces: [namespace] }, - text: 'Namespace Dashboard Link', - }, + await k8sClient.createClusterCustomResource('console.openshift.io', 'v1', 'consolelinks', { + apiVersion: 'console.openshift.io/v1', + kind: 'ConsoleLink', + metadata: { name: consoleLinkName }, + spec: { + href: 'https://www.example.com/', + location: 'NamespaceDashboard', + namespaceDashboard: { namespaces: [namespace] }, + text: 'Namespace Dashboard Link', }, - ); + }); cleanup.trackClusterCustomResource( consoleLinkName, 'console.openshift.io', diff --git a/frontend/e2e/tests/console/nodes/node-groups-filter.spec.ts b/frontend/e2e/tests/console/nodes/node-groups-filter.spec.ts index 1b13b090bf2..a73396d68ac 100644 --- a/frontend/e2e/tests/console/nodes/node-groups-filter.spec.ts +++ b/frontend/e2e/tests/console/nodes/node-groups-filter.spec.ts @@ -1,6 +1,7 @@ import type { Page } from '@playwright/test'; import { test, expect } from '../../../fixtures'; +import { warmupSPA } from '../../../pages/base-page'; /** * E2E tests for Node Groups filtering functionality @@ -8,10 +9,14 @@ import { test, expect } from '../../../fixtures'; */ async function gotoNodesPage(page: Page): Promise { + // Warm the SPA shell first (self-heals a lost session and waits out plugin + // init) so navigating straight to this data-heavy page doesn't race a cold + // bootstrap — the direct goto was timing out on CI before the shell rendered. + await warmupSPA(page); await page.goto('/k8s/cluster/nodes'); await expect( page.getByTestId('data-view-table').or(page.getByTestId('page-heading')).first(), - ).toBeVisible(); + ).toBeVisible({ timeout: 30_000 }); } function groupsFilter(page: Page) { @@ -31,7 +36,11 @@ function filterChips(page: Page) { } async function skipIfGroupsFilterDisabled(page: Page): Promise { - if (!(await groupsFilter(page).isVisible().catch(() => false))) { + if ( + !(await groupsFilter(page) + .isVisible() + .catch(() => false)) + ) { test.skip(true, 'FLAG_OPENSHIFT_5 is not enabled'); } } @@ -54,9 +63,9 @@ test.describe('Node Groups Filter', () => { const groupsFilterButton = groupsFilter(page); await expect(groupsFilterButton).toBeVisible(); - const filtersToolbar = page.locator('[data-testid="filter-toolbar"]').or( - page.locator('.pf-v6-c-toolbar'), - ); + const filtersToolbar = page + .locator('[data-testid="filter-toolbar"]') + .or(page.locator('.pf-v6-c-toolbar')); await expect(filtersToolbar).toBeVisible(); const groupsFilterInToolbar = page @@ -105,8 +114,12 @@ test.describe('Node Groups Filter', () => { const dropdown = filterDropdown(page); await expect(dropdown).toBeVisible(); - const firstOption = dropdown.locator('[role="menuitemcheckbox"], .pf-v6-c-check__input').first(); - const optionCount = await dropdown.locator('[role="menuitemcheckbox"], .pf-v6-c-check__label').count(); + const firstOption = dropdown + .locator('[role="menuitemcheckbox"], .pf-v6-c-check__input') + .first(); + const optionCount = await dropdown + .locator('[role="menuitemcheckbox"], .pf-v6-c-check__label') + .count(); if (optionCount === 0) { await page.keyboard.press('Escape'); @@ -160,7 +173,9 @@ test.describe('Node Groups Filter', () => { await groupsFilter(page).click(); const dropdown = filterDropdown(page); - const firstOption = dropdown.locator('[role="menuitemcheckbox"], .pf-v6-c-check__input').first(); + const firstOption = dropdown + .locator('[role="menuitemcheckbox"], .pf-v6-c-check__input') + .first(); const optionCount = await dropdown.locator('[role="menuitemcheckbox"]').count(); if (optionCount === 0) { @@ -209,7 +224,9 @@ test.describe('Node Groups Filter', () => { await groupsFilter(page).click(); const groupsDropdown = filterDropdown(page); - const firstGroup = groupsDropdown.locator('[role="menuitemcheckbox"], .pf-v6-c-check__input').first(); + const firstGroup = groupsDropdown + .locator('[role="menuitemcheckbox"], .pf-v6-c-check__input') + .first(); await expect(firstGroup).toBeVisible(); await firstGroup.click(); await page.keyboard.press('Escape'); @@ -225,7 +242,9 @@ test.describe('Edit Groups Button', () => { await gotoNodesPage(page); }); - test('should display Edit groups button in page header when FLAG_OPENSHIFT_5 is enabled', async ({ page }) => { + test('should display Edit groups button in page header when FLAG_OPENSHIFT_5 is enabled', async ({ + page, + }) => { await skipIfEditGroupsButtonHidden(page); const editButton = page.getByRole('button', { name: /edit groups/i }); @@ -240,8 +259,17 @@ test.describe('Edit Groups Button', () => { await skipIfEditGroupsButtonHidden(page); const editButton = page.getByRole('button', { name: /edit groups/i }); - if (!(await editButton.isDisabled())) { - test.skip(true, 'Edit groups button is not disabled'); + // The button starts disabled while the permission (SelfSubjectAccessReview) + // check is in flight, then enables for users who can edit. Poll until that + // settles: if it becomes enabled the user has permission and this test does + // not apply (e.g. cluster-admin CI runs); only a button that stays disabled + // indicates a genuine lack of permission. + const deadline = Date.now() + 15_000; + while ((await editButton.isDisabled()) && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 500)); + } + if (await editButton.isEnabled()) { + test.skip(true, 'User has permission to edit groups; tooltip not applicable'); return; } @@ -250,7 +278,7 @@ test.describe('Edit Groups Button', () => { const tooltip = page.locator('[role="tooltip"]').filter({ hasText: /permission.*edit groups.*administrator/i, }); - await expect(tooltip).toBeVisible(); + await expect(tooltip).toBeVisible({ timeout: 10_000 }); }); test('should open Groups Editor modal when clicked', async ({ page }) => { @@ -269,7 +297,10 @@ test.describe('Edit Groups Button', () => { }); await expect(modal).toBeVisible(); - const closeButton = modal.locator('[aria-label="Close"], button').filter({ hasText: /cancel|close/i }).first(); + const closeButton = modal + .locator('[aria-label="Close"], button') + .filter({ hasText: /cancel|close/i }) + .first(); await expect(closeButton).toBeVisible(); await closeButton.click(); await expect(modal).toBeHidden(); @@ -292,7 +323,9 @@ test.describe('Node Detail Page - Edit Groups Button', () => { const nodeLink = rows.first().locator('a').first(); await nodeLink.click(); - await expect(page.locator('[data-test-id="details-card"], .pf-v6-c-card').first()).toBeVisible(); + await expect( + page.locator('[data-test-id="details-card"], .pf-v6-c-card').first(), + ).toBeVisible(); const groupsSection = page.locator('text=/groups/i').first(); if (!(await groupsSection.isVisible().catch(() => false))) { @@ -300,7 +333,8 @@ test.describe('Node Detail Page - Edit Groups Button', () => { return; } - const editButton = page.locator('[data-test="details-card"], .pf-v6-c-card') + const editButton = page + .locator('[data-test="details-card"], .pf-v6-c-card') .getByRole('button', { name: /^edit$/i }); if (!(await editButton.isVisible().catch(() => false))) { @@ -323,9 +357,12 @@ test.describe('Node Detail Page - Edit Groups Button', () => { const nodeLink = rows.first().locator('a').first(); await nodeLink.click(); - await expect(page.locator('[data-test-id="details-card"], .pf-v6-c-card').first()).toBeVisible(); + await expect( + page.locator('[data-test-id="details-card"], .pf-v6-c-card').first(), + ).toBeVisible(); - const editButton = page.locator('[data-test="details-card"], .pf-v6-c-card') + const editButton = page + .locator('[data-test="details-card"], .pf-v6-c-card') .getByRole('button', { name: /^edit$/i }); if (!(await editButton.isVisible().catch(() => false))) { diff --git a/frontend/e2e/tests/console/storage/clone.spec.ts b/frontend/e2e/tests/console/storage/clone.spec.ts index 87eb9a87e99..471ad400ad6 100644 --- a/frontend/e2e/tests/console/storage/clone.spec.ts +++ b/frontend/e2e/tests/console/storage/clone.spec.ts @@ -7,7 +7,10 @@ const cloneSize = '2'; test.describe('Clone Tests', { tag: ['@admin', '@storage'] }, () => { test('creates and deletes a PVC clone', async ({ page, k8sClient, cleanup }) => { - test.skip(!(await isAwsPlatform(k8sClient)), 'No CSI based storage classes are available on this platform'); + test.skip( + !(await isAwsPlatform(k8sClient)), + 'No CSI based storage classes are available on this platform', + ); const ns = `test-clone-${Date.now()}`; const pvcName = PVC.metadata.name; const cloneName = `${pvcName}-clone`; @@ -17,7 +20,10 @@ test.describe('Clone Tests', { tag: ['@admin', '@storage'] }, () => { await test.step('Set up namespace and resources', async () => { await k8sClient.createNamespace(ns); cleanup.trackNamespace(ns); - await k8sClient.createPVC(ns, { ...PVC, metadata: { ...PVC.metadata, namespace: ns } } as any); + await k8sClient.createPVC(ns, { + ...PVC, + metadata: { ...PVC.metadata, namespace: ns }, + } as any); await k8sClient.createPVC(ns, { ...PVCGP3, metadata: { ...PVCGP3.metadata, namespace: ns }, @@ -73,12 +79,11 @@ test.describe('Clone Tests', { tag: ['@admin', '@storage'] }, () => { }); }); - test('creates PVC clone with different storage class', async ({ - page, - k8sClient, - cleanup, - }) => { - test.skip(!(await isAwsPlatform(k8sClient)), 'No CSI based storage classes are available on this platform'); + test('creates PVC clone with different storage class', async ({ page, k8sClient, cleanup }) => { + test.skip( + !(await isAwsPlatform(k8sClient)), + 'No CSI based storage classes are available on this platform', + ); const ns = `test-clone-sc-${Date.now()}`; const pvcName = PVC.metadata.name; const cloneName = `${pvcName}-clone`; @@ -88,7 +93,10 @@ test.describe('Clone Tests', { tag: ['@admin', '@storage'] }, () => { await test.step('Set up namespace and resources', async () => { await k8sClient.createNamespace(ns); cleanup.trackNamespace(ns); - await k8sClient.createPVC(ns, { ...PVC, metadata: { ...PVC.metadata, namespace: ns } } as any); + await k8sClient.createPVC(ns, { + ...PVC, + metadata: { ...PVC.metadata, namespace: ns }, + } as any); await k8sClient.createPVC(ns, { ...PVCGP3, metadata: { ...PVCGP3.metadata, namespace: ns }, diff --git a/frontend/e2e/tests/console/storage/create-storage-class.spec.ts b/frontend/e2e/tests/console/storage/create-storage-class.spec.ts index aa64a85118b..b50c4408a81 100644 --- a/frontend/e2e/tests/console/storage/create-storage-class.spec.ts +++ b/frontend/e2e/tests/console/storage/create-storage-class.spec.ts @@ -45,7 +45,10 @@ test.describe( .getByTestId('storage-class-description') .fill('Storage class to be used for E2E tests only.'); await page.getByTestId('storage-class-provisioner-dropdown').click(); - await page.getByTestId('console-select-search-input').locator('input').fill(provisionerName); + await page + .getByTestId('console-select-search-input') + .locator('input') + .fill(provisionerName); await page.getByRole('option', { name: provisionerName }).click(); }); @@ -75,7 +78,10 @@ test.describe( }, ); -async function fillParameter(page: import('@playwright/test').Page, parameter: Parameter): Promise { +async function fillParameter( + page: import('@playwright/test').Page, + parameter: Parameter, +): Promise { const testId = getParameterTestId(parameter.name); const paramType = getParameterType(parameter); diff --git a/frontend/e2e/tests/console/storage/snapshot.spec.ts b/frontend/e2e/tests/console/storage/snapshot.spec.ts index bb9770a23d1..e11e3527796 100644 --- a/frontend/e2e/tests/console/storage/snapshot.spec.ts +++ b/frontend/e2e/tests/console/storage/snapshot.spec.ts @@ -11,7 +11,10 @@ import { test.describe('Snapshot Tests', { tag: ['@admin', '@storage'] }, () => { test('creates, lists, and deletes a VolumeSnapshot', async ({ page, k8sClient, cleanup }) => { - test.skip(!(await isAwsPlatform(k8sClient)), 'No CSI based storage classes are available on this platform'); + test.skip( + !(await isAwsPlatform(k8sClient)), + 'No CSI based storage classes are available on this platform', + ); const ns = `test-snap-${Date.now()}`; const pvcName = PVC.metadata.name; const snapshotName = `${pvcName}-snapshot`; @@ -21,7 +24,10 @@ test.describe('Snapshot Tests', { tag: ['@admin', '@storage'] }, () => { await test.step('Set up namespace and resources', async () => { await k8sClient.createNamespace(ns); cleanup.trackNamespace(ns); - await k8sClient.createPVC(ns, { ...PVC, metadata: { ...PVC.metadata, namespace: ns } } as any); + await k8sClient.createPVC(ns, { + ...PVC, + metadata: { ...PVC.metadata, namespace: ns }, + } as any); await k8sClient.createDeployment(ns, { ...testerDeployment, metadata: { ...testerDeployment.metadata, namespace: ns }, @@ -115,7 +121,10 @@ test.describe('Snapshot Tests', { tag: ['@admin', '@storage'] }, () => { }); test('restores a snapshot to create a new PVC', async ({ page, k8sClient, cleanup }) => { - test.skip(!(await isAwsPlatform(k8sClient)), 'No CSI based storage classes are available on this platform'); + test.skip( + !(await isAwsPlatform(k8sClient)), + 'No CSI based storage classes are available on this platform', + ); const ns = `test-snap-restore-${Date.now()}`; const pvcName = PVC.metadata.name; const snapshotName = `${pvcName}-snapshot`; @@ -126,7 +135,10 @@ test.describe('Snapshot Tests', { tag: ['@admin', '@storage'] }, () => { await test.step('Set up namespace and resources', async () => { await k8sClient.createNamespace(ns); cleanup.trackNamespace(ns); - await k8sClient.createPVC(ns, { ...PVC, metadata: { ...PVC.metadata, namespace: ns } } as any); + await k8sClient.createPVC(ns, { + ...PVC, + metadata: { ...PVC.metadata, namespace: ns }, + } as any); await k8sClient.createDeployment(ns, { ...testerDeployment, metadata: { ...testerDeployment.metadata, namespace: ns }, diff --git a/frontend/e2e/tests/console/storage/volume-attributes-class.spec.ts b/frontend/e2e/tests/console/storage/volume-attributes-class.spec.ts index c0985a061c9..a8318cca0a4 100644 --- a/frontend/e2e/tests/console/storage/volume-attributes-class.spec.ts +++ b/frontend/e2e/tests/console/storage/volume-attributes-class.spec.ts @@ -71,19 +71,17 @@ test.describe('VolumeAttributesClass E2E tests', { tag: ['@admin', '@storage'] } await test.step('Verify PVC details with requested VAC', async () => { await expect(page.getByTestId('page-heading')).toContainText(TEST_PVC); - await expect(page.getByTestId('pvc-requested-vac')).toContainText( - TEST_VAC_LOW_IOPS, - { timeout: 30_000 }, - ); + await expect(page.getByTestId('pvc-requested-vac')).toContainText(TEST_VAC_LOW_IOPS, { + timeout: 30_000, + }); await expect( page.getByTestId('pvc-status').locator('[data-test="status-text"]'), ).toContainText('Bound', { timeout: 120_000, }); - await expect(page.getByTestId('pvc-current-vac')).toContainText( - TEST_VAC_LOW_IOPS, - { timeout: 30_000 }, - ); + await expect(page.getByTestId('pvc-current-vac')).toContainText(TEST_VAC_LOW_IOPS, { + timeout: 30_000, + }); }); await test.step('Modify VolumeAttributesClass to high IOPS', async () => { @@ -96,14 +94,12 @@ test.describe('VolumeAttributesClass E2E tests', { tag: ['@admin', '@storage'] } await modal.submit(); await modal.waitForClosed(); - await expect(page.getByTestId('pvc-requested-vac')).toContainText( - TEST_VAC_HIGH_IOPS, - { timeout: 30_000 }, - ); - await expect(page.getByTestId('pvc-current-vac')).toContainText( - TEST_VAC_HIGH_IOPS, - { timeout: 30_000 }, - ); + await expect(page.getByTestId('pvc-requested-vac')).toContainText(TEST_VAC_HIGH_IOPS, { + timeout: 30_000, + }); + await expect(page.getByTestId('pvc-current-vac')).toContainText(TEST_VAC_HIGH_IOPS, { + timeout: 30_000, + }); }); await test.step('Attempt invalid VAC modification and verify error', async () => { @@ -116,14 +112,12 @@ test.describe('VolumeAttributesClass E2E tests', { tag: ['@admin', '@storage'] } await modal.submit(); await modal.waitForClosed(); - await expect(page.getByTestId('pvc-requested-vac')).toContainText( - TEST_VAC_INVALID, - { timeout: 30_000 }, - ); - await expect(page.getByTestId('pvc-current-vac')).toContainText( - TEST_VAC_HIGH_IOPS, - { timeout: 30_000 }, - ); + await expect(page.getByTestId('pvc-requested-vac')).toContainText(TEST_VAC_INVALID, { + timeout: 30_000, + }); + await expect(page.getByTestId('pvc-current-vac')).toContainText(TEST_VAC_HIGH_IOPS, { + timeout: 30_000, + }); await expect(page.getByTestId('vac-error-alert')).toBeVisible({ timeout: 60_000, diff --git a/frontend/e2e/tests/dev-console/add-page.spec.ts b/frontend/e2e/tests/dev-console/add-page.spec.ts index 3b7eee4ebbf..33f8a338582 100644 --- a/frontend/e2e/tests/dev-console/add-page.spec.ts +++ b/frontend/e2e/tests/dev-console/add-page.spec.ts @@ -12,11 +12,9 @@ import { AddPage } from '../../pages/dev-console/add-page'; */ async function ensureGettingStartedVisible(k8sClient: KubernetesClient): Promise { - await k8sClient.patchConfigMap( - 'user-settings-kubeadmin', - 'openshift-console-user-settings', - { 'devconsole.addPage.gettingStarted': 'show' }, - ); + await k8sClient.patchConfigMap('user-settings-kubeadmin', 'openshift-console-user-settings', { + 'devconsole.addPage.gettingStarted': 'show', + }); } test.describe('Add page on Developer Console', { tag: ['@dev-console', '@regression'] }, () => { diff --git a/frontend/e2e/tests/dev-console/catalog.spec.ts b/frontend/e2e/tests/dev-console/catalog.spec.ts index b2bec215d49..8d1c944e18a 100644 --- a/frontend/e2e/tests/dev-console/catalog.spec.ts +++ b/frontend/e2e/tests/dev-console/catalog.spec.ts @@ -19,87 +19,79 @@ import { TopologyPage } from '../../pages/topology-page'; * - A-09-TC011 (@manual) - Devfiles on Software Catalog */ -test.describe( - 'Create Application from Catalog', - { tag: ['@dev-console', '@smoke'] }, - () => { - const ns = `aut-addflow-catalog-${Date.now()}`; - let addPage: AddPage; - let catalogPage: CatalogPage; - let topologyPage: TopologyPage; +test.describe('Create Application from Catalog', { tag: ['@dev-console', '@smoke'] }, () => { + const ns = `aut-addflow-catalog-${Date.now()}`; + let addPage: AddPage; + let catalogPage: CatalogPage; + let topologyPage: TopologyPage; - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - addPage = new AddPage(page); - catalogPage = new CatalogPage(page); - topologyPage = new TopologyPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await warmupSPA(page); - }); + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + catalogPage = new CatalogPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await warmupSPA(page); + }); - test('deploy application using Catalog Template - MariaDB [A-01-TC02]', async () => { - test.slow(); + test('deploy application using Catalog Template - MariaDB [A-01-TC02]', async () => { + test.slow(); - await test.step('Navigate to Templates in Software Catalog', async () => { - await catalogPage.navigateToTemplates(ns); - }); + await test.step('Navigate to Templates in Software Catalog', async () => { + await catalogPage.navigateToTemplates(ns); + }); - await test.step('Select Databases category and MariaDB template', async () => { - await catalogPage.selectTemplateCategory('Databases'); - await catalogPage.searchAndSelectCard('MariaDB'); - }); + await test.step('Select Databases category and MariaDB template', async () => { + await catalogPage.selectTemplateCategory('Databases'); + await catalogPage.searchAndSelectCard('MariaDB'); + }); - await test.step('Instantiate template', async () => { - await catalogPage.clickInstantiateTemplate(); - await catalogPage.getFormSubmitButton().click(); - }); + await test.step('Instantiate template', async () => { + await catalogPage.clickInstantiateTemplate(); + await catalogPage.getFormSubmitButton().click(); + }); - await test.step('Verify workload in topology', async () => { - await topologyPage.waitForWorkload('mariadb'); - await expect(topologyPage.getWorkload('mariadb')).toBeVisible(); - }); + await test.step('Verify workload in topology', async () => { + await topologyPage.waitForWorkload('mariadb'); + await expect(topologyPage.getWorkload('mariadb')).toBeVisible(); }); - }, -); + }); +}); -test.describe( - 'Create Database from Add page', - { tag: ['@dev-console', '@smoke'] }, - () => { - const ns = `aut-addflow-database-${Date.now()}`; - let addPage: AddPage; - let catalogPage: CatalogPage; - let topologyPage: TopologyPage; +test.describe('Create Database from Add page', { tag: ['@dev-console', '@smoke'] }, () => { + const ns = `aut-addflow-database-${Date.now()}`; + let addPage: AddPage; + let catalogPage: CatalogPage; + let topologyPage: TopologyPage; - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - addPage = new AddPage(page); - catalogPage = new CatalogPage(page); - topologyPage = new TopologyPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); - }); + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + catalogPage = new CatalogPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); + }); - test('create Database from Add page - MariaDB [A-03-TC01]', async () => { - test.slow(); + test('create Database from Add page - MariaDB [A-03-TC01]', async () => { + test.slow(); - await test.step('Click Database card', async () => { - await addPage.clickDatabaseCard(); - }); + await test.step('Click Database card', async () => { + await addPage.clickDatabaseCard(); + }); - await test.step('Select MariaDB and instantiate', async () => { - await catalogPage.searchAndSelectCard('MariaDB'); - await catalogPage.clickInstantiateTemplate(); - await catalogPage.getFormSubmitButton().click(); - }); + await test.step('Select MariaDB and instantiate', async () => { + await catalogPage.searchAndSelectCard('MariaDB'); + await catalogPage.clickInstantiateTemplate(); + await catalogPage.getFormSubmitButton().click(); + }); - await test.step('Verify workload in topology', async () => { - await topologyPage.waitForWorkload('mariadb'); - await expect(topologyPage.getWorkload('mariadb')).toBeVisible(); - }); + await test.step('Verify workload in topology', async () => { + await topologyPage.waitForWorkload('mariadb'); + await expect(topologyPage.getWorkload('mariadb')).toBeVisible(); }); - }, -); + }); +}); test.describe( 'Software Catalog with All Namespaces', @@ -154,38 +146,34 @@ test.describe( }, ); -test.describe( - 'Software Catalog Page details', - { tag: ['@dev-console', '@regression'] }, - () => { - const ns = `aut-catalog-pagedetails-${Date.now()}`; - let catalogPage: CatalogPage; +test.describe('Software Catalog Page details', { tag: ['@dev-console', '@regression'] }, () => { + const ns = `aut-catalog-pagedetails-${Date.now()}`; + let catalogPage: CatalogPage; - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - catalogPage = new CatalogPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await warmupSPA(page); - await catalogPage.navigateToCatalog(ns); - }); + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + catalogPage = new CatalogPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await warmupSPA(page); + await catalogPage.navigateToCatalog(ns); + }); - test('Software Catalog page default view [A-09-TC01]', async () => { - await expect(catalogPage.getFilterInput()).toBeVisible(); - }); + test('Software Catalog page default view [A-09-TC01]', async () => { + await expect(catalogPage.getFilterInput()).toBeVisible(); + }); - test('Helm Charts on Software Catalog [A-09-TC06]', async () => { - await test.step('Click on Helm Charts type', async () => { - await catalogPage.selectTypeOption('Helm Charts'); - }); + test('Helm Charts on Software Catalog [A-09-TC06]', async () => { + await test.step('Click on Helm Charts type', async () => { + await catalogPage.selectTypeOption('Helm Charts'); + }); - await test.step('Verify Helm Charts are displayed', async () => { - const tiles = catalogPage.getCatalogTiles(); - await expect(tiles.first()).toBeVisible({ timeout: 30_000 }); - await expect(catalogPage.getFilterInput()).toBeVisible(); - }); + await test.step('Verify Helm Charts are displayed', async () => { + const tiles = catalogPage.getCatalogTiles(); + await expect(tiles.first()).toBeVisible({ timeout: 30_000 }); + await expect(catalogPage.getFilterInput()).toBeVisible(); }); - }, -); + }); +}); test.describe('Software Catalog operators', { tag: ['@dev-console', '@regression'] }, () => { let catalogPage: CatalogPage; diff --git a/frontend/e2e/tests/dev-console/cluster-customization.spec.ts b/frontend/e2e/tests/dev-console/cluster-customization.spec.ts index 98dc8fb0206..5025b12927c 100644 --- a/frontend/e2e/tests/dev-console/cluster-customization.spec.ts +++ b/frontend/e2e/tests/dev-console/cluster-customization.spec.ts @@ -2,51 +2,55 @@ import { test, expect } from '../../fixtures'; import { ensureDeveloperPerspective, warmupSPA } from '../../pages/base-page'; import { ClusterCustomizationPage } from '../../pages/dev-console/cluster-customization-page'; -test.describe('Cluster configuration customization', { tag: ['@dev-console', '@regression'] }, () => { - let customizationPage: ClusterCustomizationPage; - - test.beforeEach(async ({ page, k8sClient }) => { - await warmupSPA(page); - await ensureDeveloperPerspective(page, k8sClient); - customizationPage = new ClusterCustomizationPage(page); - await customizationPage.navigateToCustomize(); - await expect(customizationPage.getHeading()).toBeVisible({ timeout: 30_000 }); - }); - - // eslint-disable-next-line playwright/expect-expect - test('DC-01-TC01: Disable Developer catalog', async () => { - test.skip(true, 'Deferred to a future batch'); - }); - - // eslint-disable-next-line playwright/expect-expect - test('DC-01-TC02: Disable specific sub-catalogs', async () => { - test.skip(true, 'Deferred to a future batch'); - }); - - // eslint-disable-next-line playwright/expect-expect - test('DC-01-TC03: Disable Add page items', async () => { - test.skip(true, 'Deferred to a future batch'); - }); - - // eslint-disable-next-line playwright/expect-expect - test('DC-01-TC04: Re-enable catalogs after disabling', async () => { - test.skip(true, 'Deferred to a future batch'); - }); - - // eslint-disable-next-line playwright/expect-expect - test('DC-01-TC05: Verify console rollout after customization', async () => { - test.skip(true, 'Deferred to a future batch'); - }); - - test('verifies perspectives section on General tab', async () => { - await expect(customizationPage.getPerspectivesSection()).toBeVisible(); - await expect(customizationPage.getPerspectiveSectionItem('Developer')).toBeVisible(); - }); - - test('verifies Developer tab shows pre-pinned navigation', async () => { - await customizationPage.getTab('Developer').click(); - await expect(customizationPage.getPrePinnedSection()).toBeVisible({ timeout: 30_000 }); - await expect(customizationPage.getAvailableResources()).toBeVisible(); - await expect(customizationPage.getPinnedResources()).toBeVisible(); - }); -}); +test.describe( + 'Cluster configuration customization', + { tag: ['@dev-console', '@regression'] }, + () => { + let customizationPage: ClusterCustomizationPage; + + test.beforeEach(async ({ page, k8sClient }) => { + await warmupSPA(page); + await ensureDeveloperPerspective(page, k8sClient); + customizationPage = new ClusterCustomizationPage(page); + await customizationPage.navigateToCustomize(); + await expect(customizationPage.getHeading()).toBeVisible({ timeout: 30_000 }); + }); + + // eslint-disable-next-line playwright/expect-expect + test('DC-01-TC01: Disable Developer catalog', async () => { + test.skip(true, 'Deferred to a future batch'); + }); + + // eslint-disable-next-line playwright/expect-expect + test('DC-01-TC02: Disable specific sub-catalogs', async () => { + test.skip(true, 'Deferred to a future batch'); + }); + + // eslint-disable-next-line playwright/expect-expect + test('DC-01-TC03: Disable Add page items', async () => { + test.skip(true, 'Deferred to a future batch'); + }); + + // eslint-disable-next-line playwright/expect-expect + test('DC-01-TC04: Re-enable catalogs after disabling', async () => { + test.skip(true, 'Deferred to a future batch'); + }); + + // eslint-disable-next-line playwright/expect-expect + test('DC-01-TC05: Verify console rollout after customization', async () => { + test.skip(true, 'Deferred to a future batch'); + }); + + test('verifies perspectives section on General tab', async () => { + await expect(customizationPage.getPerspectivesSection()).toBeVisible(); + await expect(customizationPage.getPerspectiveSectionItem('Developer')).toBeVisible(); + }); + + test('verifies Developer tab shows pre-pinned navigation', async () => { + await customizationPage.getTab('Developer').click(); + await expect(customizationPage.getPrePinnedSection()).toBeVisible({ timeout: 30_000 }); + await expect(customizationPage.getAvailableResources()).toBeVisible(); + await expect(customizationPage.getPinnedResources()).toBeVisible(); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/configure-perspectives.spec.ts b/frontend/e2e/tests/dev-console/configure-perspectives.spec.ts index 4d3b51f6006..191f723ca61 100644 --- a/frontend/e2e/tests/dev-console/configure-perspectives.spec.ts +++ b/frontend/e2e/tests/dev-console/configure-perspectives.spec.ts @@ -7,9 +7,7 @@ class PerspectivePage extends BasePage { } getPerspectiveOption(name: string) { - return this.page - .getByTestId('perspective-switcher-menu-option') - .filter({ hasText: name }); + return this.page.getByTestId('perspective-switcher-menu-option').filter({ hasText: name }); } } diff --git a/frontend/e2e/tests/dev-console/container-image.spec.ts b/frontend/e2e/tests/dev-console/container-image.spec.ts index 517c538ae05..a72f8351d11 100644 --- a/frontend/e2e/tests/dev-console/container-image.spec.ts +++ b/frontend/e2e/tests/dev-console/container-image.spec.ts @@ -1,8 +1,5 @@ import { test, expect } from '../../fixtures'; -import { - AddPage, - DeployImagePage, -} from '../../pages/dev-console/add-page'; +import { AddPage, DeployImagePage } from '../../pages/dev-console/add-page'; import { TopologyPage } from '../../pages/topology-page'; /** @@ -67,49 +64,45 @@ test.describe( }, ); -test.describe( - 'Deploy image from internal registry', - { tag: ['@dev-console', '@smoke'] }, - () => { - const ns = `aut-addflow-deploy-int-${Date.now()}`; - let addPage: AddPage; - let deployPage: DeployImagePage; - let topologyPage: TopologyPage; - - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - addPage = new AddPage(page); - deployPage = new DeployImagePage(page); - topologyPage = new TopologyPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); +test.describe('Deploy image from internal registry', { tag: ['@dev-console', '@smoke'] }, () => { + const ns = `aut-addflow-deploy-int-${Date.now()}`; + let addPage: AddPage; + let deployPage: DeployImagePage; + let topologyPage: TopologyPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + deployPage = new DeployImagePage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); + }); + + test('deploy image from internal registry with Runtime icon [A-02-TC03]', async () => { + test.slow(); + + await test.step('Navigate to Deploy Image page', async () => { + await addPage.clickContainerImage(); }); - test('deploy image from internal registry with Runtime icon [A-02-TC03]', async () => { - test.slow(); - - await test.step('Navigate to Deploy Image page', async () => { - await addPage.clickContainerImage(); - }); - - await test.step('Select internal registry image stream', async () => { - await deployPage.selectImageStreamTag(); - await deployPage.selectProject('openshift'); - await deployPage.selectImageStream('golang'); - await deployPage.selectTag('latest'); - }); + await test.step('Select internal registry image stream', async () => { + await deployPage.selectImageStreamTag(); + await deployPage.selectProject('openshift'); + await deployPage.selectImageStream('golang'); + await deployPage.selectTag('latest'); + }); - await test.step('Configure and create deployment', async () => { - await deployPage.selectRuntimeIcon('fedora'); - await deployPage.enterName('hello-internal'); - await deployPage.selectResourceType('Deployment'); - await deployPage.clickCreate(); - }); + await test.step('Configure and create deployment', async () => { + await deployPage.selectRuntimeIcon('fedora'); + await deployPage.enterName('hello-internal'); + await deployPage.selectResourceType('Deployment'); + await deployPage.clickCreate(); + }); - await test.step('Verify workload in topology', async () => { - await topologyPage.waitForWorkload('hello-internal'); - await expect(topologyPage.getWorkload('hello-internal')).toBeVisible(); - }); + await test.step('Verify workload in topology', async () => { + await topologyPage.waitForWorkload('hello-internal'); + await expect(topologyPage.getWorkload('hello-internal')).toBeVisible(); }); - }, -); + }); +}); diff --git a/frontend/e2e/tests/dev-console/create-from-yaml.spec.ts b/frontend/e2e/tests/dev-console/create-from-yaml.spec.ts index 30359f44c16..9f1bba9b223 100644 --- a/frontend/e2e/tests/dev-console/create-from-yaml.spec.ts +++ b/frontend/e2e/tests/dev-console/create-from-yaml.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '../../fixtures'; -import { warmupSPA } from '../../pages/base-page'; +import { setEditorContent, warmupSPA } from '../../pages/base-page'; import { AddPage, ImportYAMLPage } from '../../pages/dev-console/add-page'; import { TopologyPage } from '../../pages/topology-page'; @@ -41,62 +41,57 @@ spec: - ALL `; -test.describe( - 'Create Application from YAML file', - { tag: ['@dev-console', '@smoke'] }, - () => { - const ns = `aut-addflow-yaml-${Date.now()}`; - let addPage: AddPage; - let yamlPage: ImportYAMLPage; - let topologyPage: TopologyPage; +test.describe('Create Application from YAML file', { tag: ['@dev-console', '@smoke'] }, () => { + const ns = `aut-addflow-yaml-${Date.now()}`; + let addPage: AddPage; + let yamlPage: ImportYAMLPage; + let topologyPage: TopologyPage; - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - addPage = new AddPage(page); - yamlPage = new ImportYAMLPage(page); - topologyPage = new TopologyPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await warmupSPA(page); - await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); - }); + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + yamlPage = new ImportYAMLPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await warmupSPA(page); + await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); + }); - test('create a workload from YAML file [A-07-TC01]', async ({ page }) => { - test.slow(); + test('create a workload from YAML file [A-07-TC01]', async ({ page }) => { + test.slow(); - await test.step('Navigate to Import YAML page', async () => { - await addPage.clickImportYAML(); - }); + await test.step('Navigate to Import YAML page', async () => { + await addPage.clickImportYAML(); + }); - await test.step('Enter YAML content and create', async () => { - await page.waitForFunction( - () => !!(window as any).monaco?.editor?.getModels()?.[0], - { timeout: 30_000 }, - ); - await page.evaluate((yaml) => { - (window as any).monaco.editor.getModels()[0].setValue(yaml); - }, GIT_DC_YAML); - await yamlPage.getSubmitButton().click(); - }); + await test.step('Enter YAML content and create', async () => { + await setEditorContent(page, GIT_DC_YAML); + await yamlPage.getSubmitButton().click(); + // The editor issues the create POST asynchronously and redirects to the + // new resource on success. Wait for that redirect before navigating + // away — otherwise the next navigation aborts the in-flight create + // request ("Failed to fetch") and the workload is never created. + await page.waitForURL(/\/deploymentconfigs\/shell-app(\/|$|\?)/, { timeout: 60_000 }); + }); - await test.step('Verify workload in topology', async () => { - await topologyPage.navigateToTopology(ns); - await topologyPage.waitForWorkload('shell-app'); - await expect(topologyPage.getWorkload('shell-app')).toBeVisible(); - }); + await test.step('Verify workload in topology', async () => { + await topologyPage.navigateToTopology(ns); + await topologyPage.waitForWorkload('shell-app'); + await expect(topologyPage.getWorkload('shell-app')).toBeVisible(); }); + }); - test('cancel operation on YAML file redirects to Add page [A-07-TC02]', async () => { - await test.step('Navigate to Import YAML page', async () => { - await addPage.clickImportYAML(); - }); + test('cancel operation on YAML file redirects to Add page [A-07-TC02]', async () => { + await test.step('Navigate to Import YAML page', async () => { + await addPage.clickImportYAML(); + }); - await test.step('Click cancel', async () => { - await yamlPage.getCancelButton().click(); - }); + await test.step('Click cancel', async () => { + await yamlPage.getCancelButton().click(); + }); - await test.step('Verify redirect to Add page', async () => { - await expect(addPage.getPageHeading()).toBeVisible(); - }); + await test.step('Verify redirect to Add page', async () => { + await expect(addPage.getPageHeading()).toBeVisible(); }); - }, -); + }); +}); diff --git a/frontend/e2e/tests/dev-console/deployment.spec.ts b/frontend/e2e/tests/dev-console/deployment.spec.ts index 7522ce240cf..9c2c83d9d45 100644 --- a/frontend/e2e/tests/dev-console/deployment.spec.ts +++ b/frontend/e2e/tests/dev-console/deployment.spec.ts @@ -16,7 +16,11 @@ test.describe('Deployment form view', { tag: ['@dev-console', '@smoke'] }, () => }); for (const strategyName of strategies) { - test(`creates deployment with ${strategyName} strategy`, async ({ page, k8sClient, cleanup }) => { + test(`creates deployment with ${strategyName} strategy`, async ({ + page, + k8sClient, + cleanup, + }) => { const ns = `aut-deploy-${strategyName.replace(/\s+/g, '').toLowerCase()}-${Date.now()}`; const deploymentName = 'test-deploy'; const deploymentPage = new DeploymentPage(page); diff --git a/frontend/e2e/tests/dev-console/filter-quick-starts.spec.ts b/frontend/e2e/tests/dev-console/filter-quick-starts.spec.ts index 5afc6ebd7ed..b119e8d9e68 100644 --- a/frontend/e2e/tests/dev-console/filter-quick-starts.spec.ts +++ b/frontend/e2e/tests/dev-console/filter-quick-starts.spec.ts @@ -1,74 +1,70 @@ import { test, expect } from '../../fixtures'; import { QuickStartsPage } from '../../pages/dev-console/quick-starts-page'; -test.describe( - 'Filter Quick Starts catalog', - { tag: ['@dev-console', '@guided-tour'] }, - () => { - let quickStartsPage: QuickStartsPage; +test.describe('Filter Quick Starts catalog', { tag: ['@dev-console', '@guided-tour'] }, () => { + let quickStartsPage: QuickStartsPage; - test.beforeEach(async ({ page }) => { - quickStartsPage = new QuickStartsPage(page); - await quickStartsPage.navigateToCatalog(); - }); + test.beforeEach(async ({ page }) => { + quickStartsPage = new QuickStartsPage(page); + await quickStartsPage.navigateToCatalog(); + }); - test( - 'QS-01-TC01: Quick Starts catalog page has title and filters', - { tag: ['@smoke'] }, - async () => { - await test.step('Verify page title is visible', async () => { - await expect(quickStartsPage.getPageTitle()).toBeVisible({ timeout: 30_000 }); - }); + test( + 'QS-01-TC01: Quick Starts catalog page has title and filters', + { tag: ['@smoke'] }, + async () => { + await test.step('Verify page title is visible', async () => { + await expect(quickStartsPage.getPageTitle()).toBeVisible({ timeout: 30_000 }); + }); - await test.step('Verify keyword filter is visible', async () => { - await expect(quickStartsPage.getFilterInput()).toBeVisible(); - }); + await test.step('Verify keyword filter is visible', async () => { + await expect(quickStartsPage.getFilterInput()).toBeVisible(); + }); - await test.step('Verify status filter is visible', async () => { - await expect(quickStartsPage.getStatusFilterToggle()).toBeVisible(); - }); - }, - ); + await test.step('Verify status filter is visible', async () => { + await expect(quickStartsPage.getStatusFilterToggle()).toBeVisible(); + }); + }, + ); - test( - 'QS-01-TC02: Filter by keyword shows matching quick start', - { tag: ['@regression'] }, - async () => { - await quickStartsPage.filterByKeyword('sample'); - await expect( - quickStartsPage.getQuickStartCard('sample-application'), - ).toBeVisible({ timeout: 10_000 }); - }, - ); + test( + 'QS-01-TC02: Filter by keyword shows matching quick start', + { tag: ['@regression'] }, + async () => { + await quickStartsPage.filterByKeyword('sample'); + await expect(quickStartsPage.getQuickStartCard('sample-application')).toBeVisible({ + timeout: 10_000, + }); + }, + ); - test( - 'QS-01-TC03: Status filter dropdown shows all status options', - { tag: ['@regression'] }, - async () => { - await quickStartsPage.openStatusFilter(); + test( + 'QS-01-TC03: Status filter dropdown shows all status options', + { tag: ['@regression'] }, + async () => { + await quickStartsPage.openStatusFilter(); - await test.step('Verify all status options are visible', async () => { - await expect(quickStartsPage.getStatusOption('Complete')).toBeVisible(); - await expect(quickStartsPage.getStatusOption('In progress')).toBeVisible(); - await expect(quickStartsPage.getStatusOption('Not started')).toBeVisible(); - }); - }, - ); + await test.step('Verify all status options are visible', async () => { + await expect(quickStartsPage.getStatusOption('Complete')).toBeVisible(); + await expect(quickStartsPage.getStatusOption('In progress')).toBeVisible(); + await expect(quickStartsPage.getStatusOption('Not started')).toBeVisible(); + }); + }, + ); - test( - 'QS-01-TC05: Filter with no matches shows empty state', - { tag: ['@regression'] }, - async () => { - await quickStartsPage.filterByKeyword('abcxyz123'); + test( + 'QS-01-TC05: Filter with no matches shows empty state', + { tag: ['@regression'] }, + async () => { + await quickStartsPage.filterByKeyword('abcxyz123'); - await test.step('Verify empty state message', async () => { - await expect(quickStartsPage.getEmptyState()).toBeVisible(); - }); + await test.step('Verify empty state message', async () => { + await expect(quickStartsPage.getEmptyState()).toBeVisible(); + }); - await test.step('Verify clear all filters button', async () => { - await expect(quickStartsPage.getClearFilterButton()).toBeVisible(); - }); - }, - ); - }, -); + await test.step('Verify clear all filters button', async () => { + await expect(quickStartsPage.getClearFilterButton()).toBeVisible(); + }); + }, + ); +}); diff --git a/frontend/e2e/tests/dev-console/import-from-devfile.spec.ts b/frontend/e2e/tests/dev-console/import-from-devfile.spec.ts index b6ac57fccd2..22db6b1f3fc 100644 --- a/frontend/e2e/tests/dev-console/import-from-devfile.spec.ts +++ b/frontend/e2e/tests/dev-console/import-from-devfile.spec.ts @@ -1,8 +1,5 @@ import { test, expect } from '../../fixtures'; -import { - AddPage, - ImportFromGitPage, -} from '../../pages/dev-console/add-page'; +import { AddPage, ImportFromGitPage } from '../../pages/dev-console/add-page'; import { TopologyPage } from '../../pages/topology-page'; /** @@ -15,46 +12,42 @@ import { TopologyPage } from '../../pages/topology-page'; * - A-04-TC05 (@to-do) - Create Devfiles workload from Software Catalog */ -test.describe( - 'Create Application from Devfile', - { tag: ['@dev-console', '@regression'] }, - () => { - const ns = `aut-addflow-devfile-${Date.now()}`; - let addPage: AddPage; - let gitPage: ImportFromGitPage; - let topologyPage: TopologyPage; +test.describe('Create Application from Devfile', { tag: ['@dev-console', '@regression'] }, () => { + const ns = `aut-addflow-devfile-${Date.now()}`; + let addPage: AddPage; + let gitPage: ImportFromGitPage; + let topologyPage: TopologyPage; - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - addPage = new AddPage(page); - gitPage = new ImportFromGitPage(page); - topologyPage = new TopologyPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); - }); + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + gitPage = new ImportFromGitPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); + }); - test('deploy git workload with devfile from add page [A-04-TC02]', async () => { - test.slow(); + test('deploy git workload with devfile from add page [A-04-TC02]', async () => { + test.slow(); - await test.step('Navigate to Import from Git form', async () => { - await addPage.clickImportFromGit(); - }); + await test.step('Navigate to Import from Git form', async () => { + await addPage.clickImportFromGit(); + }); - await test.step('Fill out the devfile import form', async () => { - await gitPage.enterGitRepoURL( - 'https://github.com/nodeshift-starters/devfile-sample', - ); - await gitPage.waitForGitValidation(); - await expect(gitPage.getDevfileDetectedHeading()).toBeVisible({ timeout: 30_000 }); - await gitPage.enterWorkloadName('node-example'); - await gitPage.clickCreate(); - }); + await test.step('Fill out the devfile import form', async () => { + await gitPage.enterGitRepoURL('https://github.com/nodeshift-starters/devfile-sample'); + await gitPage.waitForGitValidation(); + await expect(gitPage.getDevfileDetectedHeading()).toBeVisible({ timeout: 30_000 }); + await gitPage.enterWorkloadName('node-example'); + await gitPage.clickCreate(); + }); - await test.step('Verify workload appears in topology', async () => { - await topologyPage.waitForWorkload('node-example'); - await topologyPage.clickWorkload('node-example'); - await expect(topologyPage.getSidebarTitle()).toContainText('node-example', { timeout: 15_000 }); + await test.step('Verify workload appears in topology', async () => { + await topologyPage.waitForWorkload('node-example'); + await topologyPage.clickWorkload('node-example'); + await expect(topologyPage.getSidebarTitle()).toContainText('node-example', { + timeout: 30_000, }); }); - }, -); + }); +}); diff --git a/frontend/e2e/tests/dev-console/import-from-dockerfile.spec.ts b/frontend/e2e/tests/dev-console/import-from-dockerfile.spec.ts index 93056ca57c1..ea313cf0133 100644 --- a/frontend/e2e/tests/dev-console/import-from-dockerfile.spec.ts +++ b/frontend/e2e/tests/dev-console/import-from-dockerfile.spec.ts @@ -1,8 +1,5 @@ import { test, expect } from '../../fixtures'; -import { - AddPage, - ImportFromGitPage, -} from '../../pages/dev-console/add-page'; +import { AddPage, ImportFromGitPage } from '../../pages/dev-console/add-page'; import { TopologyPage } from '../../pages/topology-page'; /** @@ -37,17 +34,14 @@ test.describe( }); await test.step('Enter Dockerfile git URL', async () => { - await gitPage.enterGitRepoURL( - 'https://github.com/rohitkrai03/flask-dockerfile-example', - ); + await gitPage.enterGitRepoURL('https://github.com/rohitkrai03/flask-dockerfile-example'); await gitPage.waitForGitValidation(); }); await test.step('Verify auto-detected values', async () => { - await expect(gitPage.getAppNameInput()).toHaveValue( - 'flask-dockerfile-example-app', - { timeout: 15_000 }, - ); + await expect(gitPage.getAppNameInput()).toHaveValue('flask-dockerfile-example-app', { + timeout: 15_000, + }); await expect(gitPage.getNameInput()).toHaveValue('flask-dockerfile-example'); }); }); @@ -60,9 +54,7 @@ test.describe( }); await test.step('Enter URL and cancel', async () => { - await gitPage.enterGitRepoURL( - 'https://github.com/rohitkrai03/flask-dockerfile-example', - ); + await gitPage.enterGitRepoURL('https://github.com/rohitkrai03/flask-dockerfile-example'); await gitPage.waitForGitValidation(); await gitPage.selectResourceType('Deployment'); await gitPage.clickCancel(); @@ -81,9 +73,7 @@ test.describe( }); await test.step('Fill form with Dockerfile repo and create', async () => { - await gitPage.enterGitRepoURL( - 'https://github.com/rohitkrai03/flask-dockerfile-example', - ); + await gitPage.enterGitRepoURL('https://github.com/rohitkrai03/flask-dockerfile-example'); await gitPage.waitForGitValidation(); await gitPage.enterName('dockerfile-5000'); await gitPage.selectResourceType('Deployment'); diff --git a/frontend/e2e/tests/dev-console/import-from-git.spec.ts b/frontend/e2e/tests/dev-console/import-from-git.spec.ts index b0fcf0d52f9..8308bdb06b6 100644 --- a/frontend/e2e/tests/dev-console/import-from-git.spec.ts +++ b/frontend/e2e/tests/dev-console/import-from-git.spec.ts @@ -1,8 +1,5 @@ import { test, expect } from '../../fixtures'; -import { - AddPage, - ImportFromGitPage, -} from '../../pages/dev-console/add-page'; +import { AddPage, ImportFromGitPage } from '../../pages/dev-console/add-page'; import { TopologyPage } from '../../pages/topology-page'; /** @@ -20,105 +17,96 @@ import { TopologyPage } from '../../pages/topology-page'; * - A-06-TC17 (@broken-test) - Secure Route option */ -test.describe( - 'Create Application from git form', - { tag: ['@dev-console', '@regression'] }, - () => { - const ns = `aut-addflow-git-${Date.now()}`; - let addPage: AddPage; - let gitPage: ImportFromGitPage; - let topologyPage: TopologyPage; - - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - addPage = new AddPage(page); - gitPage = new ImportFromGitPage(page); - topologyPage = new TopologyPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); +test.describe('Create Application from git form', { tag: ['@dev-console', '@regression'] }, () => { + const ns = `aut-addflow-git-${Date.now()}`; + let addPage: AddPage; + let gitPage: ImportFromGitPage; + let topologyPage: TopologyPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + gitPage = new ImportFromGitPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); + }); + + test('cancel git workload creation [A-06-TC03]', async ({ page }) => { + await test.step('Navigate to Import from Git', async () => { + await addPage.clickImportFromGit(); }); - test('cancel git workload creation [A-06-TC03]', async ({ page }) => { - await test.step('Navigate to Import from Git', async () => { - await addPage.clickImportFromGit(); - }); - - await test.step('Enter git URL and cancel', async () => { - await gitPage.enterGitRepoURL('https://github.com/sclorg/dancer-ex.git'); - await gitPage.clickCancel(); - }); + await test.step('Enter git URL and cancel', async () => { + await gitPage.enterGitRepoURL('https://github.com/sclorg/dancer-ex.git'); + await gitPage.clickCancel(); + }); - await test.step('Verify redirect to Add page', async () => { - await expect(page).toHaveURL(new RegExp(`/add/ns/${ns}`), { timeout: 15_000 }); - await expect(addPage.getPageHeading()).toBeVisible(); - }); + await test.step('Verify redirect to Add page', async () => { + await expect(page).toHaveURL(new RegExp(`/add/ns/${ns}`), { timeout: 15_000 }); + await expect(addPage.getPageHeading()).toBeVisible(); }); + }); - test('create workload without application route [A-06-TC04]', async ({ k8sClient }) => { - test.slow(); + test('create workload without application route [A-06-TC04]', async ({ k8sClient }) => { + test.slow(); - await test.step('Navigate to Import from Git', async () => { - await addPage.clickImportFromGit(); - }); + await test.step('Navigate to Import from Git', async () => { + await addPage.clickImportFromGit(); + }); - await test.step('Fill form without route', async () => { - await gitPage.enterGitRepoURL('https://github.com/sclorg/dancer-ex.git'); - await gitPage.waitForGitValidation(); - await gitPage.enterApplicationName('app-no-route'); - await gitPage.enterName('name-no-route'); - await gitPage.uncheckCreateRoute(); - await gitPage.clickCreate(); - }); + await test.step('Fill form without route', async () => { + await gitPage.enterGitRepoURL('https://github.com/sclorg/dancer-ex.git'); + await gitPage.waitForGitValidation(); + await gitPage.enterApplicationName('app-no-route'); + await gitPage.enterName('name-no-route'); + await gitPage.uncheckCreateRoute(); + await gitPage.clickCreate(); + }); - await test.step('Verify workload in topology', async () => { - await topologyPage.waitForWorkload('name-no-route'); - await topologyPage.clickWorkload('name-no-route'); - await expect(topologyPage.getSidebarTitle()).toContainText('name-no-route', { timeout: 15_000 }); + await test.step('Verify workload in topology', async () => { + await topologyPage.waitForWorkload('name-no-route'); + await topologyPage.clickWorkload('name-no-route'); + await expect(topologyPage.getSidebarTitle()).toContainText('name-no-route', { + timeout: 15_000, }); + }); - await test.step('Verify no Route was created', async () => { - const routes = await k8sClient.listCustomResources( - 'route.openshift.io', - 'v1', - ns, - 'routes', - ); - const matching = (routes as any[]).filter( - (r) => r.metadata?.name === 'name-no-route', - ); - expect(matching).toHaveLength(0); - }); + await test.step('Verify no Route was created', async () => { + const routes = await k8sClient.listCustomResources('route.openshift.io', 'v1', ns, 'routes'); + const matching = (routes as any[]).filter((r) => r.metadata?.name === 'name-no-route'); + expect(matching).toHaveLength(0); }); + }); - test('disable devfile import strategy for non-standard git type [A-06-TC18]', async () => { - await test.step('Navigate to Import from Git', async () => { - await addPage.clickImportFromGit(); - }); + test('disable devfile import strategy for non-standard git type [A-06-TC18]', async () => { + await test.step('Navigate to Import from Git', async () => { + await addPage.clickImportFromGit(); + }); - await test.step('Enter non-standard git URL', async () => { - await gitPage.enterGitRepoURL('https://mysupersecretgit.example.com/org/repo'); - }); + await test.step('Enter non-standard git URL', async () => { + await gitPage.enterGitRepoURL('https://mysupersecretgit.example.com/org/repo'); + }); - await test.step('Verify devfile strategy is disabled', async () => { - await expect(gitPage.getDevfileStrategyDisabled()).toBeVisible({ timeout: 15_000 }); - }); + await test.step('Verify devfile strategy is disabled', async () => { + await expect(gitPage.getDevfileStrategyDisabled()).toBeVisible({ timeout: 15_000 }); }); + }); - test('devfile not detected warning [A-06-TC19]', async () => { - await test.step('Navigate to Import from Git', async () => { - await addPage.clickImportFromGit(); - }); + test('devfile not detected warning [A-06-TC19]', async () => { + await test.step('Navigate to Import from Git', async () => { + await addPage.clickImportFromGit(); + }); - await test.step('Enter devfile URL and invalid path', async () => { - await gitPage.enterGitRepoURL('https://github.com/nodeshift-starters/devfile-sample'); - await gitPage.waitForGitValidation(); - await gitPage.clickEditImportStrategy(); - await gitPage.enterDevfilePath('devfile1'); - }); + await test.step('Enter devfile URL and invalid path', async () => { + await gitPage.enterGitRepoURL('https://github.com/nodeshift-starters/devfile-sample'); + await gitPage.waitForGitValidation(); + await gitPage.clickEditImportStrategy(); + await gitPage.enterDevfilePath('devfile1'); + }); - await test.step('Verify devfile not detected message', async () => { - await expect(gitPage.getDevfileNotDetectedMessage()).toBeVisible({ timeout: 15_000 }); - }); + await test.step('Verify devfile not detected message', async () => { + await expect(gitPage.getDevfileNotDetectedMessage()).toBeVisible({ timeout: 15_000 }); }); - }, -); + }); +}); diff --git a/frontend/e2e/tests/dev-console/pinned-resources.spec.ts b/frontend/e2e/tests/dev-console/pinned-resources.spec.ts index 5a66d504c17..c19b2d21779 100644 --- a/frontend/e2e/tests/dev-console/pinned-resources.spec.ts +++ b/frontend/e2e/tests/dev-console/pinned-resources.spec.ts @@ -2,30 +2,26 @@ import { test, expect } from '../../fixtures'; import { ensureDeveloperPerspective, warmupSPA } from '../../pages/base-page'; import { AddPage } from '../../pages/dev-console/add-page'; -test.describe( - 'Configure pinned resources', - { tag: ['@dev-console', '@perspective'] }, - () => { - test( - 'CPR-01-TC01: Default pinned resources are visible in Developer perspective', - { tag: ['@smoke'] }, - async ({ page, k8sClient }) => { - await warmupSPA(page); - await ensureDeveloperPerspective(page, k8sClient); +test.describe('Configure pinned resources', { tag: ['@dev-console', '@perspective'] }, () => { + test( + 'CPR-01-TC01: Default pinned resources are visible in Developer perspective', + { tag: ['@smoke'] }, + async ({ page, k8sClient }) => { + await warmupSPA(page); + await ensureDeveloperPerspective(page, k8sClient); - const addPage = new AddPage(page); - await addPage.switchPerspective('Developer'); + const addPage = new AddPage(page); + await addPage.switchPerspective('Developer'); - await test.step('Verify Secrets is pinned in navigation', async () => { - await expect(addPage.getPinnedResource('Secrets')).toBeVisible({ - timeout: 30_000, - }); + await test.step('Verify Secrets is pinned in navigation', async () => { + await expect(addPage.getPinnedResource('Secrets')).toBeVisible({ + timeout: 30_000, }); + }); - await test.step('Verify ConfigMaps is pinned in navigation', async () => { - await expect(addPage.getPinnedResource('ConfigMaps')).toBeVisible(); - }); - }, - ); - }, -); + await test.step('Verify ConfigMaps is pinned in navigation', async () => { + await expect(addPage.getPinnedResource('ConfigMaps')).toBeVisible(); + }); + }, + ); +}); diff --git a/frontend/e2e/tests/dev-console/pod-list.spec.ts b/frontend/e2e/tests/dev-console/pod-list.spec.ts index 6dcc0400506..2ca6c97b278 100644 --- a/frontend/e2e/tests/dev-console/pod-list.spec.ts +++ b/frontend/e2e/tests/dev-console/pod-list.spec.ts @@ -27,38 +27,46 @@ async function createTestPod(k8sClient: KubernetesClient, ns: string): Promise { - test('shows Receiving Traffic column for a project', async ({ page, k8sClient, cleanup }) => { - const ns = `aut-pods-project-${Date.now()}`; - const podList = new PodListPage(page); +test.describe( + 'Pod list - Receiving Traffic column', + { tag: ['@dev-console', '@regression'] }, + () => { + test('shows Receiving Traffic column for a project', async ({ page, k8sClient, cleanup }) => { + const ns = `aut-pods-project-${Date.now()}`; + const podList = new PodListPage(page); - await test.step('Set up namespace with a pod', async () => { - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await createTestPod(k8sClient, ns); - }); + await test.step('Set up namespace with a pod', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await createTestPod(k8sClient, ns); + }); - await test.step('Enable Receiving Traffic column and verify', async () => { - await podList.navigateToPods(ns); - await podList.showReceivingTrafficColumn(); - await expect(podList.getColumnHeader('Receiving Traffic')).toBeVisible(); + await test.step('Enable Receiving Traffic column and verify', async () => { + await podList.navigateToPods(ns); + await podList.showReceivingTrafficColumn(); + await expect(podList.getColumnHeader('Receiving Traffic')).toBeVisible(); + }); }); - }); - test('shows Receiving Traffic column for all projects', async ({ page, k8sClient, cleanup }) => { - const ns = `aut-pods-all-${Date.now()}`; - const podList = new PodListPage(page); + test('shows Receiving Traffic column for all projects', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `aut-pods-all-${Date.now()}`; + const podList = new PodListPage(page); - await test.step('Set up namespace with a pod', async () => { - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await createTestPod(k8sClient, ns); - }); + await test.step('Set up namespace with a pod', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await createTestPod(k8sClient, ns); + }); - await test.step('Enable Receiving Traffic column and verify', async () => { - await podList.navigateToPodsAllProjects(); - await podList.showReceivingTrafficColumn(); - await expect(podList.getColumnHeader('Receiving Traffic')).toBeVisible(); + await test.step('Enable Receiving Traffic column and verify', async () => { + await podList.navigateToPodsAllProjects(); + await podList.showReceivingTrafficColumn(); + await expect(podList.getColumnHeader('Receiving Traffic')).toBeVisible(); + }); }); - }); -}); + }, +); diff --git a/frontend/e2e/tests/dev-console/quick-search.spec.ts b/frontend/e2e/tests/dev-console/quick-search.spec.ts index 9c99b2ae69d..eb2957533a7 100644 --- a/frontend/e2e/tests/dev-console/quick-search.spec.ts +++ b/frontend/e2e/tests/dev-console/quick-search.spec.ts @@ -10,76 +10,68 @@ import { AddPage } from '../../pages/dev-console/add-page'; * - A-11-TC07 (@regression) - Bindable resource (requires Service Binding + Crunchy Postgres operators) */ -test.describe( - 'Quick search in Add page', - { tag: ['@dev-console', '@regression'] }, - () => { - const ns = `aut-add-qs-${Date.now()}`; - let addPage: AddPage; +test.describe('Quick search in Add page', { tag: ['@dev-console', '@regression'] }, () => { + const ns = `aut-add-qs-${Date.now()}`; + let addPage: AddPage; - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - addPage = new AddPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await warmupSPA(page); - await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); - }); + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await warmupSPA(page); + await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); + }); - test('Add to project button shows search bar [A-11-TC01]', async () => { - await test.step('Click Add to project', async () => { - await addPage.clickAddToProject(); - }); + test('Add to project button shows search bar [A-11-TC01]', async () => { + await test.step('Click Add to project', async () => { + await addPage.clickAddToProject(); + }); - await test.step('Verify search bar is visible', async () => { - await expect(addPage.getQuickSearchInput()).toBeVisible({ timeout: 10_000 }); - }); + await test.step('Verify search bar is visible', async () => { + await expect(addPage.getQuickSearchInput()).toBeVisible({ timeout: 10_000 }); }); + }); - test('View all results option for search [A-11-TC03]', async ({ page }) => { - await test.step('Search for django', async () => { - await addPage.clickAddToProject(); - await addPage.getQuickSearchInput().fill('django'); - }); + test('View all results option for search [A-11-TC03]', async ({ page }) => { + await test.step('Search for django', async () => { + await addPage.clickAddToProject(); + await addPage.getQuickSearchInput().fill('django'); + }); - await test.step('Click view all and verify catalog', async () => { - const viewAllLink = addPage.getViewAllLink(); - await expect(viewAllLink).toBeVisible({ timeout: 10_000 }); - await viewAllLink.click(); - await expect(page).toHaveURL(/\/catalog\//, { timeout: 15_000 }); - }); + await test.step('Click view all and verify catalog', async () => { + const viewAllLink = addPage.getViewAllLink(); + await expect(viewAllLink).toBeVisible({ timeout: 10_000 }); + await viewAllLink.click(); + await expect(page).toHaveURL(/\/catalog\//, { timeout: 15_000 }); }); + }); - test('No results for invalid search [A-11-TC04]', async () => { - await test.step('Search for nonsense string', async () => { - await addPage.clickAddToProject(); - await addPage.getQuickSearchInput().fill('abcdef'); - }); + test('No results for invalid search [A-11-TC04]', async () => { + await test.step('Search for nonsense string', async () => { + await addPage.clickAddToProject(); + await addPage.getQuickSearchInput().fill('abcdef'); + }); - await test.step('Verify no results message', async () => { - await expect(addPage.getNoResultsMessage()).toBeVisible({ timeout: 10_000 }); - }); + await test.step('Verify no results message', async () => { + await expect(addPage.getNoResultsMessage()).toBeVisible({ timeout: 10_000 }); }); - }, -); + }); +}); -test.describe( - 'Quick search smoke tests', - { tag: ['@dev-console', '@smoke'] }, - () => { - const ns = `aut-add-qs-smoke-${Date.now()}`; - let addPage: AddPage; +test.describe('Quick search smoke tests', { tag: ['@dev-console', '@smoke'] }, () => { + const ns = `aut-add-qs-smoke-${Date.now()}`; + let addPage: AddPage; - test.beforeEach(async ({ page, k8sClient, cleanup }) => { - addPage = new AddPage(page); - await k8sClient.createNamespace(ns); - cleanup.trackNamespace(ns); - await warmupSPA(page); - await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); - }); + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await warmupSPA(page); + await addPage.ensureDevPerspectiveAndNavigate(ns, k8sClient); + }); - test('Add to project button shows search bar [A-11-TC01]', async () => { - await addPage.clickAddToProject(); - await expect(addPage.getQuickSearchInput()).toBeVisible({ timeout: 10_000 }); - }); - }, -); + test('Add to project button shows search bar [A-11-TC01]', async () => { + await addPage.clickAddToProject(); + await expect(addPage.getQuickSearchInput()).toBeVisible({ timeout: 10_000 }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/quick-starts.spec.ts b/frontend/e2e/tests/dev-console/quick-starts.spec.ts index b9883bbc37e..c3cb3bf33d6 100644 --- a/frontend/e2e/tests/dev-console/quick-starts.spec.ts +++ b/frontend/e2e/tests/dev-console/quick-starts.spec.ts @@ -15,12 +15,10 @@ test.describe('Quick Starts - Developer Perspective', { tag: ['@dev-console'] }, }); await test.step('Verify known quick starts are visible', async () => { - await expect( - quickStarts.getQuickStartCard('sample-application'), - ).toBeVisible({ timeout: 30_000 }); - await expect( - quickStarts.getQuickStartCard('add-healthchecks'), - ).toBeVisible(); + await expect(quickStarts.getQuickStartCard('sample-application')).toBeVisible({ + timeout: 30_000, + }); + await expect(quickStarts.getQuickStartCard('add-healthchecks')).toBeVisible(); }); await test.step('Verify duration info is shown on cards', async () => { diff --git a/frontend/e2e/tests/dev-console/route.spec.ts b/frontend/e2e/tests/dev-console/route.spec.ts index 13354795dfa..b9fc760e7de 100644 --- a/frontend/e2e/tests/dev-console/route.spec.ts +++ b/frontend/e2e/tests/dev-console/route.spec.ts @@ -102,29 +102,29 @@ test.describe('Route', { tag: ['@dev-console'] }, () => { }); }); - test('deletes route via Actions menu', { tag: ['@regression'] }, async ({ - page, - k8sClient, - cleanup, - }) => { - const ns = `aut-routes-delete-${Date.now()}`; - const routeName = 'test-route'; - const detailsPage = new DetailsPage(page); + test( + 'deletes route via Actions menu', + { tag: ['@regression'] }, + async ({ page, k8sClient, cleanup }) => { + const ns = `aut-routes-delete-${Date.now()}`; + const routeName = 'test-route'; + const detailsPage = new DetailsPage(page); - await test.step('Set up namespace and create route', async () => { - await createRoutePrerequisites(k8sClient, cleanup, ns); - await createTestRoute(k8sClient, ns, routeName); - }); + await test.step('Set up namespace and create route', async () => { + await createRoutePrerequisites(k8sClient, cleanup, ns); + await createTestRoute(k8sClient, ns, routeName); + }); - await test.step('Navigate to route details and delete', async () => { - await detailsPage.navigateToDetailsPage(`/k8s/ns/${ns}/routes/${routeName}`); - await expect(detailsPage.getHeadingByName(routeName)).toBeVisible({ timeout: 30_000 }); - await detailsPage.clickActionsMenuAction('Delete Route'); - }); + await test.step('Navigate to route details and delete', async () => { + await detailsPage.navigateToDetailsPage(`/k8s/ns/${ns}/routes/${routeName}`); + await expect(detailsPage.getHeadingByName(routeName)).toBeVisible({ timeout: 30_000 }); + await detailsPage.clickActionsMenuAction('Delete Route'); + }); - await test.step('Confirm deletion', async () => { - await detailsPage.confirmDelete(); - await expect(detailsPage.getHeadingByName('Routes')).toBeVisible({ timeout: 30_000 }); - }); - }); + await test.step('Confirm deletion', async () => { + await detailsPage.confirmDelete(); + await expect(detailsPage.getHeadingByName('Routes')).toBeVisible({ timeout: 30_000 }); + }); + }, + ); }); diff --git a/frontend/e2e/tests/dev-console/sample-app.spec.ts b/frontend/e2e/tests/dev-console/sample-app.spec.ts index 37518e38166..ae07c13230b 100644 --- a/frontend/e2e/tests/dev-console/sample-app.spec.ts +++ b/frontend/e2e/tests/dev-console/sample-app.spec.ts @@ -23,120 +23,124 @@ async function navigateToSamplesPage(addPage: AddPage, ns: string): Promise { - test.beforeEach(async ({ page, k8sClient }) => { - await warmupSPA(page); - await ensureDeveloperPerspective(page, k8sClient); - }); - - test( - 'GS-03-TC01: View all samples link navigates to Samples page', - { tag: ['@regression'] }, - async ({ page, k8sClient, cleanup }) => { - const ns = await createTestNamespace(k8sClient, cleanup, 'tc01'); - const addPage = new AddPage(page); - - await test.step('Navigate to Add page and verify samples link', async () => { - await addPage.switchPerspective('Developer'); - await addPage.navigateToAdd(ns); - await expect(addPage.getViewAllSamples()).toBeVisible({ timeout: 30_000 }); - }); - - await test.step('Click View all samples and verify Samples page', async () => { - await addPage.clickViewAllSamples(); - await expect(addPage.getPageHeading()).toContainText('Samples', { timeout: 30_000 }); - }); - - await test.step('Verify sample cards are visible', async () => { - await expect(addPage.getSampleCards().first()).toBeVisible({ timeout: 30_000 }); - }); - }, - ); - - test( - 'GS-03-TC02: Review sample application form for Httpd', - { tag: ['@regression'] }, - async ({ page, k8sClient, cleanup }) => { - const ns = await createTestNamespace(k8sClient, cleanup, 'tc02'); - const addPage = new AddPage(page); - - await test.step('Navigate to Samples page', async () => { - await navigateToSamplesPage(addPage, ns); - await expect(addPage.getPageHeading()).toContainText('Samples', { timeout: 30_000 }); - }); - - await test.step('Select Httpd sample', async () => { - await addPage.clickSampleCard('Httpd'); - await expect(addPage.getPageHeading()).toContainText('Create Sample application', { - timeout: 30_000, +test.describe( + 'Sample Application from Add page', + { tag: ['@dev-console', '@getting-started'] }, + () => { + test.beforeEach(async ({ page, k8sClient }) => { + await warmupSPA(page); + await ensureDeveloperPerspective(page, k8sClient); + }); + + test( + 'GS-03-TC01: View all samples link navigates to Samples page', + { tag: ['@regression'] }, + async ({ page, k8sClient, cleanup }) => { + const ns = await createTestNamespace(k8sClient, cleanup, 'tc01'); + const addPage = new AddPage(page); + + await test.step('Navigate to Add page and verify samples link', async () => { + await addPage.switchPerspective('Developer'); + await addPage.navigateToAdd(ns); + await expect(addPage.getViewAllSamples()).toBeVisible({ timeout: 30_000 }); }); - }); - - await test.step('Verify form has name field', async () => { - await expect(addPage.getFormAppName()).toBeVisible(); - }); - - await test.step('Verify builder image version dropdown', async () => { - await expect(addPage.getBuilderImageVersionToggle()).toBeVisible(); - }); - - await test.step('Verify git URL is present and disabled', async () => { - const gitInput = addPage.getGitUrlInput(); - await expect(gitInput).toBeVisible(); - await expect(gitInput).toBeDisabled(); - }); - - await test.step('Verify Create and Cancel buttons', async () => { - await expect(addPage.getSubmitButton()).toBeVisible(); - await expect(addPage.getCancelButton()).toBeVisible(); - }); - }, - ); - - test( - 'GS-03-TC03: Edit sample application form for Go', - { tag: ['@regression'] }, - async ({ page, k8sClient, cleanup }) => { - const ns = await createTestNamespace(k8sClient, cleanup, 'tc03'); - const addPage = new AddPage(page); - - await test.step('Navigate to Samples page and select Go', async () => { - await navigateToSamplesPage(addPage, ns); - await expect(addPage.getPageHeading()).toContainText('Samples', { timeout: 30_000 }); - await addPage.clickSampleCard('Go'); - await expect(addPage.getPageHeading()).toContainText('Create Sample application', { - timeout: 30_000, + + await test.step('Click View all samples and verify Samples page', async () => { + await addPage.clickViewAllSamples(); + await expect(addPage.getPageHeading()).toContainText('Samples', { timeout: 30_000 }); + }); + + await test.step('Verify sample cards are visible', async () => { + await expect(addPage.getSampleCards().first()).toBeVisible({ timeout: 30_000 }); + }); + }, + ); + + test( + 'GS-03-TC02: Review sample application form for Httpd', + { tag: ['@regression'] }, + async ({ page, k8sClient, cleanup }) => { + const ns = await createTestNamespace(k8sClient, cleanup, 'tc02'); + const addPage = new AddPage(page); + + await test.step('Navigate to Samples page', async () => { + await navigateToSamplesPage(addPage, ns); + await expect(addPage.getPageHeading()).toContainText('Samples', { timeout: 30_000 }); + }); + + await test.step('Select Httpd sample', async () => { + await addPage.clickSampleCard('Httpd'); + await expect(addPage.getPageHeading()).toContainText('Create Sample application', { + timeout: 30_000, + }); + }); + + await test.step('Verify form has name field', async () => { + await expect(addPage.getFormAppName()).toBeVisible(); + }); + + await test.step('Verify builder image version dropdown', async () => { + await expect(addPage.getBuilderImageVersionToggle()).toBeVisible(); }); - }); - - await test.step('Verify name can be edited', async () => { - const nameInput = addPage.getFormAppName(); - await expect(nameInput).toBeVisible(); - await nameInput.clear(); - await nameInput.fill('golang-sample-app1'); - await expect(nameInput).toHaveValue('golang-sample-app1'); - }); - - await test.step('Verify builder image version can be changed', async () => { - const versionToggle = addPage.getBuilderImageVersionToggle(); - await expect(versionToggle).toBeVisible(); - await versionToggle.click(); - const latestOption = addPage.getBuilderImageVersionItem('latest'); - await expect(latestOption).toBeVisible(); - }); - - // Note: This test does not submit the form or verify the application appears in topology. - // Full E2E flow verification is deferred to a future batch. - }, - ); - - // eslint-disable-next-line playwright/expect-expect - test('GS-03-TC04: Submit sample application form — placeholder', async () => { - test.skip(true, 'Deferred to a future batch'); - }); - - // eslint-disable-next-line playwright/expect-expect - test('GS-03-TC05: Verify application in topology — placeholder', async () => { - test.skip(true, 'Deferred to a future batch'); - }); -}); + + await test.step('Verify git URL is present and disabled', async () => { + const gitInput = addPage.getGitUrlInput(); + await expect(gitInput).toBeVisible(); + await expect(gitInput).toBeDisabled(); + }); + + await test.step('Verify Create and Cancel buttons', async () => { + await expect(addPage.getSubmitButton()).toBeVisible(); + await expect(addPage.getCancelButton()).toBeVisible(); + }); + }, + ); + + test( + 'GS-03-TC03: Edit sample application form for Go', + { tag: ['@regression'] }, + async ({ page, k8sClient, cleanup }) => { + const ns = await createTestNamespace(k8sClient, cleanup, 'tc03'); + const addPage = new AddPage(page); + + await test.step('Navigate to Samples page and select Go', async () => { + await navigateToSamplesPage(addPage, ns); + await expect(addPage.getPageHeading()).toContainText('Samples', { timeout: 30_000 }); + await addPage.clickSampleCard('Go'); + await expect(addPage.getPageHeading()).toContainText('Create Sample application', { + timeout: 30_000, + }); + }); + + await test.step('Verify name can be edited', async () => { + const nameInput = addPage.getFormAppName(); + await expect(nameInput).toBeVisible(); + await nameInput.clear(); + await nameInput.fill('golang-sample-app1'); + await expect(nameInput).toHaveValue('golang-sample-app1'); + }); + + await test.step('Verify builder image version can be changed', async () => { + const versionToggle = addPage.getBuilderImageVersionToggle(); + await expect(versionToggle).toBeVisible(); + await versionToggle.click(); + const latestOption = addPage.getBuilderImageVersionItem('latest'); + await expect(latestOption).toBeVisible(); + }); + + // Note: This test does not submit the form or verify the application appears in topology. + // Full E2E flow verification is deferred to a future batch. + }, + ); + + // eslint-disable-next-line playwright/expect-expect + test('GS-03-TC04: Submit sample application form — placeholder', async () => { + test.skip(true, 'Deferred to a future batch'); + }); + + // eslint-disable-next-line playwright/expect-expect + test('GS-03-TC05: Verify application in topology — placeholder', async () => { + test.skip(true, 'Deferred to a future batch'); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/user-preferences.spec.ts b/frontend/e2e/tests/dev-console/user-preferences.spec.ts index 3d8f88de53a..1cdbd40b68f 100644 --- a/frontend/e2e/tests/dev-console/user-preferences.spec.ts +++ b/frontend/e2e/tests/dev-console/user-preferences.spec.ts @@ -14,16 +14,12 @@ test.describe('User Preferences', { tag: ['@dev-console'] }, () => { test.afterEach(async ({ k8sClient }) => { try { - await k8sClient.patchConfigMap( - 'user-settings-kubeadmin', - 'openshift-console-user-settings', - { - 'console.preferredPerspective': '', - 'console.preferredCreateEditMethod': '', - 'topology.preferredView': '', - 'devconsole.preferredResource': '', - }, - ); + await k8sClient.patchConfigMap('user-settings-kubeadmin', 'openshift-console-user-settings', { + 'console.preferredPerspective': '', + 'console.preferredCreateEditMethod': '', + 'topology.preferredView': '', + 'devconsole.preferredResource': '', + }); } catch { // ConfigMap may not exist on fresh clusters where kubeadmin has no user-settings yet } @@ -66,7 +62,6 @@ test.describe('User Preferences', { tag: ['@dev-console'] }, () => { const perspectiveToggle = userPrefs.getPerspectiveSwitcherToggle(); await expect(perspectiveToggle).toContainText('Developer', { timeout: 30_000 }); }); - }, ); @@ -94,7 +89,6 @@ test.describe('User Preferences', { tag: ['@dev-console'] }, () => { await test.step('Verify graph view is active', async () => { await expect(userPrefs.getTopologyCanvas()).toBeVisible({ timeout: 30_000 }); }); - }, ); @@ -111,10 +105,7 @@ test.describe('User Preferences', { tag: ['@dev-console'] }, () => { await test.step('Set create/edit resource method to YAML', async () => { await userPrefs.navigateToPreferences(); - await userPrefs.selectPreferenceOption( - 'console.preferredCreateEditMethod', - 'YAML', - ); + await userPrefs.selectPreferenceOption('console.preferredCreateEditMethod', 'YAML'); }); await test.step('Navigate to a create form and verify YAML view', async () => { @@ -125,7 +116,6 @@ test.describe('User Preferences', { tag: ['@dev-console'] }, () => { const yamlRadio = userPrefs.getEditorRadio('YAML view'); await expect(yamlRadio).toBeChecked(); }); - }, ); @@ -140,10 +130,7 @@ test.describe('User Preferences', { tag: ['@dev-console'] }, () => { }); await test.step('Set resource type to DeploymentConfig', async () => { - await userPrefs.selectPreferenceOption( - 'devconsole.preferredResource', - 'DeploymentConfig', - ); + await userPrefs.selectPreferenceOption('devconsole.preferredResource', 'DeploymentConfig'); }); await test.step('Verify DeploymentConfig is selected', async () => { diff --git a/frontend/e2e/tests/dev-console/vulnerability.spec.ts b/frontend/e2e/tests/dev-console/vulnerability.spec.ts index 22866a27c51..0c9e439cdcb 100644 --- a/frontend/e2e/tests/dev-console/vulnerability.spec.ts +++ b/frontend/e2e/tests/dev-console/vulnerability.spec.ts @@ -19,7 +19,12 @@ async function waitForIMVData( const start = Date.now(); while (Date.now() - start < timeoutMs) { try { - const items = await k8sClient.listCustomResources(IMV_GROUP, IMV_VERSION, namespace, IMV_PLURAL); + const items = await k8sClient.listCustomResources( + IMV_GROUP, + IMV_VERSION, + namespace, + IMV_PLURAL, + ); if (items.length > 0) return; } catch { // CRD may not be registered yet — retry @@ -55,23 +60,25 @@ test.describe( metadata: { labels: { app: 'quarkus' } }, spec: { automountServiceAccountToken: false, - containers: [{ - name: 'quarkus', - image: 'quay.io/redhat-appstudio-qe/quarkus:7dd062e2e8cb4ba599185e48d628b65a', - command: ['sleep', 'infinity'], - ports: [{ containerPort: 8080 }], - resources: { - requests: { cpu: '10m', memory: '32Mi' }, - limits: { cpu: '100m', memory: '128Mi' }, + containers: [ + { + name: 'quarkus', + image: 'quay.io/redhat-appstudio-qe/quarkus:7dd062e2e8cb4ba599185e48d628b65a', + command: ['sleep', 'infinity'], + ports: [{ containerPort: 8080 }], + resources: { + requests: { cpu: '10m', memory: '32Mi' }, + limits: { cpu: '100m', memory: '128Mi' }, + }, + securityContext: { + allowPrivilegeEscalation: false, + readOnlyRootFilesystem: true, + runAsNonRoot: true, + seccompProfile: { type: 'RuntimeDefault' }, + capabilities: { drop: ['ALL'] }, + }, }, - securityContext: { - allowPrivilegeEscalation: false, - readOnlyRootFilesystem: true, - runAsNonRoot: true, - seccompProfile: { type: 'RuntimeDefault' }, - capabilities: { drop: ['ALL'] }, - }, - }], + ], }, }, }, @@ -91,9 +98,7 @@ test.describe( } }); - test('PV-01-TC01: Vulnerability tab shows Image Manifest vulnerabilities', async ({ - page, - }) => { + test('PV-01-TC01: Vulnerability tab shows Image Manifest vulnerabilities', async ({ page }) => { const vulnPage = new VulnerabilityPage(page); await test.step('Navigate to project overview and click Vulnerabilities tab', async () => { @@ -223,7 +228,15 @@ test.describe( await test.step('Verify severity filter is available', async () => { await vulnPage.clickRowFilterDropdown(); - for (const severity of ['Defcon1', 'Critical', 'High', 'Medium', 'Low', 'Negligible', 'Unknown']) { + for (const severity of [ + 'Defcon1', + 'Critical', + 'High', + 'Medium', + 'Low', + 'Negligible', + 'Unknown', + ]) { await expect(vulnPage.getRowFilterOption(severity)).toBeVisible(); } }); diff --git a/frontend/e2e/tests/knative/serverless/knative-ci.spec.ts b/frontend/e2e/tests/knative/serverless/knative-ci.spec.ts index 82cf1dcb059..2f02f9012b4 100644 --- a/frontend/e2e/tests/knative/serverless/knative-ci.spec.ts +++ b/frontend/e2e/tests/knative/serverless/knative-ci.spec.ts @@ -10,403 +10,426 @@ import KubernetesClient from '../../../clients/kubernetes-client'; const SERVICE_NAME = 'kn-service'; const GIT_URL = 'https://github.com/sclorg/nodejs-ex.git'; -test.describe( - 'Knative CI smoke tests', - { tag: ['@smoke', '@regression'] }, - () => { - test.describe.configure({ mode: 'serial' }); - - let k8sClient: KubernetesClient; - let namespace: string; - - test.beforeAll(async ({ k8sClient: client }) => { - k8sClient = client; - namespace = `aut-knative-ci-${Date.now()}`; - await k8sClient.createNamespace(namespace); - // OCP 5.0: knative queue-proxy sidecar lacks seccompProfile, relax PodSecurity - try { - execFileSync('oc', [ - 'label', 'namespace', namespace, +test.describe('Knative CI smoke tests', { tag: ['@smoke', '@regression'] }, () => { + test.describe.configure({ mode: 'serial' }); + + let k8sClient: KubernetesClient; + let namespace: string; + + test.beforeAll(async ({ k8sClient: client }) => { + k8sClient = client; + namespace = `aut-knative-ci-${Date.now()}`; + await k8sClient.createNamespace(namespace); + // OCP 5.0: knative queue-proxy sidecar lacks seccompProfile, relax PodSecurity + try { + execFileSync( + 'oc', + [ + 'label', + 'namespace', + namespace, 'pod-security.kubernetes.io/enforce=privileged', 'pod-security.kubernetes.io/warn=privileged', 'pod-security.kubernetes.io/audit=privileged', 'security.openshift.io/scc.podSecurityLabelSync=false', '--overwrite', - ], { encoding: 'utf-8', timeout: 10_000 }); - } catch { /* ignore on OCP 4 */ } + ], + { encoding: 'utf-8', timeout: 10_000 }, + ); + } catch { + /* ignore on OCP 4 */ + } + }); + + test.beforeEach(async ({ page }) => { + await warmupSPA(page); + }); + + test.afterAll(async () => { + await k8sClient.deleteNamespace(namespace); + }); + + test('KN-05-TC04: Create knative workload from Git', async ({ page }) => { + test.setTimeout(180_000); + const addFlowPage = new AddFlowPage(page); + const topologyPage = new TopologyKnativePage(page); + + await test.step('Navigate to Add page and import from Git', async () => { + await addFlowPage.navigateToAddPage(namespace); + await addFlowPage.clickImportFromGitCard(); }); - test.beforeEach(async ({ page }) => { - await warmupSPA(page); + await test.step('Enter Git URL and configure workload', async () => { + await addFlowPage.enterGitUrl(GIT_URL); + await addFlowPage.selectBuilderImage('Node.js'); + await addFlowPage.enterComponentName(SERVICE_NAME); + await addFlowPage.selectServerlessDeployment(); }); - test.afterAll(async () => { - await k8sClient.deleteNamespace(namespace); + await test.step('Submit and verify topology', async () => { + await addFlowPage.clickCreate(); + await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); }); - test('KN-05-TC04: Create knative workload from Git', async ({ page }) => { - test.setTimeout(180_000); - const addFlowPage = new AddFlowPage(page); - const topologyPage = new TopologyKnativePage(page); - - await test.step('Navigate to Add page and import from Git', async () => { - await addFlowPage.navigateToAddPage(namespace); - await addFlowPage.clickImportFromGitCard(); - }); - - await test.step('Enter Git URL and configure workload', async () => { - await addFlowPage.enterGitUrl(GIT_URL); - await addFlowPage.selectBuilderImage('Node.js'); - await addFlowPage.enterComponentName(SERVICE_NAME); - await addFlowPage.selectServerlessDeployment(); - }); - - await test.step('Submit and verify topology', async () => { - await addFlowPage.clickCreate(); - await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); - }); - - await test.step('Verify workload visible in topology', async () => { - await topologyPage.verifyWorkloadVisible(SERVICE_NAME); - }); + await test.step('Verify workload visible in topology', async () => { + await topologyPage.verifyWorkloadVisible(SERVICE_NAME); }); + }); - test('KN-02-TC02: Edit labels modal details', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); - - await test.step('Right-click service and select Edit labels', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.rightClickAndSelectAction(SERVICE_NAME, 'Edit labels'); - }); + test('KN-02-TC02: Edit labels modal details', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); - await test.step('Verify modal with save and cancel buttons', async () => { - await expect(topologyPage.getModalTitle()).toContainText('Edit labels'); - await topologyPage.verifyConfirmActionVisible(); - await expect(topologyPage.getModalCancel()).toBeVisible(); - await topologyPage.getModalCancel().click(); - }); + await test.step('Right-click service and select Edit labels', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.rightClickAndSelectAction(SERVICE_NAME, 'Edit labels'); }); - test('KN-02-TC17: Edit Annotation modal details', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); + await test.step('Verify modal with save and cancel buttons', async () => { + await expect(topologyPage.getModalTitle()).toContainText('Edit labels'); + await topologyPage.verifyConfirmActionVisible(); + await expect(topologyPage.getModalCancel()).toBeVisible(); + await topologyPage.getModalCancel().click(); + }); + }); - await test.step('Right-click service and select Edit annotations', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.rightClickAndSelectAction(SERVICE_NAME, 'Edit annotations'); - }); + test('KN-02-TC17: Edit Annotation modal details', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); - await test.step('Verify modal content', async () => { - await expect(topologyPage.getModalTitle()).toContainText('Edit annotations'); - await expect(page.getByTestId('pairs-list-name').first()).toBeVisible(); - await expect(page.getByTestId('pairs-list-value').first()).toBeVisible(); - await expect(page.getByTestId('add-button')).toBeVisible(); - await topologyPage.verifyConfirmActionVisible(); - await expect(topologyPage.getModalCancel()).toBeVisible(); - await topologyPage.getModalCancel().click(); - }); + await test.step('Right-click service and select Edit annotations', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.rightClickAndSelectAction(SERVICE_NAME, 'Edit annotations'); }); - test('KA-01-TC01: Create new Event Source via Ping Source', async ({ page }) => { - test.setTimeout(360_000); - const eventingPage = new AdminEventingPage(page); + await test.step('Verify modal content', async () => { + await expect(topologyPage.getModalTitle()).toContainText('Edit annotations'); + await expect(page.getByTestId('pairs-list-name').first()).toBeVisible(); + await expect(page.getByTestId('pairs-list-value').first()).toBeVisible(); + await expect(page.getByTestId('add-button')).toBeVisible(); + await topologyPage.verifyConfirmActionVisible(); + await expect(topologyPage.getModalCancel()).toBeVisible(); + await topologyPage.getModalCancel().click(); + }); + }); - await test.step('Wait for knative service to be ready', async () => { - await expect(async () => { - const svc = await k8sClient.customObjectsApi.getNamespacedCustomObject({ - group: 'serving.knative.dev', version: 'v1', - namespace, plural: 'services', name: SERVICE_NAME, - }) as { status?: { conditions?: Array<{ type: string; status: string }> } }; - const ready = svc?.status?.conditions?.find((c) => c.type === 'Ready'); - expect(ready?.status).toBe('True'); - }).toPass({ timeout: 300_000, intervals: [10_000] }); - }); + test('KA-01-TC01: Create new Event Source via Ping Source', async ({ page }) => { + test.setTimeout(360_000); + const eventingPage = new AdminEventingPage(page); - await test.step('Navigate to Eventing page', async () => { - await eventingPage.navigateToEventing(namespace); - }); + await test.step('Wait for knative service to be ready', async () => { + await expect(async () => { + const svc = (await k8sClient.customObjectsApi.getNamespacedCustomObject({ + group: 'serving.knative.dev', + version: 'v1', + namespace, + plural: 'services', + name: SERVICE_NAME, + })) as { status?: { conditions?: Array<{ type: string; status: string }> } }; + const ready = svc?.status?.conditions?.find((c) => c.type === 'Ready'); + expect(ready?.status).toBe('True'); + }).toPass({ timeout: 300_000, intervals: [10_000] }); + }); - await test.step('Click Create dropdown and select Event Source', async () => { - await eventingPage.selectCreateOption('eventSource'); - }); + await test.step('Navigate to Eventing page', async () => { + await eventingPage.navigateToEventing(namespace); + }); - await test.step('Select Ping Source', async () => { - await eventingPage.selectPingSourceAndCreate(); - }); + await test.step('Click Create dropdown and select Event Source', async () => { + await eventingPage.selectCreateOption('eventSource'); + }); - await test.step('Fill Ping Source form and submit', async () => { - // TODO: Use URI sink type as workaround — the Resource dropdown has a known bug - // on OCP 5 (OCPBUGS-95058) where it shows "Error loading - Select resource" - await eventingPage.fillPingSourceForm( - 'Message', - '* * * * *', - `http://${SERVICE_NAME}.${namespace}.svc.cluster.local`, - ); - await eventingPage.submitForm(); - await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); - }); + await test.step('Select Ping Source', async () => { + await eventingPage.selectPingSourceAndCreate(); + }); - await test.step('Verify event source visible in topology', async () => { - const topologyPage = new TopologyKnativePage(page); - await topologyPage.verifyWorkloadVisible('ping-source'); - }); + await test.step('Fill Ping Source form and submit', async () => { + // TODO: Use URI sink type as workaround — the Resource dropdown has a known bug + // on OCP 5 (OCPBUGS-95058) where it shows "Error loading - Select resource" + await eventingPage.fillPingSourceForm( + 'Message', + '* * * * *', + `http://${SERVICE_NAME}.${namespace}.svc.cluster.local`, + ); + await eventingPage.submitForm(); + await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); }); - test('KA-01-TC02: Create new Channel via default channel type', async ({ page }) => { - const eventingPage = new AdminEventingPage(page); + await test.step('Verify event source visible in topology', async () => { + const topologyPage = new TopologyKnativePage(page); + await topologyPage.verifyWorkloadVisible('ping-source'); + }); + }); - await test.step('Navigate to Eventing page', async () => { - await eventingPage.navigateToEventing(namespace); - }); + test('KA-01-TC02: Create new Channel via default channel type', async ({ page }) => { + const eventingPage = new AdminEventingPage(page); - await test.step('Click Create dropdown and select Channel', async () => { - await eventingPage.selectCreateOption('channels'); - }); + await test.step('Navigate to Eventing page', async () => { + await eventingPage.navigateToEventing(namespace); + }); - await test.step('Select Default Channel and create', async () => { - await eventingPage.createChannel('Default Channel'); - }); + await test.step('Click Create dropdown and select Channel', async () => { + await eventingPage.selectCreateOption('channels'); + }); - await test.step('Verify channel visible in Topology', async () => { - await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); - const topologyPage = new TopologyKnativePage(page); - await topologyPage.verifyWorkloadVisible('channel'); - }); + await test.step('Select Default Channel and create', async () => { + await eventingPage.createChannel('Default Channel'); }); - test('KE-05-TC01: Create Broker using Form view', async ({ page }) => { - const eventingPage = new AdminEventingPage(page); + await test.step('Verify channel visible in Topology', async () => { + await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); + const topologyPage = new TopologyKnativePage(page); + await topologyPage.verifyWorkloadVisible('channel'); + }); + }); - await test.step('Navigate to Eventing page', async () => { - await eventingPage.navigateToEventing(namespace); - }); + test('KE-05-TC01: Create Broker using Form view', async ({ page }) => { + const eventingPage = new AdminEventingPage(page); - await test.step('Click Create dropdown and select Broker', async () => { - await eventingPage.selectCreateOption('brokers'); - }); + await test.step('Navigate to Eventing page', async () => { + await eventingPage.navigateToEventing(namespace); + }); - await test.step('Select Form view, enter name and create', async () => { - await eventingPage.createBroker('default-broker'); - }); + await test.step('Click Create dropdown and select Broker', async () => { + await eventingPage.selectCreateOption('brokers'); + }); - await test.step('Verify broker visible in Topology', async () => { - await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); - const topologyPage = new TopologyKnativePage(page); - await topologyPage.verifyWorkloadVisible('default-broker'); - }); + await test.step('Select Form view, enter name and create', async () => { + await eventingPage.createBroker('default-broker'); }); - // TODO: The Add Subscription UI modal has a broken Subscriber dropdown on OCP 5 - // (OCPBUGS-95058). The dropdown shows "Error loading - Select Subscriber" and cannot - // list Knative Services. Works on OCP 4. Creating subscription via API as workaround. - test('Add Subscription to channel', async ({ page }) => { + await test.step('Verify broker visible in Topology', async () => { + await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); const topologyPage = new TopologyKnativePage(page); - - await test.step('Create subscription via API', async () => { - await k8sClient.customObjectsApi.createNamespacedCustomObject({ - group: 'messaging.knative.dev', - version: 'v1', - namespace, - plural: 'subscriptions', - body: { - apiVersion: 'messaging.knative.dev/v1', - kind: 'Subscription', - metadata: { name: 'channel-subscrip', namespace }, - spec: { - channel: { - apiVersion: 'messaging.knative.dev/v1', - kind: 'Channel', - name: 'channel', - }, - subscriber: { - ref: { - apiVersion: 'serving.knative.dev/v1', - kind: 'Service', - name: SERVICE_NAME, - }, + await topologyPage.verifyWorkloadVisible('default-broker'); + }); + }); + + // TODO: The Add Subscription UI modal has a broken Subscriber dropdown on OCP 5 + // (OCPBUGS-95058). The dropdown shows "Error loading - Select Subscriber" and cannot + // list Knative Services. Works on OCP 4. Creating subscription via API as workaround. + test('Add Subscription to channel', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); + + await test.step('Create subscription via API', async () => { + await k8sClient.customObjectsApi.createNamespacedCustomObject({ + group: 'messaging.knative.dev', + version: 'v1', + namespace, + plural: 'subscriptions', + body: { + apiVersion: 'messaging.knative.dev/v1', + kind: 'Subscription', + metadata: { name: 'channel-subscrip', namespace }, + spec: { + channel: { + apiVersion: 'messaging.knative.dev/v1', + kind: 'Channel', + name: 'channel', + }, + subscriber: { + ref: { + apiVersion: 'serving.knative.dev/v1', + kind: 'Service', + name: SERVICE_NAME, }, }, }, - }); - }); - - await test.step('Verify subscriber in channel sidebar', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.clickOnTopologyNode('channel'); - await topologyPage.verifySidePaneOpen(); - await expect(topologyPage.getSidePane()).toContainText(SERVICE_NAME); - await topologyPage.closeSidePane(); + }, }); }); - test('KN-02-TC08: Update service to new application group', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); - - await test.step('Right-click and select Edit application grouping', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.rightClickAndSelectAction(SERVICE_NAME, 'Edit application grouping'); - }); - - await test.step('Create new application group', async () => { - await topologyPage.editApplicationGrouping('openshift-app'); - }); - - await test.step('Verify service is in new application group', async () => { - await topologyPage.verifyWorkloadVisible('openshift-app'); - await topologyPage.clickOnApplicationGrouping('openshift-app'); - await topologyPage.verifySidePaneOpen(); - await expect(topologyPage.getSidePane()).toContainText(SERVICE_NAME); - }); + await test.step('Verify subscriber in channel sidebar', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.clickOnTopologyNode('channel'); + await topologyPage.verifySidePaneOpen(); + await expect(topologyPage.getSidePane()).toContainText(SERVICE_NAME); + await topologyPage.closeSidePane(); }); + }); - test('KN-01-TC12: Delete Revision not possible for single revision', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); + test('KN-02-TC08: Update service to new application group', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); - await test.step('Right-click revision and select Delete Revision', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.rightClickRevisionAndSelectAction(SERVICE_NAME, 'Delete Revision'); - }); - - await test.step('Verify unable-to-delete modal', async () => { - await expect(page.getByText('Unable to delete Revision')).toBeVisible({ timeout: 30_000 }); - await expect(page.getByText('You cannot delete the last Revision for the Service.')).toBeVisible(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); - }); + await test.step('Right-click and select Edit application grouping', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.rightClickAndSelectAction(SERVICE_NAME, 'Edit application grouping'); }); - test('Create Revision for existing knative Service', async ({ page }) => { - test.setTimeout(300_000); - - await test.step('Edit service to create a new revision', async () => { - await page.goto(`/edit/ns/${namespace}?name=${SERVICE_NAME}&kind=serving.knative.dev~v1~Service`); - await page.waitForLoadState('load'); - await page.locator('button').filter({ hasText: 'Labels' }).click({ timeout: 30_000 }); - const labelsInput = page.getByTestId('labels'); - await labelsInput.fill('app=frontend'); - await labelsInput.press('Enter'); - await page.getByTestId('save-changes').click(); - await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); - }); - - await test.step('Verify multiple revisions via API', async () => { - await expect(async () => { - const revisions = (await k8sClient.customObjectsApi.listNamespacedCustomObject({ - group: 'serving.knative.dev', - version: 'v1', - namespace, - plural: 'revisions', - labelSelector: `serving.knative.dev/service=${SERVICE_NAME}`, - })) as { items?: Array }; - expect(revisions.items?.length).toBe(2); - }).toPass({ timeout: 60_000, intervals: [5_000] }); - }); + await test.step('Create new application group', async () => { + await topologyPage.editApplicationGrouping('openshift-app'); }); - test('KN-02-TC10: Set traffic distribution >100%', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); + await test.step('Verify service is in new application group', async () => { + await topologyPage.verifyWorkloadVisible('openshift-app'); + await topologyPage.clickOnApplicationGrouping('openshift-app'); + await topologyPage.verifySidePaneOpen(); + await expect(topologyPage.getSidePane()).toContainText(SERVICE_NAME); + }); + }); - await test.step('Open Set traffic distribution modal', async () => { - await topologyPage.openServiceAction(namespace, SERVICE_NAME, 'Set traffic distribution'); - }); + test('KN-01-TC12: Delete Revision not possible for single revision', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); - await test.step('Set traffic >100% and verify error', async () => { - await expect( - page.getByText('Set traffic distribution', { exact: true }), - ).toBeVisible({ timeout: 30_000 }); - await topologyPage.addTrafficTarget(); - await topologyPage.setTrafficPercent('last', '50'); - await topologyPage.selectTrafficTargetRevision(1); - await topologyPage.submitTrafficDistribution(); - await topologyPage.verifyTrafficDistributionError('Traffic targets sum to 150, want 100'); - }); + await test.step('Right-click revision and select Delete Revision', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.rightClickRevisionAndSelectAction(SERVICE_NAME, 'Delete Revision'); }); - test('KN-02-TC11: Set traffic distribution <100%', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); - - await test.step('Open Set traffic distribution modal', async () => { - await topologyPage.openServiceAction(namespace, SERVICE_NAME, 'Set traffic distribution'); - }); + await test.step('Verify unable-to-delete modal', async () => { + await expect(page.getByText('Unable to delete Revision')).toBeVisible({ timeout: 30_000 }); + await expect( + page.getByText('You cannot delete the last Revision for the Service.'), + ).toBeVisible(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + }); + }); + + test('Create Revision for existing knative Service', async ({ page }) => { + test.setTimeout(300_000); + + await test.step('Edit service to create a new revision', async () => { + await page.goto( + `/edit/ns/${namespace}?name=${SERVICE_NAME}&kind=serving.knative.dev~v1~Service`, + ); + await page.waitForLoadState('load'); + await page.locator('button').filter({ hasText: 'Labels' }).click({ timeout: 30_000 }); + const labelsInput = page.getByTestId('labels'); + await labelsInput.fill('app=frontend'); + await labelsInput.press('Enter'); + await page.getByTestId('save-changes').click(); + await expect(page).toHaveURL(/topology/, { timeout: 30_000 }); + }); - await test.step('Set traffic <100% and verify error', async () => { - await expect( - page.getByText('Set traffic distribution', { exact: true }), - ).toBeVisible({ timeout: 30_000 }); - await topologyPage.setTrafficPercent('first', '25'); - await topologyPage.addTrafficTarget(); - await topologyPage.setTrafficPercent('last', '50'); - await topologyPage.selectTrafficTargetRevision(1); - await topologyPage.submitTrafficDistribution(); - await topologyPage.verifyTrafficDistributionError('Traffic targets sum to 75, want 100'); - }); + await test.step('Verify multiple revisions via API', async () => { + await expect(async () => { + const revisions = (await k8sClient.customObjectsApi.listNamespacedCustomObject({ + group: 'serving.knative.dev', + version: 'v1', + namespace, + plural: 'revisions', + labelSelector: `serving.knative.dev/service=${SERVICE_NAME}`, + })) as { items?: Array }; + expect(revisions.items?.length).toBe(2); + }).toPass({ timeout: 60_000, intervals: [5_000] }); }); + }); - test('KE-05-TC11: Delete Broker', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); + test('KN-02-TC10: Set traffic distribution >100%', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); - await test.step('Delete broker via details page', async () => { - await topologyPage.openServiceAction(namespace, 'default-broker', 'Delete Broker', - 'eventing.knative.dev~v1~Broker'); - await expect(topologyPage.getModalTitle()).toContainText('Delete'); - await topologyPage.confirmModalSubmit(); - }); + await test.step('Open Set traffic distribution modal', async () => { + await topologyPage.openServiceAction(namespace, SERVICE_NAME, 'Set traffic distribution'); + }); - await test.step('Verify broker removed', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.verifyResourceRemoved('default-broker'); + await test.step('Set traffic >100% and verify error', async () => { + await expect(page.getByText('Set traffic distribution', { exact: true })).toBeVisible({ + timeout: 30_000, }); + await topologyPage.addTrafficTarget(); + await topologyPage.setTrafficPercent('last', '50'); + await topologyPage.selectTrafficTargetRevision(1); + await topologyPage.submitTrafficDistribution(); + await topologyPage.verifyTrafficDistributionError('Traffic targets sum to 150, want 100'); }); + }); - test('KE-06-TC16: Delete Channel', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); + test('KN-02-TC11: Set traffic distribution <100%', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); - await test.step('Delete channel via details page', async () => { - await topologyPage.openServiceAction(namespace, 'channel', 'Delete Channel', - 'messaging.knative.dev~v1~Channel'); - await expect(topologyPage.getModalTitle()).toContainText('Delete'); - await topologyPage.confirmModalSubmit(); - }); + await test.step('Open Set traffic distribution modal', async () => { + await topologyPage.openServiceAction(namespace, SERVICE_NAME, 'Set traffic distribution'); + }); - await test.step('Verify channel removed', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.verifyResourceRemoved('channel'); + await test.step('Set traffic <100% and verify error', async () => { + await expect(page.getByText('Set traffic distribution', { exact: true })).toBeVisible({ + timeout: 30_000, }); + await topologyPage.setTrafficPercent('first', '25'); + await topologyPage.addTrafficTarget(); + await topologyPage.setTrafficPercent('last', '50'); + await topologyPage.selectTrafficTargetRevision(1); + await topologyPage.submitTrafficDistribution(); + await topologyPage.verifyTrafficDistributionError('Traffic targets sum to 75, want 100'); + }); + }); + + test('KE-05-TC11: Delete Broker', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); + + await test.step('Delete broker via details page', async () => { + await topologyPage.openServiceAction( + namespace, + 'default-broker', + 'Delete Broker', + 'eventing.knative.dev~v1~Broker', + ); + await expect(topologyPage.getModalTitle()).toContainText('Delete'); + await topologyPage.confirmModalSubmit(); }); - test('KE-01-TC03: Delete event source', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); + await test.step('Verify broker removed', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.verifyResourceRemoved('default-broker'); + }); + }); + + test('KE-06-TC16: Delete Channel', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); + + await test.step('Delete channel via details page', async () => { + await topologyPage.openServiceAction( + namespace, + 'channel', + 'Delete Channel', + 'messaging.knative.dev~v1~Channel', + ); + await expect(topologyPage.getModalTitle()).toContainText('Delete'); + await topologyPage.confirmModalSubmit(); + }); - await test.step('Delete event source via details page', async () => { - await topologyPage.openServiceAction(namespace, 'ping-source', 'Delete PingSource', - 'sources.knative.dev~v1~PingSource'); - await expect(topologyPage.getModalTitle()).toContainText('Delete'); - await topologyPage.confirmModalSubmit(); - }); + await test.step('Verify channel removed', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.verifyResourceRemoved('channel'); + }); + }); + + test('KE-01-TC03: Delete event source', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); + + await test.step('Delete event source via details page', async () => { + await topologyPage.openServiceAction( + namespace, + 'ping-source', + 'Delete PingSource', + 'sources.knative.dev~v1~PingSource', + ); + await expect(topologyPage.getModalTitle()).toContainText('Delete'); + await topologyPage.confirmModalSubmit(); + }); - await test.step('Verify event source removed', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.verifyResourceRemoved('ping-source', 30_000); - }); + await test.step('Verify event source removed', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.verifyResourceRemoved('ping-source', 30_000); }); + }); - test('KN-02-TC16: Delete service', async ({ page }) => { - const topologyPage = new TopologyKnativePage(page); + test('KN-02-TC16: Delete service', async ({ page }) => { + const topologyPage = new TopologyKnativePage(page); - await test.step('Right-click service and select Delete Service', async () => { - await topologyPage.navigateToTopology(namespace); - await topologyPage.rightClickAndSelectAction(SERVICE_NAME, 'Delete Service'); - }); + await test.step('Right-click service and select Delete Service', async () => { + await topologyPage.navigateToTopology(namespace); + await topologyPage.rightClickAndSelectAction(SERVICE_NAME, 'Delete Service'); + }); - await test.step('Confirm deletion', async () => { - await expect(topologyPage.getModalTitle()).toContainText('Delete Service?'); - await topologyPage.confirmModalSubmit(); - }); + await test.step('Confirm deletion', async () => { + await expect(topologyPage.getModalTitle()).toContainText('Delete Service?'); + await topologyPage.confirmModalSubmit(); + }); - await test.step('Verify service removed', async () => { - await page.reload(); - await expect( - page.locator('[data-type="knative-service"]'), - ).not.toBeAttached({ timeout: 30_000 }); + await test.step('Verify service removed', async () => { + await page.reload(); + await expect(page.locator('[data-type="knative-service"]')).not.toBeAttached({ + timeout: 30_000, }); }); - }, -); + }); +}); diff --git a/frontend/e2e/tests/olm/create-namespace.spec.ts b/frontend/e2e/tests/olm/create-namespace.spec.ts index dde48221630..a2c34fc6546 100644 --- a/frontend/e2e/tests/olm/create-namespace.spec.ts +++ b/frontend/e2e/tests/olm/create-namespace.spec.ts @@ -57,7 +57,10 @@ test.describe('Create namespace from install operators', { tag: ['@admin'] }, () // OperatorHub catalog with an empty Software Catalog. Skip instead of timing out. await page.goto('/'); const isTechPreview = await page.evaluate(() => window.SERVER_FLAGS.techPreview); - test.skip(isTechPreview, 'OLMv1 is active on techPreview clusters — OLMv0 OperatorHub catalog is unavailable'); + test.skip( + isTechPreview, + 'OLMv1 is active on techPreview clusters — OLMv0 OperatorHub catalog is unavailable', + ); await test.step('Navigate to catalog and open operator details', async () => { await page.goto('/catalog/ns/default?catalogType=operator'); diff --git a/frontend/e2e/tests/olm/operator-lifecycle-metadata.spec.ts b/frontend/e2e/tests/olm/operator-lifecycle-metadata.spec.ts index 1a859d7c975..4a52bad44e2 100644 --- a/frontend/e2e/tests/olm/operator-lifecycle-metadata.spec.ts +++ b/frontend/e2e/tests/olm/operator-lifecycle-metadata.spec.ts @@ -97,9 +97,7 @@ test.describe('Operator lifecycle metadata', { tag: ['@admin'] }, () => { } if (!csv) { - throw new Error( - `Timed out waiting for ${PACKAGE_NAME} CSV to reach Succeeded phase`, - ); + throw new Error(`Timed out waiting for ${PACKAGE_NAME} CSV to reach Succeeded phase`); } operatorVersion = csv.spec?.version ?? ''; @@ -171,17 +169,18 @@ test.describe('Operator lifecycle metadata', { tag: ['@admin'] }, () => { ); await page.reload(); - await expect( - installedOperators.getOperatorRow(operatorDisplayName), - ).toBeVisible({ timeout: 30_000 }); + await expect(installedOperators.getOperatorRow(operatorDisplayName)).toBeVisible({ + timeout: 30_000, + }); - await expect( - installedOperators.getCompatibleIndicator(operatorDisplayName), - ).toContainText('Compatible', { timeout: 30_000 }); + await expect(installedOperators.getCompatibleIndicator(operatorDisplayName)).toContainText( + 'Compatible', + { timeout: 30_000 }, + ); - await expect( - installedOperators.getSupportPhaseBadge(operatorDisplayName), - ).toContainText('Maintenance support'); + await expect(installedOperators.getSupportPhaseBadge(operatorDisplayName)).toContainText( + 'Maintenance support', + ); }); await test.step('Unsupported when all phases expired', async () => { @@ -192,26 +191,28 @@ test.describe('Operator lifecycle metadata', { tag: ['@admin'] }, () => { ); await page.reload(); - await expect( - installedOperators.getOperatorRow(operatorDisplayName), - ).toBeVisible({ timeout: 30_000 }); + await expect(installedOperators.getOperatorRow(operatorDisplayName)).toBeVisible({ + timeout: 30_000, + }); - await expect( - installedOperators.getSelfSupportBadge(operatorDisplayName), - ).toContainText('Unsupported', { timeout: 30_000 }); + await expect(installedOperators.getSelfSupportBadge(operatorDisplayName)).toContainText( + 'Unsupported', + { timeout: 30_000 }, + ); }); await test.step('Incompatible when cluster version not in compatibility list', async () => { activeLifecycleData = makeLifecycleIncompatible(PACKAGE_NAME, operatorVersion); await page.reload(); - await expect( - installedOperators.getOperatorRow(operatorDisplayName), - ).toBeVisible({ timeout: 30_000 }); + await expect(installedOperators.getOperatorRow(operatorDisplayName)).toBeVisible({ + timeout: 30_000, + }); - await expect( - installedOperators.getIncompatibleIndicator(operatorDisplayName), - ).toContainText('Incompatible', { timeout: 30_000 }); + await expect(installedOperators.getIncompatibleIndicator(operatorDisplayName)).toContainText( + 'Incompatible', + { timeout: 30_000 }, + ); }); }); }); diff --git a/frontend/e2e/tests/smoke/developer/smoke-test.spec.ts b/frontend/e2e/tests/smoke/developer/smoke-test.spec.ts index f6392e4182e..4d7dcbd2006 100644 --- a/frontend/e2e/tests/smoke/developer/smoke-test.spec.ts +++ b/frontend/e2e/tests/smoke/developer/smoke-test.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from '../../../fixtures'; test('console loads in developer perspective', async ({ page }) => { await page.goto('/'); - await expect( - page.getByTestId('perspective-switcher-toggle'), - ).toContainText('Developer', { timeout: 60_000 }); + await expect(page.getByTestId('perspective-switcher-toggle')).toContainText('Developer', { + timeout: 60_000, + }); }); diff --git a/frontend/e2e/tests/topology/topology-ci.spec.ts b/frontend/e2e/tests/topology/topology-ci.spec.ts index 98aed438d60..671e6f2b984 100644 --- a/frontend/e2e/tests/topology/topology-ci.spec.ts +++ b/frontend/e2e/tests/topology/topology-ci.spec.ts @@ -10,10 +10,7 @@ import type { Page } from '@playwright/test'; const NS = `aut-topology-ci-${Date.now()}`; -const MOCK_DIR = path.join( - path.dirname(fileURLToPath(import.meta.url)), - 'testData', -); +const MOCK_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'testData'); const repoMock = JSON.parse(fs.readFileSync(path.join(MOCK_DIR, 'repo.json'), 'utf-8')); const contentsMock = JSON.parse(fs.readFileSync(path.join(MOCK_DIR, 'contents.json'), 'utf-8')); @@ -27,7 +24,7 @@ async function mockGitHubApi(page: Page, repoMock: any, contentsMock: any) { await route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify(repoMock) + body: JSON.stringify(repoMock), }); }); @@ -36,7 +33,7 @@ async function mockGitHubApi(page: Page, repoMock: any, contentsMock: any) { await route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify(contentsMock) + body: JSON.stringify(contentsMock), }); }); @@ -63,34 +60,34 @@ async function createWorkload(page: Page, workloadName: string) { await dismissQuickStartDrawer(page); await topology.switchPerspective('Administrator'); await topology.navigateToTopologyGraph(NS); - + await test.step('Open quick search and select .NET builder image', async () => { await topology.clickStartBuilding(); await topology.typeInQuickSearch('.NET'); await topology.clickBuilderImageItem('.NET SDK-Builder Images'); }); - + await test.step('Create application from quick search', async () => { await topology.selectBuilderImageFromList(/^\.NET SDK.*Builder Images$/); await topology.clickCreateButton(); }); - + await test.step('Mock GitHub API and fill git repo URL', async () => { await mockGitHubApi(page, repoMock, contentsMock); await topology.fillGitRepoUrl('https://github.com/redhat-developer/s2i-dotnetcore-ex'); await topology.waitForGitUrlValidation(); }); - + await test.step('Enter application and workload names', async () => { await topology.fillApplicationName(`${workloadName}-app`); await topology.fillWorkloadName(workloadName); }); - + await test.step('Select Deployment resource type and submit', async () => { await topology.selectResourceType('kubernetes'); await topology.clickSaveChanges(); }); - + await test.step('Verify workload appears in topology', async () => { // Wait for the workload to be visible with a longer timeout await topology.verifyWorkloadVisible(workloadName, 120_000); @@ -113,24 +110,24 @@ test.describe('Perform actions on topology', { tag: ['@smoke'] }, () => { await k8sClient.createNamespace(NS); await k8sClient.waitForNamespaceReady(NS); }); - + test.afterAll(async ({ k8sClient }) => { await k8sClient.deleteNamespace(NS); }); - + test('empty state of topology: T-06-TC01', async ({ page }) => { const topology = new TopologyPage(page); await page.goto('/'); await dismissQuickStartDrawer(page); await topology.switchPerspective('Administrator'); await topology.navigateToTopology(NS); - + await test.step('Verify empty state message and links', async () => { await expect(topology.getNoResourcesFound()).toBeVisible({ timeout: 30_000 }); await expect(topology.getStartBuildingLink()).toBeVisible(); await expect(topology.getAddPageLink()).toBeVisible(); }); - + await test.step('Verify controls are disabled', async () => { await expect(topology.getDisplayOptionsButton()).toBeDisabled(); await expect(topology.getFilterByResourceDropdown()).toBeDisabled(); @@ -138,87 +135,85 @@ test.describe('Perform actions on topology', { tag: ['@smoke'] }, () => { await expect(topology.getSwitcher()).toBeDisabled(); }); }); - + test('Build the application from topology page', async ({ page }) => { test.setTimeout(300_000); const topology = new TopologyPage(page); await createWorkload(page, 'dotnet-build-test'); - + await test.step('Clean up: Delete workload', async () => { await deleteWorkload(page, 'dotnet-build-test'); await expect(topology.getNoResourcesFound()).toBeVisible({ timeout: 60_000 }); }); }); - + test('Edit workload application groupings: T-09-TC01', async ({ page }) => { test.setTimeout(300_000); const topology = new TopologyPage(page); + const sidebar = new TopologySidebarPage(page); await createWorkload(page, 'dotnet-edit-test'); - - await test.step('Prepare for right-click: clear search and close any sidebar', async () => { - // Clear the search field to avoid interference - await topology.search(''); - // Close sidebar if open - await topology.closeSidebarIfOpen(); - }); - - await test.step('Right-click workload and select Edit', async () => { - await topology.rightClickOnNode('dotnet-edit-test'); - await topology.selectContextMenuAction('Edit dotnet-edit-test'); + + await test.step('Open workload sidebar and select Edit', async () => { + // Use the sidebar Actions menu rather than the topology right-click context + // menu: PF Topology nodes are SVG groups with no usable bounding box, so a + // real right-click cannot be dispatched reliably in headless runs. This is + // the same interaction path deleteWorkload uses. + await topology.clickOnNode('dotnet-edit-test'); + await sidebar.verify(); + await sidebar.selectAction('Edit dotnet-edit-test'); }); - + await test.step('Change application groupings to "app"', async () => { await topology.clickApplicationDropdown(); await topology.selectFirstApplicationOption(); await topology.fillApplicationName('app'); await topology.clickSaveChanges(); }); - + await test.step('Verify application grouping changed', async () => { - // Verify the workload still exists await topology.verifyWorkloadVisible('dotnet-edit-test', 60_000); await topology.verifyGroupLabel('dotnet-edit-test', 'app', 5_000); - + await expect(topology.getGraphSurface()).toBeAttached(); }); - + await test.step('Clean up: Delete workload', async () => { await deleteWorkload(page, 'dotnet-edit-test'); await expect(topology.getNoResourcesFound()).toBeVisible({ timeout: 60_000 }); }); }); - + test('Default state of Display dropdown: T-16-TC01', async ({ page }) => { test.setTimeout(300_000); const topology = new TopologyPage(page); await createWorkload(page, 'dotnet-display-test'); - + await test.step('Check default display options', async () => { await topology.clickDisplayOptions(); await expect(topology.getExpandToggle()).toBeChecked(); await expect(topology.getDisplayOptionCheckbox('Pod count')).not.toBeChecked(); await expect(topology.getDisplayOptionCheckbox('Labels')).toBeChecked(); }); - + await test.step('Clean up: Delete workload', async () => { await deleteWorkload(page, 'dotnet-display-test'); await expect(topology.getNoResourcesFound()).toBeVisible({ timeout: 60_000 }); }); }); - + test('Delete workload via Action menu: T-15-TC01', async ({ page }) => { test.setTimeout(300_000); const topology = new TopologyPage(page); const sidebar = new TopologySidebarPage(page); await createWorkload(page, 'dotnet-delete-test'); - + await test.step('Open sidebar and delete via Actions menu', async () => { await topology.clickOnNode('dotnet-delete-test'); await sidebar.verify(); await sidebar.selectAction('Delete Deployment'); }); - + await test.step('Confirm deletion and verify empty state', async () => { await topology.clickConfirmAction(); await expect(topology.getNoResourcesFound()).toBeVisible({ timeout: 60_000 }); diff --git a/frontend/e2e/tests/webterminal/developer/web-terminal-devuser.spec.ts b/frontend/e2e/tests/webterminal/developer/web-terminal-devuser.spec.ts index e816ae011ad..466e3c27966 100644 --- a/frontend/e2e/tests/webterminal/developer/web-terminal-devuser.spec.ts +++ b/frontend/e2e/tests/webterminal/developer/web-terminal-devuser.spec.ts @@ -51,40 +51,37 @@ test.describe('Web Terminal for Developer user', () => { await uninstallWebTerminalOperator(k8sClient); }); - test( - 'create new project and use Web Terminal', - async ({ page, k8sClient, cleanup }) => { - const webTerminal = new WebTerminalPage(page); - cleanup.trackNamespace(NEW_PROJECT); - - await test.step('Wait for terminal icon', async () => { - await webTerminal.waitForTerminalIconVisible(); - }); - - await test.step('Create new project from terminal init screen', async () => { - await webTerminal.clickTerminalIcon(); - await webTerminal.clickProjectDropdown(); - await webTerminal.selectCreateProject(); - await webTerminal.typeProjectName(NEW_PROJECT); - await webTerminal.confirmProjectCreation(); - }); - - await test.step('Set timeout and start terminal', async () => { - await webTerminal.clickAdvancedTimeout(); - await webTerminal.setTimeoutValue('1'); - await webTerminal.clickStartButton(); - }); - - await test.step('Verify terminal window is visible', async () => { - await webTerminal.waitForTerminalWindow(); - await expect(webTerminal.getTerminalWindow()).toBeVisible(); - }); - - await test.step('Verify DevWorkspace is running in developer namespace', async () => { - await verifyDevWorkspaceRunning(page, k8sClient, webTerminal, NEW_PROJECT); - }); - }, - ); + test('create new project and use Web Terminal', async ({ page, k8sClient, cleanup }) => { + const webTerminal = new WebTerminalPage(page); + cleanup.trackNamespace(NEW_PROJECT); + + await test.step('Wait for terminal icon', async () => { + await webTerminal.waitForTerminalIconVisible(); + }); + + await test.step('Create new project from terminal init screen', async () => { + await webTerminal.clickTerminalIcon(); + await webTerminal.clickProjectDropdown(); + await webTerminal.selectCreateProject(); + await webTerminal.typeProjectName(NEW_PROJECT); + await webTerminal.confirmProjectCreation(); + }); + + await test.step('Set timeout and start terminal', async () => { + await webTerminal.clickAdvancedTimeout(); + await webTerminal.setTimeoutValue('1'); + await webTerminal.clickStartButton(); + }); + + await test.step('Verify terminal window is visible', async () => { + await webTerminal.waitForTerminalWindow(); + await expect(webTerminal.getTerminalWindow()).toBeVisible(); + }); + + await test.step('Verify DevWorkspace is running in developer namespace', async () => { + await verifyDevWorkspaceRunning(page, k8sClient, webTerminal, NEW_PROJECT); + }); + }); // eslint-disable-next-line playwright/expect-expect test('open Web Terminal for existing project', async ({ page, k8sClient }) => { diff --git a/frontend/e2e/tests/webterminal/utils/web-terminal-operator.ts b/frontend/e2e/tests/webterminal/utils/web-terminal-operator.ts index 7c655cdefc1..fd4c81e00ae 100644 --- a/frontend/e2e/tests/webterminal/utils/web-terminal-operator.ts +++ b/frontend/e2e/tests/webterminal/utils/web-terminal-operator.ts @@ -68,9 +68,7 @@ export async function ensureWebTerminalOperatorInstalled( const CSV_PLURAL = 'clusterserviceversions'; -export async function uninstallWebTerminalOperator( - k8sClient: KubernetesClient, -): Promise { +export async function uninstallWebTerminalOperator(k8sClient: KubernetesClient): Promise { try { await k8sClient.deleteCustomResource( SUBSCRIPTION_GROUP, @@ -86,8 +84,8 @@ export async function uninstallWebTerminalOperator( OPERATOR_NAMESPACE, CSV_PLURAL, ); - const webTerminalCsv = csvs.find( - (csv) => (csv as any).metadata?.name?.startsWith('web-terminal'), + const webTerminalCsv = csvs.find((csv) => + (csv as any).metadata?.name?.startsWith('web-terminal'), ); if (webTerminalCsv) { await k8sClient.deleteCustomResource( diff --git a/frontend/e2e/tests/webterminal/web-terminal-admin.spec.ts b/frontend/e2e/tests/webterminal/web-terminal-admin.spec.ts index afce4f0cb64..abe6e6ce921 100644 --- a/frontend/e2e/tests/webterminal/web-terminal-admin.spec.ts +++ b/frontend/e2e/tests/webterminal/web-terminal-admin.spec.ts @@ -85,76 +85,67 @@ test.describe('Web Terminal for Admin user', () => { await uninstallWebTerminalOperator(k8sClient); }); - test( - 'open and close multiple terminal tabs', - async ({ page }) => { - const webTerminal = new WebTerminalPage(page); - - await test.step('Wait for terminal icon and start terminal', async () => { - await webTerminal.waitForTerminalIconVisible(); - await webTerminal.clickTerminalIcon(); - await webTerminal.clickStartButton(); - await webTerminal.waitForTerminalWindow(); - }); - - await test.step('Open 3 additional tabs', async () => { - await webTerminal.addTerminalTabs(3); - }); - - await test.step('Close the 2nd tab', async () => { - await webTerminal.closeTerminalTab(1); - }); - - await test.step('Verify 3 tabs remain', async () => { - const tabCount = await webTerminal.getOpenTabCount(); - expect(tabCount).toEqual(3); - }); - - await test.step('Close terminal drawer', async () => { - await webTerminal.closeTerminalDrawer(); - }); - }, - ); + test('open and close multiple terminal tabs', async ({ page }) => { + const webTerminal = new WebTerminalPage(page); + + await test.step('Wait for terminal icon and start terminal', async () => { + await webTerminal.waitForTerminalIconVisible(); + await webTerminal.clickTerminalIcon(); + await webTerminal.clickStartButton(); + await webTerminal.waitForTerminalWindow(); + }); + + await test.step('Open 3 additional tabs', async () => { + await webTerminal.addTerminalTabs(3); + }); + + await test.step('Close the 2nd tab', async () => { + await webTerminal.closeTerminalTab(1); + }); + + await test.step('Verify 3 tabs remain', async () => { + const tabCount = await webTerminal.getOpenTabCount(); + expect(tabCount).toEqual(3); + }); + + await test.step('Close terminal drawer', async () => { + await webTerminal.closeTerminalDrawer(); + }); + }); // eslint-disable-next-line playwright/expect-expect - test( - 'start terminal with timeout and verify DevWorkspace', - async ({ page, k8sClient }) => { - const webTerminal = new WebTerminalPage(page); - - await test.step('Open terminal with 10-minute timeout', async () => { - await webTerminal.waitForTerminalIconVisible(); - await webTerminal.clickTerminalIcon(); - await webTerminal.clickAdvancedTimeout(); - await webTerminal.setTimeoutValue('10'); - await webTerminal.clickStartButton(); - }); - - await test.step('Verify DevWorkspace UID matches YAML editor', async () => { - await verifyDevWorkspaceUid(page, k8sClient, webTerminal, TERMINAL_NAMESPACE); - }); - }, - ); + test('start terminal with timeout and verify DevWorkspace', async ({ page, k8sClient }) => { + const webTerminal = new WebTerminalPage(page); + + await test.step('Open terminal with 10-minute timeout', async () => { + await webTerminal.waitForTerminalIconVisible(); + await webTerminal.clickTerminalIcon(); + await webTerminal.clickAdvancedTimeout(); + await webTerminal.setTimeoutValue('10'); + await webTerminal.clickStartButton(); + }); + + await test.step('Verify DevWorkspace UID matches YAML editor', async () => { + await verifyDevWorkspaceUid(page, k8sClient, webTerminal, TERMINAL_NAMESPACE); + }); + }); - test( - 'start terminal with defaults and verify DevWorkspace', - async ({ page, k8sClient }) => { - const webTerminal = new WebTerminalPage(page); - - await test.step('Open terminal with default settings', async () => { - await webTerminal.waitForTerminalIconVisible(); - await webTerminal.clickTerminalIcon(); - await webTerminal.clickStartButton(); - }); - - await test.step('Verify terminal window is visible', async () => { - await webTerminal.waitForTerminalWindow(); - await expect(webTerminal.getTerminalWindow()).toBeVisible(); - }); - - await test.step('Verify DevWorkspace UID matches YAML editor', async () => { - await verifyDevWorkspaceUid(page, k8sClient, webTerminal, TERMINAL_NAMESPACE); - }); - }, - ); + test('start terminal with defaults and verify DevWorkspace', async ({ page, k8sClient }) => { + const webTerminal = new WebTerminalPage(page); + + await test.step('Open terminal with default settings', async () => { + await webTerminal.waitForTerminalIconVisible(); + await webTerminal.clickTerminalIcon(); + await webTerminal.clickStartButton(); + }); + + await test.step('Verify terminal window is visible', async () => { + await webTerminal.waitForTerminalWindow(); + await expect(webTerminal.getTerminalWindow()).toBeVisible(); + }); + + await test.step('Verify DevWorkspace UID matches YAML editor', async () => { + await verifyDevWorkspaceUid(page, k8sClient, webTerminal, TERMINAL_NAMESPACE); + }); + }); }); diff --git a/frontend/e2e/tests/webterminal/web-terminal-config.spec.ts b/frontend/e2e/tests/webterminal/web-terminal-config.spec.ts index dd24603d1fa..f2a4b44e3ec 100644 --- a/frontend/e2e/tests/webterminal/web-terminal-config.spec.ts +++ b/frontend/e2e/tests/webterminal/web-terminal-config.spec.ts @@ -24,139 +24,132 @@ test.describe('Customization of web terminal options', () => { await uninstallWebTerminalOperator(k8sClient); }); - test( - 'navigate to Web Terminal Configuration page', - async ({ page }) => { - const configPage = new WebTerminalConfigPage(page); + test('navigate to Web Terminal Configuration page', async ({ page }) => { + const configPage = new WebTerminalConfigPage(page); - await test.step('Navigate to Consoles and open Customize', async () => { - await configPage.navigateToWebTerminalConfig(); - }); + await test.step('Navigate to Consoles and open Customize', async () => { + await configPage.navigateToWebTerminalConfig(); + }); - await test.step('Verify configuration page is visible', async () => { - await expect(configPage.getConfigSection()).toBeVisible(); - }); - }, - ); + await test.step('Verify configuration page is visible', async () => { + await expect(configPage.getConfigSection()).toBeVisible(); + }); + }); - test( - 'change timeout and image with persist checkboxes', - async ({ page }) => { - const configPage = new WebTerminalConfigPage(page); - - await test.step('Navigate to Web Terminal Configuration', async () => { - await configPage.navigateToWebTerminalConfig(); - }); - - await test.step('Set timeout to Minutes and enter image', async () => { - await configPage.incrementTimeout(); - await configPage.selectTimeoutUnit('Minutes'); - await configPage.setImageValue(TEST_IMAGE_805); - }); - - await test.step('Check persist checkboxes and save', async () => { - await configPage.checkPersistCheckboxes(); - await configPage.clickSaveButton(); - }); - - await test.step('Verify success alert', async () => { - await expect(configPage.getSuccessAlert()).toBeVisible(); - }); - }, - ); + test('change timeout and image with persist checkboxes', async ({ page }) => { + const configPage = new WebTerminalConfigPage(page); - test( - 'change timeout to Hours and verify values persist after tab switch', - async ({ page }) => { - const configPage = new WebTerminalConfigPage(page); - - await test.step('Navigate to Web Terminal Configuration', async () => { - await configPage.navigateToWebTerminalConfig(); - }); - - await test.step('Set timeout to Hours, enter image, check persist, and save', async () => { - await configPage.incrementTimeout(); - await configPage.selectTimeoutUnit('Hours'); - await configPage.setImageValue(TEST_IMAGE_806); - await configPage.checkPersistCheckboxes(); - await configPage.clickSaveButton(); - }); - - await test.step('Re-navigate to Web Terminal Configuration to verify persistence', async () => { - await configPage.navigateToWebTerminalConfig(); - }); - - await test.step('Verify saved values persist', async () => { - await expect(configPage.getImageInput()).toHaveValue(TEST_IMAGE_806); - await expect(configPage.getSelectToggle()).toContainText('Hours'); - await expect(configPage.getTimeoutCheckbox()).toBeChecked(); - await expect(configPage.getImageCheckbox()).toBeChecked(); - }); - }, - ); + await test.step('Navigate to Web Terminal Configuration', async () => { + await configPage.navigateToWebTerminalConfig(); + }); + + await test.step('Set timeout to Minutes and enter image', async () => { + await configPage.incrementTimeout(); + await configPage.selectTimeoutUnit('Minutes'); + await configPage.setImageValue(TEST_IMAGE_805); + }); + + await test.step('Check persist checkboxes and save', async () => { + await configPage.checkPersistCheckboxes(); + await configPage.clickSaveButton(); + }); + + await test.step('Verify success alert', async () => { + await expect(configPage.getSuccessAlert()).toBeVisible(); + }); + }); + + test('change timeout to Hours and verify values persist after tab switch', async ({ page }) => { + const configPage = new WebTerminalConfigPage(page); + + await test.step('Navigate to Web Terminal Configuration', async () => { + await configPage.navigateToWebTerminalConfig(); + }); + + await test.step('Set timeout to Hours, enter image, check persist, and save', async () => { + await configPage.incrementTimeout(); + await configPage.selectTimeoutUnit('Hours'); + await configPage.setImageValue(TEST_IMAGE_806); + await configPage.checkPersistCheckboxes(); + await configPage.clickSaveButton(); + }); + + await test.step('Re-navigate to Web Terminal Configuration to verify persistence', async () => { + await configPage.navigateToWebTerminalConfig(); + }); + + await test.step('Verify saved values persist', async () => { + await expect(configPage.getImageInput()).toHaveValue(TEST_IMAGE_806); + await expect(configPage.getSelectToggle()).toContainText('Hours'); + await expect(configPage.getTimeoutCheckbox()).toBeChecked(); + await expect(configPage.getImageCheckbox()).toBeChecked(); + }); + }); + + test('save without persist checkboxes', async ({ page }) => { + const configPage = new WebTerminalConfigPage(page); + await test.step('Navigate to Web Terminal Configuration', async () => { + await configPage.navigateToWebTerminalConfig(); + }); + + await test.step('Set timeout, image, uncheck persist, and save', async () => { + await configPage.incrementTimeout(); + await configPage.selectTimeoutUnit('Hours'); + await configPage.setImageValue(TEST_IMAGE_806); + await configPage.uncheckPersistCheckboxes(); + await configPage.clickSaveButton(); + }); + + await test.step('Verify success alert', async () => { + await expect(configPage.getSuccessAlert()).toBeVisible(); + }); + }); + + test('verify unchecked checkboxes persist after tab switch', async ({ page }) => { + const configPage = new WebTerminalConfigPage(page); + + await test.step('Navigate to Web Terminal Configuration', async () => { + await configPage.navigateToWebTerminalConfig(); + }); + + await test.step('Set values, uncheck persist, and save', async () => { + await configPage.incrementTimeout(); + await configPage.selectTimeoutUnit('Hours'); + await configPage.setImageValue(TEST_IMAGE_806); + await configPage.uncheckPersistCheckboxes(); + await configPage.clickSaveButton(); + }); + + await test.step('Re-navigate to Web Terminal Configuration to verify persistence', async () => { + await configPage.navigateToWebTerminalConfig(); + }); + + await test.step('Verify checkboxes are unchecked', async () => { + await expect(configPage.getTimeoutCheckbox()).not.toBeChecked(); + await expect(configPage.getImageCheckbox()).not.toBeChecked(); + }); + }); + + // eslint-disable-next-line playwright/expect-expect test( - 'save without persist checkboxes', - async ({ page }) => { - const configPage = new WebTerminalConfigPage(page); - - await test.step('Navigate to Web Terminal Configuration', async () => { - await configPage.navigateToWebTerminalConfig(); - }); - - await test.step('Set timeout, image, uncheck persist, and save', async () => { - await configPage.incrementTimeout(); - await configPage.selectTimeoutUnit('Hours'); - await configPage.setImageValue(TEST_IMAGE_806); - await configPage.uncheckPersistCheckboxes(); - await configPage.clickSaveButton(); - }); - - await test.step('Verify success alert', async () => { - await expect(configPage.getSuccessAlert()).toBeVisible(); - }); + 'verify timeout in DevWorkspaceTemplate YAML (manual)', + { + annotation: { type: 'skip', description: 'Manual verification required' }, + }, + async () => { + test.skip(true, 'Manual verification required'); }, ); + // eslint-disable-next-line playwright/expect-expect test( - 'verify unchecked checkboxes persist after tab switch', - async ({ page }) => { - const configPage = new WebTerminalConfigPage(page); - - await test.step('Navigate to Web Terminal Configuration', async () => { - await configPage.navigateToWebTerminalConfig(); - }); - - await test.step('Set values, uncheck persist, and save', async () => { - await configPage.incrementTimeout(); - await configPage.selectTimeoutUnit('Hours'); - await configPage.setImageValue(TEST_IMAGE_806); - await configPage.uncheckPersistCheckboxes(); - await configPage.clickSaveButton(); - }); - - await test.step('Re-navigate to Web Terminal Configuration to verify persistence', async () => { - await configPage.navigateToWebTerminalConfig(); - }); - - await test.step('Verify checkboxes are unchecked', async () => { - await expect(configPage.getTimeoutCheckbox()).not.toBeChecked(); - await expect(configPage.getImageCheckbox()).not.toBeChecked(); - }); + 'verify image in DevWorkspaceTemplate YAML (manual)', + { + annotation: { type: 'skip', description: 'Manual verification required' }, + }, + async () => { + test.skip(true, 'Manual verification required'); }, ); - - // eslint-disable-next-line playwright/expect-expect - test('verify timeout in DevWorkspaceTemplate YAML (manual)', { - annotation: { type: 'skip', description: 'Manual verification required' }, - }, async () => { - test.skip(true, 'Manual verification required'); - }); - - // eslint-disable-next-line playwright/expect-expect - test('verify image in DevWorkspaceTemplate YAML (manual)', { - annotation: { type: 'skip', description: 'Manual verification required' }, - }, async () => { - test.skip(true, 'Manual verification required'); - }); }); diff --git a/frontend/e2e/utils/i18n.ts b/frontend/e2e/utils/i18n.ts index ace2f060335..00ab9fb5cc7 100644 --- a/frontend/e2e/utils/i18n.ts +++ b/frontend/e2e/utils/i18n.ts @@ -5,10 +5,9 @@ const PSEUDO_LOCALIZED_PATTERN = /\[[^a-zA-Z]+\]/; async function isPseudoLocalized(text: string, context: string): Promise { if (text.trim().length > 0) { - expect( - text, - `Expected pseudolocalized text in ${context}, got: "${text}"`, - ).toMatch(PSEUDO_LOCALIZED_PATTERN); + expect(text, `Expected pseudolocalized text in ${context}, got: "${text}"`).toMatch( + PSEUDO_LOCALIZED_PATTERN, + ); } } diff --git a/frontend/e2e/utils/operator-check.ts b/frontend/e2e/utils/operator-check.ts index d6b233abed3..848cabf57a7 100644 --- a/frontend/e2e/utils/operator-check.ts +++ b/frontend/e2e/utils/operator-check.ts @@ -19,10 +19,7 @@ export async function hasOperatorSubscription( ); return true; } catch (err) { - const code = - (err as any).statusCode ?? - (err as any).response?.statusCode ?? - (err as any).code; + const code = (err as any).statusCode ?? (err as any).response?.statusCode ?? (err as any).code; if (code === 404) { return false; } diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts index 8da81445878..f6cef56b5dd 100644 --- a/frontend/eslint.config.ts +++ b/frontend/eslint.config.ts @@ -214,7 +214,10 @@ const config = defineConfig([ { files: ['e2e/**/*.{js,jsx,ts,tsx,json}'], ignores: ['e2e/**/testData/**'], - extends: compat.extends('plugin:console/playwright'), + extends: compat.extends( + 'plugin:console/prettier', + 'plugin:console/playwright' + ), languageOptions: { parser: tsParser, parserOptions: { diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 09a2ed6a63b..cd0f42d3709 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -11,6 +11,9 @@ const chrome = { ...devices['Desktop Chrome'], userAgent: INTEGRATION_TEST_USER_ const isDebug = process.env.DEBUG === '1' || process.env.DEBUG === 'true'; const baseURL = process.env.WEB_CONSOLE_URL || 'http://localhost:9000'; +// Keep these paths in sync with adminStorageState/developerStorageState in +// e2e/setup/login-helper.ts. They cannot be imported here: that module uses +// import.meta (ESM) while Playwright loads this config as CommonJS. const adminStorageState = path.resolve(__dirname, 'e2e', '.auth', 'kubeadmin.json'); const developerStorageState = path.resolve(__dirname, 'e2e', '.auth', 'developer.json'); const hasDeveloper = !!process.env.BRIDGE_HTPASSWD_USERNAME; @@ -51,7 +54,6 @@ export default defineConfig({ testMatch: '**/*.spec.ts', forbidOnly: isCI, globalTimeout: Number(process.env.GLOBAL_TIMEOUT_MS) || 0, - maxFailures: isCI ? 10 : 0, retries: isCI ? 2 : 0, timeout: 120_000, reporter: isCI