153 lines
4.6 KiB
JavaScript
153 lines
4.6 KiB
JavaScript
/**
|
||
* 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);
|
||
});
|