/** * Capture screenshots for every route × viewport against the dummy backend. * * Usage: * node test/capture.mjs [--out test/screenshots/current] * * Flow: spawns the dummy backend (default :4218) + `vite --port 4229` with * RIG_API_TARGET pointing at the dummy, then drives a persistent headless * Chrome (remote-debugging port) via CDP: one fresh page target per shot, * waits for React hydration + data, captures full-page PNGs. No npm deps. * * The dummy backend is always-authed, so no login step is needed. */ import { spawn } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ROUTES, VIEWPORTS } from './routes.mjs'; import { capturePage } from './cdp.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FRONT_ROOT = path.resolve(__dirname, '..'); const args = Object.fromEntries( process.argv.slice(2).map((a) => { const [k, v] = a.split('='); return [k.replace(/^--/, ''), v ?? true]; }), ); const OUT_DIR = path.resolve(FRONT_ROOT, args.out ?? 'test/screenshots/current'); const DUMMY_PORT = Number(args['dummy-port'] ?? 4218); const VITE_PORT = Number(args['vite-port'] ?? 4229); const DEBUG_PORT = Number(args['debug-port'] ?? 9333); const BASE = `http://localhost:${VITE_PORT}`; const SETTLE_MS = Number(args.settle ?? 2500); const CHROME = args.chrome ?? '/usr/bin/google-chrome'; function wait(ms) { return new Promise((r) => setTimeout(r, ms)); } async function waitForPort(port, host = '127.0.0.1', timeoutMs = 45000) { const net = await import('node:net'); const start = Date.now(); for (;;) { try { await new Promise((resolve, reject) => { const s = net.default.connect(port, host); s.on('connect', () => { s.end(); resolve(); }); s.on('error', reject); }); return; } catch { if (Date.now() - start > timeoutMs) throw new Error(`port ${port} not ready`); await wait(300); } } } function startProc(cmd, procArgs, env, tag) { const child = spawn(cmd, procArgs, { cwd: FRONT_ROOT, env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'pipe'], }); child.stdout.on('data', (d) => process.stdout.write(`[${tag}] ${d}`)); child.stderr.on('data', (d) => { const text = String(d); // vite proxy logs are noise during capture; keep errors. if (tag === 'vite' && text.includes('[vite-proxy]')) return; process.stderr.write(`[${tag}] ${d}`); }); return child; } async function main() { if (!fs.existsSync(CHROME)) { throw new Error(`Chrome not found at ${CHROME} (pass --chrome=...)`); } if (!fs.existsSync(path.join(FRONT_ROOT, 'test', 'fixtures', 'snapshot.json'))) { throw new Error('Missing test/fixtures/snapshot.json — run: npm run rig:collect'); } fs.mkdirSync(OUT_DIR, { recursive: true }); const dummy = startProc('node', ['test/dummyBackend.mjs', `--port=${DUMMY_PORT}`], {}, 'dummy'); await waitForPort(DUMMY_PORT); const vite = startProc( 'npx', ['vite', '--port', String(VITE_PORT), '--strictPort'], { RIG_API_TARGET: `http://localhost:${DUMMY_PORT}` }, 'vite', ); await waitForPort(VITE_PORT); await wait(3000); const profile = '/tmp/rig-chrome-profile'; fs.mkdirSync(profile, { recursive: true }); const chrome = startProc( CHROME, [ '--headless=new', '--no-sandbox', '--disable-gpu', '--hide-scrollbars', `--remote-debugging-port=${DEBUG_PORT}`, `--user-data-dir=${profile}`, 'about:blank', ], {}, 'chrome', ); await waitForPort(DEBUG_PORT); let failed = 0; try { for (const viewport of VIEWPORTS) { for (const route of ROUTES) { const file = path.join(OUT_DIR, `${route.name}.${viewport.name}.png`); try { await capturePage(DEBUG_PORT, `${BASE}${route.path}`, file, { width: viewport.width, height: viewport.height, settleMs: SETTLE_MS, }); const kb = (fs.statSync(file).size / 1024).toFixed(0); console.log(`[capture] ${route.path} @ ${viewport.name} → ${path.basename(file)} (${kb} KiB)`); } catch (err) { console.error(`[capture] FAIL ${route.path} @ ${viewport.name}: ${err.message}`); failed++; } } } } finally { chrome.kill(); vite.kill(); dummy.kill(); } if (failed > 0) { console.error(`[capture] ${failed} shots failed`); process.exit(1); } console.log(`[capture] done → ${OUT_DIR}`); } main().catch((err) => { console.error(`[capture] FAILED: ${err.message}`); process.exit(1); });