/ tutorials / wave-acoustics
Sign in
tutorials / wave-acoustics
wave-acoustics / scripts / site-check.mjs
82 lines · 3.7 KBBlameHistoryRaw
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
11// Chapter 1's first lab runs numbl on load, so the check is simply whether its
12// figures arrive.
13import puppeteer from 'puppeteer'
15const url = process.argv[2] ?? 'https://vault1.magland.org/tutorials/wave-acoustics/site/'
17let failures = 0
18const check = (name, ok, detail = '') => {
19 console.log(`${ok ? ' ok ' : ' FAIL '} ${name}${detail ? ` ${detail}` : ''}`)
20 if (!ok) failures++
23const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
24const page = await browser.newPage()
25await page.setViewport({ width: 1400, height: 1000 })
27// numbl announces which LAPACK bridge it picked on console.error; that is
28// information, not a fault.
29const BENIGN = /using bridge:/
30const problems = []
31page.on('console', (m) => {
32 if (m.type() === 'error' && !BENIGN.test(m.text())) problems.push(`console.error: ${m.text()}`)
33})
34page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`))
35page.on('requestfailed', (r) => problems.push(`requestfailed: ${r.url()}`))
37try {
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)
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) => /An exact string/.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(' | '))
66 // the sandbox is also where audio could fail: press the first listen
67 // button and check that the player started rather than reporting an error
68 await page.evaluate(() => document.querySelector('.listen-btn')?.click())
69 await new Promise((r) => setTimeout(r, 800))
70 const audio = await page.evaluate(() => ({
71 on: document.querySelector('.listen-btn.on') !== null,
72 err: document.querySelector('.listen .lab-error')?.textContent ?? '',
73 }))
74 check('the audio player starts in the sandboxed origin', audio.on && audio.err === '', audio.err)
75 check('the lab drew its figures', info.drawn > 0, `${info.drawn} paths ${info.stats.join(' ')}`)
76 check('no console errors', problems.length === 0, problems.slice(0, 3).join(' | '))
77} finally {
78 await browser.close()
81console.log(failures === 0 ? '\nthe deployed site works' : `\n${failures} check(s) failed`)
82process.exit(failures === 0 ? 0 : 1)
↑↓ move↵openescclose