Skip to content
Merged
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
12 changes: 5 additions & 7 deletions frontend/e2e/clients/kubernetes-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface ClusterAuthConfig {
token?: string;
}

function isNotFound(err: unknown): boolean {
export function isNotFound(err: unknown): boolean {
if (typeof err === 'object' && err !== null) {
const statusCode = (err as any).statusCode ?? (err as any).response?.statusCode;
if (statusCode === 404) {
Expand Down Expand Up @@ -374,12 +374,10 @@ export default class KubernetesClient {
const existing = await this.k8sApi.readNamespacedConfigMap({ name, namespace });
const existingData = (existing as any)?.data || {};
const mergedData = { ...existingData, ...patchData };
await this.k8sApi.patchNamespacedConfigMap({
name,
namespace,
body: { data: mergedData },
contentType: k8s.PatchStrategy.MergePatch,
} as any);
await this.mergePatchResource(
`/api/v1/namespaces/${namespace}/configmaps/${name}`,
{ data: mergedData },
);
}

async createConfigMap(
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/pages/topology-sidebar-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class TopologySidebarPage extends BasePage {
}

async clickActionsDropdown(): Promise<void> {
await this.robustClick(this.actionsDropdown);
await this.robustClick(this.actionsDropdown, { timeout: 60_000 });
}

async selectAction(action: string): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion frontend/e2e/pages/web-terminal-config-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class WebTerminalConfigPage extends BasePage {

async clickWebTerminalTab(): Promise<void> {
const tab = this.page.getByRole('tab', { name: 'Web Terminal' });
await this.robustClick(tab);
await this.robustClick(tab, { timeout: 60_000 });
await this.waitForLoadingComplete(5_000);
}

Expand Down
74 changes: 36 additions & 38 deletions frontend/e2e/tests/console/app/poll-console-updates.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Frame, Page, Response, Route } from '@playwright/test';
import { test, expect } from '../../../fixtures';

const CHECK_UPDATES_URL = '**/api/check-updates';
Expand All @@ -19,14 +20,9 @@ const PLUGIN_MANIFEST_DEFAULT = { name: PLUGIN_NAME, version: '0.0.0' };
const PLUGIN_MANIFEST_DEFAULT2 = { name: PLUGIN_NAME2, version: '0.0.0' };
const PLUGIN_MANIFEST_NEW_VERSION = { name: PLUGIN_NAME, version: '1.0.0' };

type RouteHandler = (route: import('@playwright/test').Route) => void;
type RouteHandler = (route: Route) => void;

/**
* Creates a mutable route handler whose behavior can be swapped at runtime.
* This avoids Playwright's handler stacking issues (where calling page.route()
* multiple times on the same URL adds stacked handlers). Instead, we register
* ONE handler that delegates to a mutable reference.
*/
// Single handler avoids Playwright's handler stacking when re-routing the same URL.
function createMutableHandler(initial: RouteHandler) {
let current = initial;
const handler: RouteHandler = (route) => current(route);
Expand All @@ -36,46 +32,48 @@ function createMutableHandler(initial: RouteHandler) {
return { handler, setHandler };
}

/**
* Navigate to `/` and wait for the PollConsoleUpdates component to initialize.
*
* The component uses a prev/current ref pattern: it needs at least 2 poll
* cycles (~15s apart) before `stateInitialized` becomes true. In CI, OAuth
* redirects can cause multiple navigations (and component remounts) even
* after the dashboard appears. Each navigation resets the counter because
* a remount means the component starts fresh.
*/
async function navigateAndWaitForInit(page: import('@playwright/test').Page) {
await page.goto('/');
await expect(page.locator('[data-test-id="dashboard"]').first()).toBeVisible({ timeout: 60_000 });

// PollConsoleUpdates needs 2 poll cycles to initialize; main-frame navs reset the counter.
async function navigateAndWaitForInit(page: Page) {
let count = 0;
let initResolve: () => void;
let initReject: (err: Error) => void;
const initPromise = new Promise<void>((resolve, reject) => {
initResolve = resolve;
initReject = reject;
});
Comment on lines +38 to +43

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

Attach the no-op catch when initPromise is created, not in finally.

The 90s timer can reject initPromise while control is still inside page.goto() or the 60s dashboard-visibility wait. Until line 76 runs, that rejection has no handler, which can surface as an unhandled rejection. Attaching the guard at creation makes it unconditional.

🛡️ Proposed fix
   const initPromise = new Promise<void>((resolve, reject) => {
     initResolve = resolve;
     initReject = reject;
   });
+  // Guard against the timeout rejecting before `await initPromise` is reached.
+  initPromise.catch(() => {});
@@
   } finally {
     clearTimeout(timer);
     page.off('framenavigated', resetOnNav);
     page.off('response', onResponse);
-    initPromise.catch(() => {});
   }

Also applies to: 61-64, 76-76

🤖 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/tests/console/app/poll-console-updates.spec.ts` around lines 38
- 43, Attach a no-op catch to initPromise immediately when it is created, rather
than adding it in the later finally block, so rejections from the 90-second
timer are handled during page.goto and dashboard-visibility waits. Update the
relevant initPromise setup and remove the delayed finally-based guard while
preserving the existing rejection flow.


const resetOnNav = () => {
count = 0;
const resetOnNav = (frame: Frame) => {
if (frame === page.mainFrame()) count = 0;
};
const onResponse = (resp: Response) => {
if (resp.url().includes('/api/check-updates') && resp.status() === 200) {
count++;
if (count >= 2) {
page.off('response', onResponse);
initResolve();
}
}
};

page.on('framenavigated', resetOnNav);
page.on('response', onResponse);

const timer = setTimeout(() => {
page.off('response', onResponse);
initReject(new Error(`navigateAndWaitForInit: only saw ${count}/2 responses`));
}, 90_000);

try {
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error(`navigateAndWaitForInit: only saw ${count}/2 responses`)),
90_000,
);
const handler = (resp: import('@playwright/test').Response) => {
if (resp.url().includes('/api/check-updates') && resp.status() === 200) {
count++;
if (count >= 2) {
clearTimeout(timer);
page.off('response', handler);
resolve();
}
}
};
page.on('response', handler);
await page.goto('/');
await expect(page.locator('[data-test-id="dashboard"]').first()).toBeVisible({
timeout: 60_000,
});
await initPromise;
} finally {
clearTimeout(timer);
page.off('framenavigated', resetOnNav);
page.off('response', onResponse);
initPromise.catch(() => {});
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down

This file was deleted.

Loading