5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 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 404 before the results repo has
52 // content; the app reports that in the UI by design.
53 if (url.includes("raw.githubusercontent.com")) return;
54 // numbl logs its linear-algebra bridge choice at error level.
55 if (text.includes("using bridge:")) return;
56 errors.push(`${text} (${url})`);
57 }
58 });
59 page.on("pageerror", (err) => errors.push(String(err)));
61 await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: "networkidle2" });
62 await page.waitForSelector("h1");
404de06Split into home / problem / about pages; minimal landing with problems listJeremy Magland 64 // Home: the problem list with a clickable card.
65 const card = await page.$('a[href="#/problem/laplace-dirichlet-2d"]');
66 if (!card) {
67 console.error("FAIL: problem card missing on home page");
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 68 failures++;
404de06Split into home / problem / about pages; minimal landing with problems listJeremy Magland 69 } else {
70 await card.click();
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 71 }
404de06Split into home / problem / about pages; minimal landing with problems listJeremy Magland 72 await page.waitForFunction(
73 () => document.body.innerText.includes("Work-precision results"),
74 { timeout: 30000 }
75 );
5127df5fastandaccurate: PDE solver benchmarks, laplace-dirichlet-2d with MFS and Nystrom DLP solversJeremy Magland 76
77 // One real solve through the worker: the Solution section's compute
78 // button, default solver (mfs) at its default n.
79 const buttons = await page.$$("button");
80 let computeBtn = null;
81 for (const b of buttons) {
82 const t = await b.evaluate((el) => el.textContent);
83 if (t && t.includes("Compute in this browser")) computeBtn = b;
84 }
85 if (!computeBtn) {
86 console.error("FAIL: compute button not found");
87 failures++;
88 } else {
89 await computeBtn.click();
90 await page.waitForFunction(
91 () => document.body.innerText.includes("rel max error"),
92 { timeout: 120000 }
93 );
94 const text = await page.evaluate(() => document.body.innerText);
95 const m = text.match(/rel max error ([0-9.]+e[+-][0-9]+)/);
96 if (!m) {
97 console.error("FAIL: no reported error after compute");
98 failures++;
99 } else {
100 const err = parseFloat(m[1]);
101 // mfs at its default n on star-medium should be far below 1e-6.
102 if (!(err < 1e-6)) {
103 console.error(`FAIL: in-browser mfs error ${err} not < 1e-6`);
104 failures++;
105 } else {
106 console.log(`in-browser solve ok (rel max error ${m[1]})`);
107 }
108 }
109 }
111 if (errors.length > 0) {
112 console.error("FAIL: console errors:");
113 for (const e of errors) console.error(` ${e}`);
114 failures++;
115 }
116} finally {
117 await browser.close();
118 server.close();
119}
121if (failures > 0) {
122 console.error(`${failures} failure(s)`);
123 process.exit(1);
124}
125console.log("check-app: all checks passed");