-
Notifications
You must be signed in to change notification settings - Fork 702
CONSOLE-5244: Migrate webterminal-plugin Cypress tests to Playwright #16461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import type { Locator } from '@playwright/test'; | ||
|
|
||
| import BasePage from './base-page'; | ||
|
|
||
| export class WebTerminalConfigPage extends BasePage { | ||
| private readonly configSection = this.page.getByTestId('web-terminal form-section'); | ||
| private readonly incrementButton = this.page.locator( | ||
| '[data-test="Increment"], [data-test-id="Increment"]', | ||
| ); | ||
| private readonly selectToggle = this.page.getByTestId('console-select-menu-toggle'); | ||
| private readonly imageInput = this.page.getByTestId('web-terminal-image'); | ||
| private readonly timeoutCheckbox = this.page.getByTestId('timeout-value-checkbox'); | ||
| private readonly imageCheckbox = this.page.getByTestId('image-value-checkbox'); | ||
| private readonly saveButton = this.page.getByTestId('save-button'); | ||
| private readonly successAlert = this.page.getByTestId('success-alert'); | ||
|
|
||
| async navigateToWebTerminalConfig(): Promise<void> { | ||
| await this.goTo('/k8s/cluster/operator.openshift.io~v1~Console/cluster'); | ||
|
fsgreco marked this conversation as resolved.
|
||
| await this.waitForLoadingComplete(10_000); | ||
| const customizeButton = this.page.locator('button', { hasText: 'Customize' }); | ||
| await customizeButton | ||
| .first() | ||
| .waitFor({ state: 'visible', timeout: 30_000 }) | ||
| .catch(() => {}); | ||
| if (await customizeButton.first().isVisible()) { | ||
| await this.robustClick(customizeButton.first()); | ||
| } else { | ||
|
fsgreco marked this conversation as resolved.
|
||
| const actionsMenu = this.page.locator( | ||
| '[data-test="actions-menu-button"], [data-test-id="actions-menu-button"]', | ||
| ); | ||
| await this.robustClick(actionsMenu); | ||
| const customizeAction = this.page.locator('[data-test-action="Customize"]:not([disabled])'); | ||
| await this.robustClick(customizeAction); | ||
| } | ||
| await this.waitForLoadingComplete(10_000); | ||
| await this.clickWebTerminalTab(); | ||
| } | ||
|
|
||
| async clickWebTerminalTab(): Promise<void> { | ||
| const tab = this.page | ||
| .locator('[role="presentation"]') | ||
| .filter({ hasText: 'Web Terminal' }) | ||
| .first(); | ||
| await this.robustClick(tab); | ||
| await this.waitForLoadingComplete(5_000); | ||
| } | ||
|
|
||
| async incrementTimeout(): Promise<void> { | ||
| await this.robustClick(this.incrementButton); | ||
| } | ||
|
|
||
| async selectTimeoutUnit(unit: string): Promise<void> { | ||
| await this.robustClick(this.selectToggle); | ||
| const option = this.page.getByTestId('console-select-item').filter({ hasText: unit }); | ||
| await this.robustClick(option); | ||
| } | ||
|
|
||
| async setImageValue(image: string): Promise<void> { | ||
| await this.imageInput.fill(image); | ||
| } | ||
|
|
||
| async checkPersistCheckboxes(): Promise<void> { | ||
| await this.timeoutCheckbox.check(); | ||
| await this.imageCheckbox.check(); | ||
| } | ||
|
|
||
| async uncheckPersistCheckboxes(): Promise<void> { | ||
| await this.timeoutCheckbox.uncheck(); | ||
| await this.imageCheckbox.uncheck(); | ||
| } | ||
|
|
||
| async clickSaveButton(): Promise<void> { | ||
| await this.robustClick(this.saveButton); | ||
| } | ||
|
|
||
| getConfigSection(): Locator { | ||
| return this.configSection; | ||
| } | ||
|
|
||
| getSuccessAlert(): Locator { | ||
| return this.successAlert; | ||
| } | ||
|
|
||
| getImageInput(): Locator { | ||
| return this.imageInput; | ||
| } | ||
|
|
||
| getSelectToggle(): Locator { | ||
| return this.selectToggle; | ||
| } | ||
|
|
||
| getTimeoutCheckbox(): Locator { | ||
| return this.timeoutCheckbox; | ||
| } | ||
|
|
||
| getImageCheckbox(): Locator { | ||
| return this.imageCheckbox; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import type { Locator } from '@playwright/test'; | ||
|
|
||
| import BasePage from './base-page'; | ||
|
|
||
| export class WebTerminalPage extends BasePage { | ||
| private readonly terminalIcon = this.page.locator( | ||
| 'button[data-tour-id="tour-cloud-shell-button"]', | ||
| ); | ||
| private readonly terminalContainer = this.page.locator('.co-cloudshell-terminal__container'); | ||
| private readonly terminalWindow = this.page.locator('div.xterm-screen>div.xterm-rows'); | ||
| private readonly addTabButton = this.page.locator( | ||
| '[data-test="multi-tab-terminal"] [aria-label="Add new tab"]', | ||
| ); | ||
| private readonly closeTabButtons = this.page.locator('[aria-label="Close terminal tab"]'); | ||
| private readonly tabsList = this.page.locator('[data-test="multi-tab-terminal"] ul'); | ||
| private readonly drawerCloseButton = this.page.getByTestId('cloudshell-drawer-close-button'); | ||
| private readonly loadingBox = this.page.getByTestId('loading-box'); | ||
| private readonly timeoutLink = this.page.getByText('Timeout', { exact: true }); | ||
| private readonly incrementButton = this.page.locator( | ||
| '[data-test="Increment"], [data-test-id="Increment"]', | ||
| ); | ||
| private readonly timeoutInput = this.page.locator('input[aria-label="Input"]'); | ||
| private readonly startButton = this.page.locator('[data-test-id="submit-button"]'); | ||
| private readonly resourceTitle = this.page.locator( | ||
| '[data-test="resource-title"], [data-test-id="resource-title"]', | ||
| ); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| private readonly monacoEditor = this.page.locator('div.lines-content.monaco-editor-background'); | ||
| private readonly openInNewTabLink = this.page.locator("a[href='/terminal']"); | ||
| private readonly closeTerminalButton = this.page.locator( | ||
|
fsgreco marked this conversation as resolved.
|
||
| "button[aria-label='Close terminal'], [aria-label='Close terminal tab']", | ||
| ); | ||
| private readonly inactivityMessageArea = this.page.locator('div.co-cloudshell-exec__error-msg'); | ||
| private readonly perspectiveSwitcherToggle = this.page.locator( | ||
| '[data-test-id="perspective-switcher-toggle"]', | ||
| ); | ||
|
|
||
| async waitForTerminalIconVisible(maxRetries = 10): Promise<void> { | ||
| await this.goTo('/'); | ||
| try { | ||
| await this.terminalIcon.waitFor({ state: 'visible', timeout: 30_000 }); | ||
| return; | ||
| } catch { | ||
| // Icon not visible on first load — retry with reloads | ||
| } | ||
| for (let attempt = 0; attempt < maxRetries; attempt++) { | ||
| await this.page.reload(); | ||
| try { | ||
| await this.terminalIcon.waitFor({ state: 'visible', timeout: 15_000 }); | ||
| return; | ||
| } catch { | ||
| // Retry | ||
| } | ||
| } | ||
| throw new Error(`Terminal icon not visible after ${maxRetries} retries`); | ||
| } | ||
|
|
||
| async clickTerminalIcon(): Promise<void> { | ||
| await this.robustClick(this.terminalIcon); | ||
| await this.loadingBox.waitFor({ state: 'detached', timeout: 60_000 }).catch(() => {}); | ||
|
fsgreco marked this conversation as resolved.
|
||
| } | ||
|
|
||
| async waitForTerminalWindow(timeoutMs = 60_000): Promise<void> { | ||
| await this.terminalContainer.waitFor({ state: 'visible', timeout: timeoutMs }); | ||
| await this.terminalWindow.waitFor({ state: 'visible', timeout: timeoutMs }); | ||
| } | ||
|
|
||
| async closeTerminalDrawer(): Promise<void> { | ||
| await this.robustClick(this.drawerCloseButton); | ||
| await this.terminalContainer.waitFor({ state: 'hidden', timeout: 10_000 }).catch(() => {}); | ||
| } | ||
|
|
||
| async closeTerminalSession(): Promise<void> { | ||
| await this.robustClick(this.closeTerminalButton.first()); | ||
| const confirmButton = this.page.getByRole('button', { name: 'Close' }); | ||
| await this.robustClick(confirmButton); | ||
| } | ||
|
|
||
| async clickAdvancedTimeout(): Promise<void> { | ||
| await this.timeoutLink.waitFor({ state: 'visible', timeout: 30_000 }); | ||
| await this.robustClick(this.timeoutLink); | ||
| } | ||
|
|
||
| async setTimeoutValue(value: string): Promise<void> { | ||
| await this.incrementButton.waitFor({ state: 'visible', timeout: 10_000 }); | ||
| await this.robustClick(this.incrementButton); | ||
| await this.timeoutInput.fill(value); | ||
| await this.timeoutInput.press('Tab'); | ||
| } | ||
|
|
||
| async clickStartButton(): Promise<void> { | ||
| await this.robustClick(this.startButton); | ||
| await this.waitForLoadingComplete(10_000); | ||
| } | ||
|
|
||
| async addTerminalTabs(count: number): Promise<void> { | ||
| for (let i = 0; i < count; i++) { | ||
| await this.robustClick(this.addTabButton); | ||
| } | ||
| } | ||
|
|
||
| async closeTerminalTab(tabIndex: number): Promise<void> { | ||
| await this.robustClick(this.closeTabButtons.nth(tabIndex)); | ||
| } | ||
|
|
||
| async getOpenTabCount(): Promise<number> { | ||
| const children = this.tabsList.locator('> *'); | ||
| return children.count(); | ||
| } | ||
|
|
||
| async getResourceTitle(): Promise<string> { | ||
| await this.resourceTitle.waitFor({ state: 'visible' }); | ||
| return (await this.resourceTitle.textContent()) || ''; | ||
| } | ||
|
|
||
| getMonacoEditor(): Locator { | ||
| return this.monacoEditor; | ||
| } | ||
|
|
||
| getTerminalIcon(): Locator { | ||
| return this.terminalIcon; | ||
| } | ||
|
|
||
| getTerminalWindow(): Locator { | ||
| return this.terminalWindow; | ||
| } | ||
|
|
||
| getOpenInNewTabLink(): Locator { | ||
| return this.openInNewTabLink; | ||
| } | ||
|
|
||
| getInactivityMessageArea(): Locator { | ||
| return this.inactivityMessageArea; | ||
| } | ||
|
|
||
| async clickProjectDropdown(): Promise<void> { | ||
| const dropdown = this.page.locator('[data-test-id="namespace-bar-dropdown"] [type="button"]'); | ||
| await this.robustClick(dropdown); | ||
| } | ||
|
|
||
| async selectCreateProject(): Promise<void> { | ||
| const createBtn = this.page.locator('[data-test-dropdown-menu="#CREATE_RESOURCE_ACTION#"]'); | ||
| await this.robustClick(createBtn); | ||
| } | ||
|
|
||
| async typeProjectName(name: string): Promise<void> { | ||
| await this.page.getByTestId('input-name').fill(name); | ||
| } | ||
|
|
||
| async confirmProjectCreation(): Promise<void> { | ||
| await this.robustClick(this.page.getByTestId('confirm-action')); | ||
| } | ||
|
|
||
| async selectProjectFromDropdown(name: string): Promise<void> { | ||
| const filterInput = this.page.getByTestId('dropdown-text-filter'); | ||
| await filterInput.fill(name); | ||
| const item = this.page.getByTestId('console-select-item').filter({ hasText: name }); | ||
| await this.robustClick(item); | ||
| } | ||
|
|
||
| async navigateTo(url: string): Promise<void> { | ||
| await this.goTo(url); | ||
| } | ||
|
|
||
| async waitForPageReady(timeoutMs = 30_000): Promise<void> { | ||
| await this.waitForLoadingComplete(timeoutMs); | ||
| } | ||
|
|
||
| async navigateToDevWorkspaceSearch(namespace: string): Promise<void> { | ||
| await this.goTo(`/search/ns/${namespace}?kind=workspace.devfile.io~v1alpha2~DevWorkspace`); | ||
| await this.waitForLoadingComplete(30_000); | ||
| } | ||
|
|
||
| async navigateToDevWorkspaceYaml(namespace: string, name: string): Promise<void> { | ||
| await this.goTo(`/k8s/ns/${namespace}/workspace.devfile.io~v1alpha2~DevWorkspace/${name}/yaml`); | ||
| await this.waitForLoadingComplete(30_000); | ||
| } | ||
|
|
||
| async switchPerspective(target: 'Developer' | 'Administrator'): Promise<void> { | ||
| const labelMap: Record<string, string[]> = { | ||
| Administrator: ['Administrator', 'Core platform'], | ||
| Developer: ['Developer'], | ||
| }; | ||
| const labels = labelMap[target] || [target]; | ||
| const currentText = (await this.perspectiveSwitcherToggle.textContent()) || ''; | ||
| if (labels.some((label) => currentText.includes(label))) { | ||
| return; | ||
| } | ||
| await this.robustClick(this.perspectiveSwitcherToggle); | ||
| const menuOption = this.page.locator('[data-test-id="perspective-switcher-menu-option"]'); | ||
| for (const label of labels) { | ||
| const option = menuOption.filter({ hasText: label }); | ||
| if ((await option.count()) > 0) { | ||
| await this.robustClick(option.first()); | ||
| await this.waitForLoadingComplete(); | ||
| return; | ||
| } | ||
| } | ||
| throw new Error(`Perspective "${target}" not found in switcher menu`); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { test, expect } from '../../../fixtures'; | ||
| import { WebTerminalPage } from '../../../pages/web-terminal-page'; | ||
| import { ensureWebTerminalOperatorInstalled } from '../../../utils/web-terminal-operator'; | ||
|
|
||
| const INACTIVITY_MESSAGE = 'The terminal connection has closed due to inactivity.'; | ||
| const TERMINAL_IDLING_TIMEOUT = Number(process.env.TERMINAL_IDLING_TIMEOUT) || 200_000; | ||
| const TEST_NAMESPACE = 'aut-terminal-basic'; | ||
|
|
||
| test.describe('Web Terminal basic user', { tag: ['@web-terminal', '@regression'] }, () => { | ||
| test.beforeAll(async ({ k8sClient }) => { | ||
| await ensureWebTerminalOperatorInstalled(k8sClient); | ||
| }); | ||
|
|
||
| test.beforeEach(async ({ k8sClient, cleanup }) => { | ||
| await k8sClient.createNamespace(TEST_NAMESPACE); | ||
| cleanup.trackNamespace(TEST_NAMESPACE); | ||
| }); | ||
|
Comment on lines
+7
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use unique namespace names per test run. Lines [6-12] reuse a fixed namespace across all tests. That tends to flake on retries or overlapping runs when deletion lags. Generate a per-test suffix (e.g., with 🤖 Prompt for AI Agents |
||
|
|
||
| test('WT-01-TC01: open terminal with advanced timeout', async ({ page }) => { | ||
| const webTerminal = new WebTerminalPage(page); | ||
|
|
||
| await test.step('Open terminal with 1-minute timeout', async () => { | ||
| await webTerminal.waitForTerminalIconVisible(); | ||
| await webTerminal.clickTerminalIcon(); | ||
| 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('Close terminal session', async () => { | ||
| await webTerminal.closeTerminalSession(); | ||
| }); | ||
| }); | ||
|
|
||
| test('WT-01-TC02: verify Open in new tab button', async ({ page }) => { | ||
| const webTerminal = new WebTerminalPage(page); | ||
|
|
||
| await test.step('Wait for terminal icon and open terminal', async () => { | ||
| await webTerminal.waitForTerminalIconVisible(); | ||
| await webTerminal.clickTerminalIcon(); | ||
| }); | ||
|
|
||
| await test.step('Verify Open in new tab link has target _blank', async () => { | ||
| const newTabLink = webTerminal.getOpenInNewTabLink(); | ||
| await expect(newTabLink).toBeVisible(); | ||
| await expect(newTabLink).toHaveAttribute('target', '_blank'); | ||
| }); | ||
| }); | ||
|
|
||
| test('WT-01-TC03: inactivity timeout closes terminal', async ({ page }) => { | ||
| test.slow(); | ||
| const webTerminal = new WebTerminalPage(page); | ||
|
|
||
| await test.step('Open terminal and wait for terminal window', async () => { | ||
| await webTerminal.waitForTerminalIconVisible(); | ||
| await webTerminal.clickTerminalIcon(); | ||
| await webTerminal.clickStartButton(); | ||
| await webTerminal.waitForTerminalWindow(); | ||
| await expect(webTerminal.getTerminalWindow()).toBeVisible(); | ||
| }); | ||
|
|
||
| await test.step('Wait for inactivity message', async () => { | ||
| await expect(webTerminal.getInactivityMessageArea()).toContainText(INACTIVITY_MESSAGE, { | ||
| timeout: TERMINAL_IDLING_TIMEOUT, | ||
| }); | ||
| }); | ||
|
|
||
| await test.step('Verify restart button is shown', async () => { | ||
| const restartButton = page.locator('button', { hasText: 'Restart terminal' }); | ||
| await expect(restartButton).toBeVisible(); | ||
| }); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.