Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions frontend/e2e/pages/web-terminal-config-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class WebTerminalConfigPage extends BasePage {

async navigateToWebTerminalConfig(): Promise<void> {
await this.goTo('/k8s/cluster/operator.openshift.io~v1~Console/cluster');
await this.waitForLoadingComplete(10_000);
await this.waitForLoadingComplete(30_000);
const customizeButton = this.page.getByRole('button', { name: 'Customize' });
// eslint-disable-next-line no-restricted-syntax
await customizeButton
Expand All @@ -25,17 +25,17 @@ export class WebTerminalConfigPage extends BasePage {
await this.robustClick(customizeButton.first());
} else {
const actionsMenu = this.page.getByTestId('actions-menu-button');
await this.robustClick(actionsMenu);
await this.robustClick(actionsMenu, { timeout: 60_000 });
const customizeAction = this.page.locator('[data-test-action="Customize"]:not([disabled])');
await this.robustClick(customizeAction);
}
await this.waitForLoadingComplete(10_000);
await this.waitForLoadingComplete(30_000);
await this.clickWebTerminalTab();
}

async clickWebTerminalTab(): Promise<void> {
const tab = this.page.getByRole('tab', { name: 'Web Terminal' });
await this.robustClick(tab, { timeout: 60_000 });
await this.robustClick(tab, { timeout: 60_000, retries: 1 });
Comment on lines 36 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Set the attempt count for one retry.

Line 38 passes retries: 1. BasePage.robustClick uses this value as the total attempt count. The call therefore has no retry.

Pass retries: 2 to allow the initial click and one retry.

Proposed fix
-    await this.robustClick(tab, { timeout: 60_000, retries: 1 });
+    await this.robustClick(tab, { timeout: 60_000, retries: 2 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async clickWebTerminalTab(): Promise<void> {
const tab = this.page.getByRole('tab', { name: 'Web Terminal' });
await this.robustClick(tab, { timeout: 60_000 });
await this.robustClick(tab, { timeout: 60_000, retries: 1 });
async clickWebTerminalTab(): Promise<void> {
const tab = this.page.getByRole('tab', { name: 'Web Terminal' });
await this.robustClick(tab, { timeout: 60_000, retries: 2 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/web-terminal-config-page.ts` around lines 36 - 38, Update
clickWebTerminalTab to pass retries: 2 to BasePage.robustClick, ensuring the Web
Terminal tab click gets one retry after the initial attempt.

await this.waitForLoadingComplete(5_000);
}

Expand Down
26 changes: 6 additions & 20 deletions frontend/e2e/pages/web-terminal-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,12 @@ export class WebTerminalPage extends BasePage {
private readonly closeTerminalButton = this.page.getByLabel(/Close terminal/);
private readonly inactivityMessageArea = this.page.locator('div.co-cloudshell-exec__error-msg');

async waitForTerminalIconVisible(maxRetries = 10): Promise<void> {
async waitForTerminalIconVisible(): Promise<void> {
await warmupSPA(this.page);
try {
// eslint-disable-next-line no-restricted-syntax
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 {
// eslint-disable-next-line no-restricted-syntax
await this.terminalIcon.waitFor({ state: 'visible', timeout: 15_000 });
return;
} catch {
// Retry
}
}
throw new Error(`Terminal icon not visible after ${maxRetries} retries`);
await expect(async () => {
await this.page.reload({ waitUntil: 'domcontentloaded' });
await expect(this.terminalIcon).toBeVisible({ timeout: 15_000 });
}).toPass({ intervals: [2_000, 5_000, 10_000], timeout: 120_000 });
}

async clickTerminalIcon(): Promise<void> {
Expand All @@ -54,7 +40,7 @@ export class WebTerminalPage extends BasePage {
await this.loadingBox.waitFor({ state: 'detached', timeout: 60_000 }).catch(() => {});
}

async waitForTerminalWindow(timeoutMs = 60_000): Promise<void> {
async waitForTerminalWindow(timeoutMs = 120_000): Promise<void> {
await expect(this.terminalContainer).toBeVisible({ timeout: timeoutMs });
await expect(this.terminalWindow).toBeVisible({ timeout: timeoutMs });
}
Expand Down
9 changes: 3 additions & 6 deletions frontend/e2e/setup/admin-auth.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ import * as path from 'path';

import { test as setup } from '@playwright/test';

import { performLogin, saveStorageState } from './login-helper';
import { getAdminCredentials, performLogin, saveStorageState } from './login-helper';

const adminStorageState = path.resolve(import.meta.dirname, '..', '.auth', 'kubeadmin.json');

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');
const { username, password, idpName } = getAdminCredentials();
await performLogin(page, username, password, idpName);
await saveStorageState(page, adminStorageState);
});
13 changes: 4 additions & 9 deletions frontend/e2e/setup/developer-auth.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@ import * as path from 'path';

import { test as setup } from '@playwright/test';

import { performLogin, saveStorageState } from './login-helper';
import { getDeveloperCredentials, performLogin, saveStorageState } from './login-helper';

const developerStorageState = path.resolve(import.meta.dirname, '..', '.auth', 'developer.json');

setup('login as developer', async ({ page }) => {
setup.skip(process.env.SKIP_GLOBAL_SETUP === 'true', 'SKIP_GLOBAL_SETUP is set');

const htpasswdUser = process.env.BRIDGE_HTPASSWD_USERNAME;
const htpasswdPass = process.env.BRIDGE_HTPASSWD_PASSWORD;
const creds = getDeveloperCredentials();
setup.skip(!creds, 'No developer credentials configured');

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 performLogin(page, creds!.username, creds!.password, creds!.idpName);
await saveStorageState(page, developerStorageState);
});
30 changes: 28 additions & 2 deletions frontend/e2e/setup/login-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,40 @@ import { expect } from '@playwright/test';

const STORAGE_STATE_DIR = path.resolve(import.meta.dirname, '..', '.auth');

export function getBaseURL(): string {
return process.env.WEB_CONSOLE_URL || 'http://localhost:9000';
}

export function getAdminCredentials(): { username: string; password: string; idpName: string } {
return {
username: process.env.OPENSHIFT_USERNAME || 'kubeadmin',
password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '',
idpName: 'kube:admin',
};
}

export function getDeveloperCredentials(): {
username: string;
password: string;
idpName: string;
} | null {
const username = process.env.BRIDGE_HTPASSWD_USERNAME;
const password = process.env.BRIDGE_HTPASSWD_PASSWORD;
if (!username || !password) return null;
return {
username,
password,
idpName: process.env.BRIDGE_HTPASSWD_IDP || username,
};
}

export async function performLogin(
page: Page,
baseURL: string,
username: string,
password: string,
idpName?: string,
): Promise<void> {
await page.goto(baseURL, { timeout: 90_000, waitUntil: 'domcontentloaded' });
await page.goto(getBaseURL(), { timeout: 90_000, waitUntil: 'domcontentloaded' });

const authDisabled = await page
.evaluate(() => (window as any).SERVER_FLAGS?.authDisabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ test.describe('Web Terminal basic user', () => {
});

test('open terminal with advanced timeout', async ({ page }) => {
test.slow();
const webTerminal = new WebTerminalPage(page);

await test.step('Open terminal with 1-minute timeout', async () => {
Expand All @@ -45,6 +46,7 @@ test.describe('Web Terminal basic user', () => {
});

test('verify Open in new tab button', async ({ page }) => {
test.slow();
const webTerminal = new WebTerminalPage(page);

await test.step('Wait for terminal icon and open terminal', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ test.describe('Web Terminal for Developer user', () => {
test(
'create new project and use Web Terminal',
async ({ page, k8sClient, cleanup }) => {
test.slow();
const webTerminal = new WebTerminalPage(page);
cleanup.trackNamespace(NEW_PROJECT);

Expand Down Expand Up @@ -88,6 +89,7 @@ test.describe('Web Terminal for Developer user', () => {

// eslint-disable-next-line playwright/expect-expect
test('open Web Terminal for existing project', async ({ page, k8sClient }) => {
test.slow();
const webTerminal = new WebTerminalPage(page);

await test.step('Wait for terminal icon and open terminal', async () => {
Expand Down
5 changes: 4 additions & 1 deletion frontend/e2e/tests/webterminal/web-terminal-admin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Page } from '@playwright/test';

import { test, expect } from '../../fixtures';
import type KubernetesClient from '../../clients/kubernetes-client';
import { getEditorContent, warmupSPA } from '../../pages/base-page';
import { getEditorContent } from '../../pages/base-page';
import { WebTerminalPage } from '../../pages/web-terminal-page';
import {
ensureWebTerminalOperatorInstalled,
Expand Down Expand Up @@ -88,6 +88,7 @@ test.describe('Web Terminal for Admin user', () => {
test(
'open and close multiple terminal tabs',
async ({ page }) => {
test.slow();
const webTerminal = new WebTerminalPage(page);

await test.step('Wait for terminal icon and start terminal', async () => {
Expand Down Expand Up @@ -120,6 +121,7 @@ test.describe('Web Terminal for Admin user', () => {
test(
'start terminal with timeout and verify DevWorkspace',
async ({ page, k8sClient }) => {
test.slow();
const webTerminal = new WebTerminalPage(page);

await test.step('Open terminal with 10-minute timeout', async () => {
Expand All @@ -139,6 +141,7 @@ test.describe('Web Terminal for Admin user', () => {
test(
'start terminal with defaults and verify DevWorkspace',
async ({ page, k8sClient }) => {
test.slow();
const webTerminal = new WebTerminalPage(page);

await test.step('Open terminal with default settings', async () => {
Expand Down
5 changes: 5 additions & 0 deletions frontend/e2e/tests/webterminal/web-terminal-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ test.describe('Customization of web terminal options', () => {
test(
'navigate to Web Terminal Configuration page',
async ({ page }) => {
test.slow();
const configPage = new WebTerminalConfigPage(page);

await test.step('Navigate to Consoles and open Customize', async () => {
Expand All @@ -42,6 +43,7 @@ test.describe('Customization of web terminal options', () => {
test(
'change timeout and image with persist checkboxes',
async ({ page }) => {
test.slow();
const configPage = new WebTerminalConfigPage(page);

await test.step('Navigate to Web Terminal Configuration', async () => {
Expand All @@ -68,6 +70,7 @@ test.describe('Customization of web terminal options', () => {
test(
'change timeout to Hours and verify values persist after tab switch',
async ({ page }) => {
test.slow();
const configPage = new WebTerminalConfigPage(page);

await test.step('Navigate to Web Terminal Configuration', async () => {
Expand Down Expand Up @@ -98,6 +101,7 @@ test.describe('Customization of web terminal options', () => {
test(
'save without persist checkboxes',
async ({ page }) => {
test.slow();
const configPage = new WebTerminalConfigPage(page);

await test.step('Navigate to Web Terminal Configuration', async () => {
Expand All @@ -121,6 +125,7 @@ test.describe('Customization of web terminal options', () => {
test(
'verify unchecked checkboxes persist after tab switch',
async ({ page }) => {
test.slow();
const configPage = new WebTerminalConfigPage(page);

await test.step('Navigate to Web Terminal Configuration', async () => {
Expand Down