#!/usr/bin/env node // Loads the deployed site in a headless browser and checks that it actually // runs there, which serving it locally cannot tell you. // // node scripts/site-check.mjs [url] // // The vault serves sites sandboxed: `allow-scripts` without `allow-same-origin` // puts the page in an opaque origin, where `localStorage` throws and a worker // can only be started from a blob URL. numbl does start its worker that way, so // the tutorial is expected to work, but "expected to" is not a test, and // Chapter 1's first lab runs numbl on load, so the check is simply whether its // figures arrive. import puppeteer from 'puppeteer' const url = process.argv[2] ?? 'https://vault1.magland.org/tutorials/wave-acoustics/site/' let failures = 0 const check = (name, ok, detail = '') => { console.log(`${ok ? ' ok ' : ' FAIL '} ${name}${detail ? ` ${detail}` : ''}`) if (!ok) failures++ } const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] }) const page = await browser.newPage() await page.setViewport({ width: 1400, height: 1000 }) // numbl announces which LAPACK bridge it picked on console.error; that is // information, not a fault. const BENIGN = /using bridge:/ const problems = [] page.on('console', (m) => { if (m.type() === 'error' && !BENIGN.test(m.text())) problems.push(`console.error: ${m.text()}`) }) page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`)) page.on('requestfailed', (r) => problems.push(`requestfailed: ${r.url()}`)) try { const res = await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 }) check('the site answers', res !== null && res.ok(), `HTTP ${res?.status()}`) check('the shell rendered', (await page.$('.sidebar')) !== null) // Chapter 1's first lab runs on sight: if numbl booted inside the sandbox, // its figures appear; if the worker could not start, they never do. await page.evaluate(() => { const b = [...document.querySelectorAll('.nav-item')].find((x) => /An exact string/.test(x.textContent)) b?.click() }) await page.waitForFunction( () => { const lab = document.querySelector('.lab') if (!lab) return false const busy = [...lab.querySelectorAll('button')].some((b) => /running/.test(b.textContent)) return !busy && !lab.querySelector('.lab-placeholder') }, { timeout: 120000, polling: 500 }, ) const info = await page.evaluate(() => ({ drawn: [...document.querySelectorAll('.lab-figures svg path')].filter( (p) => (p.getAttribute('d') ?? '').length > 12, ).length, errors: [...document.querySelectorAll('.lab-error')].map((e) => e.textContent.slice(0, 200)), stats: [...document.querySelectorAll('.stat-value')].map((s) => s.textContent), })) check('numbl ran in the sandboxed origin', info.errors.length === 0, info.errors.join(' | ')) // the sandbox is also where audio could fail: press the first listen // button and check that the player started rather than reporting an error await page.evaluate(() => document.querySelector('.listen-btn')?.click()) await new Promise((r) => setTimeout(r, 800)) const audio = await page.evaluate(() => ({ on: document.querySelector('.listen-btn.on') !== null, err: document.querySelector('.listen .lab-error')?.textContent ?? '', })) check('the audio player starts in the sandboxed origin', audio.on && audio.err === '', audio.err) check('the lab drew its figures', info.drawn > 0, `${info.drawn} paths ${info.stats.join(' ')}`) check('no console errors', problems.length === 0, problems.slice(0, 3).join(' | ')) } finally { await browser.close() } console.log(failures === 0 ? '\nthe deployed site works' : `\n${failures} check(s) failed`) process.exit(failures === 0 ? 0 : 1)