1// @effect-diagnostics nodeBuiltinImport:off - plain Node script CI runs against the built client.
2// Boots the built web client in headless Chromium and fails if it reaches showBootError.
3// Source-level browser tests never run the production chunk graph, where boot breaks hide.
4import * as NodeFS from "node:fs";
5import * as NodePath from "node:path";
6import * as NodeURL from "node:url";
7import { chromium } from "playwright";
8
9const distDir = NodeURL.fileURLToPath(new URL("../dist/", import.meta.url));
10const origin = "http://t3-boot.test";
11// With no server, the client retries its session for about 15s before it renders.
12const timeoutMs = 60_000;
13
14if (!NodeFS.existsSync(NodePath.join(distDir, "index.html"))) {
15 console.error(`No web build at ${distDir}. Run the web build first.`);
16 process.exit(1);
17}
18
19const browser = await chromium.launch();
20try {
21 const page = await browser.newPage();
22 const errors: string[] = [];
23 page.on("pageerror", (error) => errors.push(error.stack ?? error.message));
24 page.on("console", (message) => {
25 if (message.type() === "error") errors.push(message.text());
26 });
27
28 // Serve dist with an SPA fallback; the client has no server here, so other requests fail fast.
29 await page.route("**/*", (route) => {
30 const url = new URL(route.request().url());
31 if (url.origin !== origin) return route.abort();
32 const file = NodePath.join(distDir, decodeURIComponent(url.pathname));
33 const isAsset =
34 file.startsWith(distDir) && NodeFS.statSync(file, { throwIfNoEntry: false })?.isFile();
35 if (isAsset) return route.fulfill({ path: file });
36 if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/.well-known/")) {
37 return route.fulfill({ status: 503 });
38 }
39 return route.fulfill({ path: NodePath.join(distDir, "index.html") });
40 });
41
42 await page.goto(`${origin}/`);
43 // React replaces the static #boot-shell splash when it mounts; showBootError fills it instead.
44 const settled = await page.waitForSelector("#root > :not(#boot-shell), #boot-error-reload", {
45 timeout: timeoutMs,
46 });
47
48 if (await settled.evaluate((node) => node.id === "boot-error-reload")) {
49 console.error("The built web client failed to boot:");
50 for (const error of errors) console.error(error);
51 process.exitCode = 1;
52 } else {
53 console.log("The built web client booted and mounted React.");
54 }
55} finally {
56 await browser.close();
57}