1/** Screenshot the app (dist/) in headless Chrome after the boot-time solve
2 * finishes. Talks to the real cache. The sweep page is screenshotted as it
3 * loads, without computing: it shows whatever of the sweep is cached.
4 * Usage: node scripts/screenshot.mjs out.png [light|dark] [tEnd] [index|sweep] */
5import { createServer } from 'node:http';
6import { readFile } from 'node:fs/promises';
7import { extname, join } from 'node:path';
8import puppeteer from 'puppeteer-core';
10const out = process.argv[2] ?? 'demo.png';
11const scheme = process.argv[3] ?? 'light';
12const tEnd = process.argv[4] ?? '100';
13const which = process.argv[5] ?? 'index';
14if (which !== 'index' && which !== 'sweep') {
15 throw new Error(`the page is 'index' or 'sweep', not '${which}'`);
16}
17const DIST = new URL('../dist/', import.meta.url).pathname;
18const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css' };
20const server = createServer(async (req, res) => {
21 try {
22 const path = req.url === '/' ? '/index.html' : req.url.split('?')[0];
23 const data = await readFile(join(DIST, path));
24 res.writeHead(200, { 'content-type': MIME[extname(path)] ?? 'application/octet-stream' });
25 res.end(data);
26 } catch {
27 res.writeHead(404);
28 res.end();
29 }
30});
31await new Promise((r) => server.listen(0, '127.0.0.1', r));
32const port = server.address().port;
34const browser = await puppeteer.launch({
35 executablePath: process.env.CHROME_PATH ?? '/usr/bin/google-chrome',
36 args: ['--headless=new', '--no-sandbox', '--enable-unsafe-webgpu',
37 '--use-webgpu-adapter=swiftshader', '--enable-unsafe-swiftshader'],
38 // A wait is one CDP call lasting as long as the wait, so the default 180 s
39 // cap on a call is what a slow run trips over first.
40 protocolTimeout: 900_000,
41});
42const page = await browser.newPage();
43await page.setViewport({ width: 1100, height: 900 });
44await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: scheme }]);
45page.on('console', (m) => console.log(' [page]', m.text()));
46// The ?tend hook accepts any end time, listed or not, so a short test run
47// can be screenshotted too.
48await page.goto(`http://127.0.0.1:${port}/${which}.html?tend=${tEnd}`, { waitUntil: 'load' });
49await page.waitForSelector('#tend');
50// Terminal statuses only — "checking the cloud cache…" is transient. On a
51// miss the single-solution page settles on empty windows; press the button so
52// the screenshot shows a pattern either way. The sweep page computes nothing:
53// three or four runs is more than a screenshot is worth, and the cached part
54// of the sweep is what it is meant to show.
55const idle =
56 which === 'sweep'
57 ? /values (in the cloud cache|loaded)|failed/
58 : /from the cloud cache|press Compute solution|failed/;
59await page.waitForFunction(
60 (re) => new RegExp(re).test(document.getElementById('status')?.textContent ?? '') ||
61 (document.getElementById('err')?.textContent?.length ?? 0) > 4,
62 { timeout: 600_000 },
63 idle.source,
64);
65if (/press Compute solution/.test(await page.$eval('#status', (el) => el.textContent))) {
66 await page.click('#solve');
67 await page.waitForFunction(
68 () => /Not uploaded|Uploaded \d|from the cloud cache|failed/.test(
69 document.getElementById('status')?.textContent ?? '') ||
70 (document.getElementById('err')?.textContent?.length ?? 0) > 4,
71 { timeout: 600_000 },
72 );
73}
74await new Promise((r) => setTimeout(r, 300));
75await page.screenshot({ path: out });
76console.log('screenshot:', out);
77console.log('status:', await page.$eval('#status', (el) => el.textContent));
78const err = await page.$eval('#err', (el) => el.textContent);
79if (err) console.log('err:', err);
80await browser.close();
81server.close();