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
5 changes: 5 additions & 0 deletions .changeset/flaky-chrome-launch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudhal/cli": patch
---

`render_page` on Node: a Chrome that hangs or crashes on a cold start is launched once more with a fresh profile instead of failing the render.
20 changes: 15 additions & 5 deletions packages/cli/src/render-chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ export function chromeRenderer(
);
const endpoint = new Promise<string>((resolve, reject) => {
let err = "";
// A cold start on a busy CI runner can take well over 10 s; a warm one is under a second.
const timer = setTimeout(() => reject(new Error("Chrome did not start")), 30_000);
// A warm start is under a second, a cold one on a busy CI runner can take over 10 s; one that
// hangs past this is killed and launched once more (session()).
const timer = setTimeout(() => reject(new Error("Chrome did not start")), 20_000);
child.stderr?.on("data", (chunk: Buffer) => {
err += chunk.toString();
const m = /DevTools listening on (ws:\/\/\S+)/.exec(err);
Expand All @@ -108,15 +109,24 @@ export function chromeRenderer(
});
endpoint.catch(() => {});
chrome = { process: child, endpoint, dir };
process.once("exit", () => child.kill());
const killOnExit = () => child.kill();
process.once("exit", killOnExit);
child.once("exit", () => process.off("exit", killOnExit));
return chrome;
};

return {
async session() {
clearTimeout(idle);
const { endpoint } = chrome ?? start();
const ws = new WebSocket(await endpoint);
let url: string;
try {
url = await (chrome ?? start()).endpoint;
} catch {
// A stuck or crashed cold start: one fresh launch with a new profile.
stop();
url = await start().endpoint;
}
const ws = new WebSocket(url);
await new Promise<void>((resolve, reject) => {
ws.addEventListener("open", () => resolve(), { once: true });
ws.addEventListener("error", () => reject(new Error("could not reach Chrome")), {
Expand Down
24 changes: 23 additions & 1 deletion packages/cli/test/render-chrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ describe.skipIf(!chrome)("render_page checks in Chrome", () => {
expect(check("landmarks")?.examples).toEqual(
expect.arrayContaining(["@1440: the page — 0 page headers (one is right)"]),
);
}, 60_000);
// The first test starts Chrome: up to two 20 s launch attempts (render-chrome.ts) plus the render.
}, 120_000);

it("checks the chrome: skip link, landmarks, a transparent header's contrast, focus under it", async () => {
dir ??= await mkdtemp(path.join(os.tmpdir(), "cloudhal-render-test-"));
Expand Down Expand Up @@ -280,3 +281,24 @@ describe("localRender", () => {
expect(localRender({ CHROME_PATH: "/nonexistent/chrome" })).toBeUndefined();
});
});

describe("chromeRenderer", () => {
it.skipIf(process.platform === "win32")(
"launches a Chrome that fails to start once more, with a new profile",
async () => {
const tmp = await mkdtemp(path.join(os.tmpdir(), "cloudhal-render-retry-"));
const log = path.join(tmp, "launches");
const fake = path.join(tmp, "chrome");
await writeFile(fake, `#!/bin/sh\necho "$@" >> "${log}"\nexit 1\n`, { mode: 0o755 });
try {
await expect(chromeRenderer(fake).session()).rejects.toThrow("Chrome exited");
const launches = (await readFile(log, "utf8")).trim().split("\n");
expect(launches).toHaveLength(2);
const profile = (line?: string) => /--user-data-dir=(\S+)/.exec(line ?? "")?.[1];
expect(profile(launches[0])).not.toBe(profile(launches[1]));
} finally {
await rm(tmp, { recursive: true, force: true });
}
},
);
});
3 changes: 3 additions & 0 deletions packages/ui/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ export default defineConfig({
environmentOptions: { happyDOM: { settings: { disableIframePageLoading: true } } },
include: ["test/**/*.test.tsx"],
setupFiles: ["./test/setup.ts"],
// The first test to open a lazy kit (the calendar, charts) pays its cold import; on a CI runner
// shared with the heavy suites that went past Vitest's 5 s default. Same limit as Studio.
testTimeout: 20_000,
},
});
Loading