Check the deployed site, not just a local build of it
3 changed files+77−2
README.mdmodified+3−1View file
@@ -98,6 +98,7 @@ npm run dev # vite dev server
9898 npm run build # typecheck and bundle to dist/
9999 npm run test:matlab # the .m layer, through the numbl CLI (needs a numbl checkout)
100100 npm run test:browser # the built app in headless Chrome
101+npm run test:site # the deployed site, in headless Chrome
101102 ```
102103
103104 `test:matlab` needs a numbl clone; set `NUMBL_DIR` if it is not at `~/src/numbl`,
@@ -118,7 +119,8 @@ publishes it as the repository's site on the vault, at
118119 build`; `vite.config.ts` uses a relative `base`, so the sub-path needs no
119120 configuration. Note that the vault serves sites sandboxed, which makes
120121 `localStorage` throw rather than degrade: nothing here touches it, and nothing
121-here should start.
122+here should start. `npm run test:site` loads the deployed page and checks that
123+numbl really does run there, which serving the build locally cannot tell you.
122124
123125 ## Licence
124126
package.jsonmodified+2−1View file
@@ -11,7 +11,8 @@
1111 "preview": "vite preview",
1212 "typecheck": "tsc -b",
1313 "test:matlab": "node scripts/matlab-test.mjs",
14- "test:browser": "node scripts/browser-test.mjs"
14+ "test:browser": "node scripts/browser-test.mjs",
15+ "test:site": "node scripts/site-check.mjs"
1516 },
1617 "dependencies": {
1718 "@codemirror/language": "^6.11.3",
scripts/site-check.mjsadded+72−0View file
@@ -0,0 +1,72 @@
1+#!/usr/bin/env node
2+// Loads the deployed site in a headless browser and checks that it actually
3+// runs there, which serving it locally cannot tell you.
4+//
5+// node scripts/site-check.mjs [url]
6+//
7+// The vault serves sites sandboxed: `allow-scripts` without `allow-same-origin`
8+// puts the page in an opaque origin, where `localStorage` throws and a worker
9+// can only be started from a blob URL. numbl does start its worker that way, so
10+// the tutorial is expected to work — but "expected to" is not a test, and the
11+// first chapter's lab runs numbl on load, so the check is simply whether its
12+// figures arrive.
13+import puppeteer from 'puppeteer'
14+
15+const url = process.argv[2] ?? 'https://vault1.magland.org/tutorials/laplace-dirichlet/site/'
16+
17+let failures = 0
18+const check = (name, ok, detail = '') => {
19+ console.log(`${ok ? ' ok ' : ' FAIL '} ${name}${detail ? ` ${detail}` : ''}`)
20+ if (!ok) failures++
21+}
22+
23+const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
24+const page = await browser.newPage()
25+await page.setViewport({ width: 1400, height: 1000 })
26+
27+// numbl announces which LAPACK bridge it picked on console.error; that is
28+// information, not a fault.
29+const BENIGN = /using bridge:/
30+const problems = []
31+page.on('console', (m) => {
32+ if (m.type() === 'error' && !BENIGN.test(m.text())) problems.push(`console.error: ${m.text()}`)
33+})
34+page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`))
35+page.on('requestfailed', (r) => problems.push(`requestfailed: ${r.url()}`))
36+
37+try {
38+ const res = await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 })
39+ check('the site answers', res !== null && res.ok(), `HTTP ${res?.status()}`)
40+ check('the shell rendered', (await page.$('.sidebar')) !== null)
41+
42+ // Chapter 1's first lab runs on sight: if numbl booted inside the sandbox,
43+ // its figures appear; if the worker could not start, they never do.
44+ await page.evaluate(() => {
45+ const b = [...document.querySelectorAll('.nav-item')].find((x) => /Ground truth/.test(x.textContent))
46+ b?.click()
47+ })
48+ await page.waitForFunction(
49+ () => {
50+ const lab = document.querySelector('.lab')
51+ if (!lab) return false
52+ const busy = [...lab.querySelectorAll('button')].some((b) => /running/.test(b.textContent))
53+ return !busy && !lab.querySelector('.lab-placeholder')
54+ },
55+ { timeout: 120000, polling: 500 },
56+ )
57+ const info = await page.evaluate(() => ({
58+ drawn: [...document.querySelectorAll('.lab-figures svg path')].filter(
59+ (p) => (p.getAttribute('d') ?? '').length > 12,
60+ ).length,
61+ errors: [...document.querySelectorAll('.lab-error')].map((e) => e.textContent.slice(0, 200)),
62+ stats: [...document.querySelectorAll('.stat-value')].map((s) => s.textContent),
63+ }))
64+ check('numbl ran in the sandboxed origin', info.errors.length === 0, info.errors.join(' | '))
65+ check('the lab drew its figures', info.drawn > 0, `${info.drawn} paths ${info.stats.join(' ')}`)
66+ check('no console errors', problems.length === 0, problems.slice(0, 3).join(' | '))
67+} finally {
68+ await browser.close()
69+}
70+
71+console.log(failures === 0 ? '\nthe deployed site works' : `\n${failures} check(s) failed`)
72+process.exit(failures === 0 ? 0 : 1)