1/**
2 * Headless end-to-end check of the built site: serves dist/, loads the
3 * page in headless Chrome, fails on any console error, and exercises one
4 * real in-browser solve through the worker (the Solution section),
5 * asserting the reported error is small. Visual appearance is checked by
6 * a human, not here.
7 *
8 * Usage: npm run build && node scripts/check-app.mjs
9 */
10import { createServer } from "node:http";
11import { readFile } from "node:fs/promises";
12import { join, extname } from "node:path";
13import puppeteer from "puppeteer-core";
15const root = new URL("../dist", import.meta.url).pathname;
16const types = {
17 ".html": "text/html",
18 ".js": "text/javascript",
19 ".css": "text/css",
20 ".wasm": "application/wasm",
21 ".json": "application/json",
22};
24const server = createServer(async (req, res) => {
25 const path = req.url === "/" ? "/index.html" : (req.url ?? "/").split("?")[0];
26 try {
27 const data = await readFile(join(root, path));
28 res.writeHead(200, { "content-type": types[extname(path)] ?? "application/octet-stream" });
29 res.end(data);
30 } catch {
31 res.writeHead(404);
32 res.end("not found");
33 }
34});
35await new Promise((resolve) => server.listen(0, resolve));
36const port = server.address().port;
38const browser = await puppeteer.launch({
39 executablePath: process.env.CHROME_PATH ?? "/usr/bin/google-chrome",
40 args: ["--no-sandbox"],
41});
43const errors = [];
44let failures = 0;
45try {
46 const page = await browser.newPage();
47 page.on("console", (msg) => {
48 if (msg.type() === "error") {
49 const text = msg.text();
50 const url = msg.location()?.url ?? "";
51 // The committed-results fetch may fail offline, or while the
52 // results site is unavailable; the app reports that in the UI by
53 // design. CORS complaints name the URL in the message text rather
54 // than in the location, so both are checked.
55 const RESULTS_HOST = "github.io/fastandaccurate-results";
56 if (url.includes(RESULTS_HOST) || text.includes(RESULTS_HOST)) return;
57 // numbl logs its linear-algebra bridge choice at error level.
58 if (text.includes("using bridge:")) return;
59 errors.push(`${text} (${url})`);
60 }
61 });
62 page.on("pageerror", (err) => errors.push(String(err)));
64 await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: "networkidle2" });
65 await page.waitForSelector("h1");
67 // Home: the problem list with a clickable card.
68 const card = await page.$('a[href="#/problem/laplace-dirichlet-2d"]');
69 if (!card) {
70 console.error("FAIL: problem card missing on home page");
71 failures++;
72 } else {
73 await card.click();
74 }
75 await page.waitForFunction(
76 () => document.body.innerText.includes("Work-precision results"),
77 { timeout: 30000 }
78 );
80 // One real solve through the worker: the Solution section's compute
81 // button, default solver (mfs) at its default n.
82 const buttons = await page.$$("button");
83 let computeBtn = null;
84 for (const b of buttons) {
85 const t = await b.evaluate((el) => el.textContent);
86 if (t && t.includes("Compute in this browser")) computeBtn = b;
87 }
88 if (!computeBtn) {
89 console.error("FAIL: compute button not found");
90 failures++;
91 } else {
92 await computeBtn.click();
93 await page.waitForFunction(
94 () => document.body.innerText.includes("rel max error"),
95 { timeout: 120000 }
96 );
97 const text = await page.evaluate(() => document.body.innerText);
98 const m = text.match(/rel max error ([0-9.]+e[+-][0-9]+)/);
99 if (!m) {
100 console.error("FAIL: no reported error after compute");
101 failures++;
102 } else {
103 const err = parseFloat(m[1]);
104 // The default instance is the hard one, where every method still has
105 // a sizeable error at a moderate n, so this is a sanity bound rather
106 // than an accuracy claim: it separates a real solve from a broken or
107 // placeholder one (which would give an O(1) error or NaN).
108 if (!(err > 0 && err < 1e-2)) {
109 console.error(`FAIL: in-browser solve error ${err} not in (0, 1e-2)`);
110 failures++;
111 } else {
112 console.log(`in-browser solve ok (rel max error ${m[1]})`);
113 }
114 }
115 }
117 if (errors.length > 0) {
118 console.error("FAIL: console errors:");
119 for (const e of errors) console.error(` ${e}`);
120 failures++;
121 }
122} finally {
123 await browser.close();
124 server.close();
125}
127if (failures > 0) {
128 console.error(`${failures} failure(s)`);
129 process.exit(1);
130}
131console.log("check-app: all checks passed");