Files
nexu-io__open-design/e2e/lib/playwright/runtime-lifecycle.ts
Marc Chan a1995a186a test(e2e): harden UI flake waits with real completion signals (#6539)
* test(e2e): harden UI flake waits with real completion signals

Replace synthetic /projects routing, blind boot sleeps, and call-site
create retries with observable readiness: real navigation, suite daemon
health warmup, expect.poll, clock fast-forward for coalesce TTL, and
quiescence for AMR refresh traffic.

* test(e2e): raise Playwright fixture timeout for daemon warmup

The suite fixture now polls daemon /api/health for up to 60s, which
consumed the prior 390s budget's bookkeeping margin. Raise the worker
fixture timeout to 450s so start + web/daemon warmup + stop still leave
a full minute for teardown bookkeeping on slow CI workers.

Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)

* test(e2e): fail closed on health body drain and narrow billing mock

Require warmPlaywrightDaemonRuntime to fully consume ok health bodies
before declaring readiness, drain non-ok probes, and fulfill only the
exact /api/workspace/billing path so nested billing routes fall through.

Generated-By: looper 0.11.8 (runner=fixer, agent=grok-build)

* test(e2e): reopen projects by id in cross-project file-tab restore

Avoid relying on chrome workspace tabs surviving leave+create hard
navigation; assert per-project file-tab restore via project routes.
2026-08-07 10:37:59 +08:00

86 lines
2.9 KiB
TypeScript

// Reserve an extra minute beyond start + web/daemon warmup + stop budgets so
// Playwright can finish fixture bookkeeping without cutting teardown short.
// Budgets: 180s start + 120s web warmup + 60s daemon warmup + 30s stop + 60s
// bookkeeping = 450s.
export const PLAYWRIGHT_TOOLS_DEV_FIXTURE_TIMEOUT_MS = 450_000;
export const PLAYWRIGHT_WEB_WARMUP_TIMEOUT_MS = 120_000;
export const PLAYWRIGHT_DAEMON_WARMUP_TIMEOUT_MS = 60_000;
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
export async function warmPlaywrightWebRuntime(
url: string,
options: {
fetch?: FetchLike;
timeoutMs?: number;
} = {},
): Promise<void> {
const fetchRuntime = options.fetch ?? fetch;
const timeoutMs = options.timeoutMs ?? PLAYWRIGHT_WEB_WARMUP_TIMEOUT_MS;
const signal = AbortSignal.timeout(timeoutMs);
let response: Response;
try {
response = await fetchRuntime(url, { signal });
} catch (error) {
if (signal.aborted) {
throw new Error(`Playwright web warmup timed out after ${timeoutMs}ms`, { cause: error });
}
throw error;
}
if (!response.ok) {
throw new Error(`Playwright web warmup failed with ${response.status} ${response.statusText}`);
}
await response.arrayBuffer();
}
/**
* Block the Playwright worker until the daemon HTTP surface can serve
* `/api/health`. The suite previously only warmed the web HTML shell, so the
* first API-backed action in a worker (usually `POST /api/projects`) raced
* daemon listen readiness and each call site invented its own retry loop.
*/
export async function warmPlaywrightDaemonRuntime(
healthUrl: string,
options: {
fetch?: FetchLike;
timeoutMs?: number;
intervalMs?: number;
} = {},
): Promise<void> {
const fetchRuntime = options.fetch ?? fetch;
const timeoutMs = options.timeoutMs ?? PLAYWRIGHT_DAEMON_WARMUP_TIMEOUT_MS;
const intervalMs = options.intervalMs ?? 100;
const deadline = Date.now() + timeoutMs;
let lastError: unknown = null;
while (Date.now() < deadline) {
try {
const response = await fetchRuntime(healthUrl, {
signal: AbortSignal.timeout(Math.max(250, Math.min(2_000, deadline - Date.now()))),
});
if (response.ok) {
// Drain the body so keep-alive sockets stay reusable under Node fetch.
// Body-drain failures must retry — a truncated 2xx is not readiness.
await response.arrayBuffer();
return;
}
// Consume non-OK bodies so repeated 503 probes do not leave streams open.
await response.arrayBuffer().catch(() => undefined);
lastError = new Error(
`Playwright daemon warmup failed with ${response.status} ${response.statusText}`,
);
} catch (error) {
lastError = error;
}
await new Promise<void>((resolve) => {
setTimeout(resolve, intervalMs);
});
}
throw new Error(`Playwright daemon warmup timed out after ${timeoutMs}ms`, {
cause: lastError,
});
}