test(ui-e2e): scale Control UI wait budget for loaded CI runners (#123623)

ui-e2e intermittently failed with `page.waitForFunction: Timeout 10000ms
exceeded` on loaded CI runners (model-agent-scoping, terminal-embedded),
both green on rerun. The 10s deadlines were repo-imposed at the mock
Gateway helper and readiness helper, plus twelve per-test
setDefaultTimeout sprinkles.

Give the harness one owner for the wait budget: 30s under CI, 10s
locally, mirroring the ui-e2e vitest config's existing expect.poll
reasoning. Suite-created contexts inherit it, so two per-test overrides
are deleted outright.
This commit is contained in:
Peter Steinberger
2026-08-14 05:20:45 -07:00
committed by GitHub
parent 5c3fd2fbe9
commit c9152fef4e
13 changed files with 40 additions and 20 deletions
+6 -2
View File
@@ -6,7 +6,11 @@ import type {
AllowedApprovalSnapshot,
PendingApprovalSnapshot,
} from "../../../packages/gateway-protocol/src/index.js";
import { installMockGateway, type MockGatewayControls } from "../test-helpers/control-ui-e2e.ts";
import {
controlUiE2eWaitTimeoutMs,
installMockGateway,
type MockGatewayControls,
} from "../test-helpers/control-ui-e2e.ts";
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
const suite = createControlUiE2eSuite({
@@ -109,7 +113,7 @@ async function createSurface(params: {
});
openContexts.add(context);
const page = await context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
await page.clock.setFixedTime(new Date(APPROVAL_NOW_MS));
const pageErrors: string[] = [];
page.on("pageerror", (error) => pageErrors.push(String(error)));
+3 -2
View File
@@ -5,6 +5,7 @@ import { chromium, type Browser } from "playwright";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
canRunPlaywrightChromium,
controlUiE2eWaitTimeoutMs,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
@@ -38,7 +39,7 @@ describeControlUiE2e("Control UI chat file links", () => {
viewport: { height: 900, width: 1280 },
});
const page = await context.newPage();
page.setDefaultTimeout(15_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
try {
const gateway = await installMockGateway(page, {
historyMessages: [
@@ -192,7 +193,7 @@ describeControlUiE2e("Control UI chat file links", () => {
});
try {
const page = await context.newPage();
page.setDefaultTimeout(15_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
const gateway = await installMockGateway(page, {
methodResponses: {
"sessions.files.get": {
@@ -7,6 +7,7 @@ import { chromium, type Browser, type BrowserContext, type Page } from "playwrig
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
canRunPlaywrightChromium,
controlUiE2eWaitTimeoutMs,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
@@ -132,7 +133,7 @@ async function newRecordedPage(label: string): Promise<RecordedPage> {
viewport,
});
page = await context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
return { browser, context, page, rawVideoDir };
} catch (error) {
await page?.close().catch(() => {});
+2 -2
View File
@@ -3,7 +3,7 @@
import path from "node:path";
import type { BrowserContext, Page } from "playwright";
import { expect, it } from "vitest";
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
import { controlUiE2eWaitTimeoutMs, installMockGateway } from "../test-helpers/control-ui-e2e.ts";
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
const suite = createControlUiE2eSuite({
@@ -123,7 +123,7 @@ async function openChat(
viewport: { height: 900, width: 1280 },
});
page = await context.newPage();
page.setDefaultTimeout(15_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
const gateway = await installMockGateway(page, {
deferredMethods,
methodResponses: { "models.authStatus": authStatus, ...extraMethodResponses },
@@ -16,6 +16,7 @@ import {
} from "../../../src/test-utils/openclaw-test-state.js";
import {
canRunPlaywrightChromium,
controlUiE2eWaitTimeoutMs,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
type ControlUiE2eServer,
@@ -470,7 +471,7 @@ async function createBrowserPage(
const page = await context.newPage();
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(String(error)));
page.setDefaultTimeout(15_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
const evidenceStartIndex = proxy.evidence.length;
const response = await page.goto(withGatewayUrl(baseUrl, gatewayUrl), {
timeout: controlUiSettleTimeoutMs,
@@ -7,6 +7,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { ConnectErrorDetailCodes } from "../../../packages/gateway-protocol/src/connect-error-details.js";
import {
canRunPlaywrightChromium,
controlUiE2eWaitTimeoutMs,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
@@ -51,7 +52,7 @@ async function createCredentialPage(): Promise<{
});
openContexts.add(context);
const page = await context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
const gateway = await installMockGateway(page, { deferredMethods: ["connect"] });
const response = await page.goto(server.baseUrl);
expect(response?.status()).toBe(200);
@@ -2,6 +2,7 @@ import { chromium, type Browser, type BrowserContext, type Page } from "playwrig
import { afterAll, afterEach, beforeAll, describe } from "vitest";
import {
canRunPlaywrightChromium,
controlUiE2eWaitTimeoutMs,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
type ControlUiE2eServer,
@@ -57,6 +58,8 @@ export function createControlUiE2eSuite(options: ControlUiE2eSuiteOptions): Cont
throw new Error("Control UI E2E browser accessed before suite setup");
}
const context = await browser.newContext(contextOptions);
// Harness owns the wait budget; per-test setDefaultTimeout sprinkles defeat CI scaling.
context.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
openBrowserContexts.add(context);
return context;
};
@@ -7,6 +7,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { ConnectErrorDetailCodes } from "../../../packages/gateway-protocol/src/connect-error-details.js";
import {
canRunPlaywrightChromium,
controlUiE2eWaitTimeoutMs,
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
@@ -34,7 +35,7 @@ async function createPage(): Promise<Page> {
});
openContexts.add(context);
const page = await context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
return page;
}
@@ -236,8 +236,6 @@ suite.define(() => {
viewport: { height: 900, width: 1440 },
},
async ({ page }) => {
page.setDefaultTimeout(10_000);
const gateway = await installMockGateway(page, {
historyMessages: [
{
@@ -407,8 +405,6 @@ suite.define(() => {
viewport: { height: 900, width: 1440 },
},
async ({ page }) => {
page.setDefaultTimeout(10_000);
const gateway = await installMockGateway(page, {
assistantAgentId: "work",
defaultAgentId: "work",
@@ -3,7 +3,11 @@ import path from "node:path";
import type { BrowserContext, Page } from "playwright";
import { expect, it } from "vitest";
import { createControlUiE2eSuite } from "../../e2e/control-ui-e2e-suite.test-support.ts";
import { installMockGateway, waitForControlUiRoute } from "../../test-helpers/control-ui-e2e.ts";
import {
controlUiE2eWaitTimeoutMs,
installMockGateway,
waitForControlUiRoute,
} from "../../test-helpers/control-ui-e2e.ts";
const suite = createControlUiE2eSuite({
name: "Control UI Workboard routing",
@@ -64,7 +68,7 @@ async function newRecordedPage(label: string): Promise<{
viewport: { width: 1600, height: 1000 },
});
const page = await context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
return { context, page, rawVideoDir };
}
+2 -1
View File
@@ -14,6 +14,7 @@ import type {
WorkboardStatus,
} from "../../lib/workboard/index.ts";
import {
controlUiE2eWaitTimeoutMs,
installMockGateway,
type MockGatewayControls,
type MockGatewayRequest,
@@ -302,7 +303,7 @@ async function newRecordedPage(label: string): Promise<RecordedPage> {
viewport,
});
page = await context.newPage();
page.setDefaultTimeout(10_000);
page.setDefaultTimeout(controlUiE2eWaitTimeoutMs);
return { context, page, rawVideoDir };
} catch (error) {
await page?.close().catch(() => {});
@@ -1,4 +1,5 @@
import type { Page } from "playwright";
import { controlUiE2eWaitTimeoutMs } from "./control-ui-e2e.ts";
/** A sent connect request is not the delivered Gateway handshake. */
export async function waitForControlUiGatewayReady(page: Page): Promise<void> {
@@ -23,11 +24,11 @@ export async function waitForControlUiGatewayReconnecting(page: Page): Promise<v
return app?.runtime?.context?.gateway?.snapshot?.phase === "reconnecting";
},
undefined,
{ timeout: 10_000 },
{ timeout: controlUiE2eWaitTimeoutMs },
),
page
.locator(".sidebar-identity-card__status", { hasText: "Reconnecting…" })
.waitFor({ state: "visible", timeout: 10_000 }),
.waitFor({ state: "visible", timeout: controlUiE2eWaitTimeoutMs }),
]);
}
+7 -1
View File
@@ -81,6 +81,12 @@ type ControlUiRouteTarget = {
// wait browser-local, but allow enough time for the router to finish committing.
const CONTROL_UI_ROUTE_TIMEOUT_MS = 60_000;
// Loaded CI runners regularly stall real Chromium renders past 10s; the larger
// CI budget trades failure latency, not coverage (mirrors the ui-e2e vitest
// config's expect.poll budget). Local runs keep the snappy 10s deadline.
export const controlUiE2eWaitTimeoutMs =
process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true" ? 30_000 : 10_000;
/**
* Wait for the browser router to commit a route, not merely update the URL.
* Browser-local polling keeps readiness independent of host-side CDP scheduling.
@@ -2268,7 +2274,7 @@ function createMockGatewayControls(page: Page, defaultSessionKey: string): MockG
return Boolean(gateway?.requests.some((request) => request.method === targetMethod));
},
method,
{ timeout: 10_000 },
{ timeout: controlUiE2eWaitTimeoutMs },
);
const requests = await getRequests(method);
const request = requests.at(-1);