u
This commit is contained in:
109
test/capture.mjs
109
test/capture.mjs
@@ -16,7 +16,7 @@
|
||||
*
|
||||
* The dummy backend is always-authed, so no login step is needed.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { execSync, spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -40,11 +40,24 @@ 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';
|
||||
const FILTER = args.filter ? String(args.filter) : null;
|
||||
const ACTIVE_ROUTES = FILTER
|
||||
? ROUTES.filter((r) => r.name.includes(FILTER) || r.path.includes(FILTER))
|
||||
: ROUTES;
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
function freePorts(ports) {
|
||||
try {
|
||||
const portList = ports.map((p) => `${p}/tcp`).join(' ');
|
||||
execSync(`fuser -k ${portList} 2>/dev/null || true`, { stdio: 'ignore' });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForPort(port, host = '127.0.0.1', timeoutMs = 45000) {
|
||||
const net = await import('node:net');
|
||||
const start = Date.now();
|
||||
@@ -66,33 +79,39 @@ async function waitForPort(port, host = '127.0.0.1', timeoutMs = 45000) {
|
||||
}
|
||||
}
|
||||
|
||||
const activeProcs = new Set();
|
||||
let activeProfile = null;
|
||||
|
||||
function startProc(cmd, procArgs, env, tag) {
|
||||
const child = spawn(cmd, procArgs, {
|
||||
cwd: FRONT_ROOT,
|
||||
env: { ...process.env, ...env },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
// Own process group per child so stopProc can kill the whole tree
|
||||
// (e.g. `npx vite` wrapper + the real vite process).
|
||||
detached: process.platform !== 'win32',
|
||||
});
|
||||
// Piped stdio keeps the parent alive even after kill(); detach the streams
|
||||
// so teardown below can actually exit.
|
||||
child.stdout.on('data', (d) => process.stdout.write(`[${tag}] ${d}`));
|
||||
activeProcs.add(child);
|
||||
child.stdout.on('data', (d) => {
|
||||
const text = String(d);
|
||||
if ((tag === 'preview' || tag === 'vite') && text.includes('[vite-proxy]')) return;
|
||||
process.stdout.write(`[${tag}] ${d}`);
|
||||
});
|
||||
child.stderr.on('data', (d) => {
|
||||
const text = String(d);
|
||||
// preview proxy logs are noise during capture; keep errors.
|
||||
if ((tag === 'preview' || tag === 'vite') && text.includes('[vite-proxy]')) return;
|
||||
process.stderr.write(`[${tag}] ${d}`);
|
||||
});
|
||||
child.on('exit', () => activeProcs.delete(child));
|
||||
child.stdout.unref?.();
|
||||
child.stderr.unref?.();
|
||||
child.unref?.();
|
||||
return child;
|
||||
}
|
||||
|
||||
async function stopProc(child, tag, timeoutMs = 8000) {
|
||||
if (!child || child.exitCode != null || child.signalCode != null) return;
|
||||
// Kill the whole process group (child + grandchildren like npx/vite).
|
||||
async function stopProc(child, tag, timeoutMs = 4000) {
|
||||
if (!child || child.exitCode != null || child.signalCode != null) {
|
||||
activeProcs.delete(child);
|
||||
return;
|
||||
}
|
||||
const killTree = (sig) => {
|
||||
try {
|
||||
if (child.pid && process.platform !== 'win32') {
|
||||
@@ -100,7 +119,7 @@ async function stopProc(child, tag, timeoutMs = 8000) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// fall through to single kill
|
||||
// fall through
|
||||
}
|
||||
try {
|
||||
child.kill(sig);
|
||||
@@ -120,19 +139,39 @@ async function stopProc(child, tag, timeoutMs = 8000) {
|
||||
});
|
||||
killTree('SIGTERM');
|
||||
});
|
||||
// Drain any buffered pipe listeners so they can't hold the loop open.
|
||||
activeProcs.delete(child);
|
||||
try {
|
||||
child.stdout?.destroy();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
child.stderr?.destroy();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function cleanupEverything() {
|
||||
for (const child of Array.from(activeProcs)) {
|
||||
await stopProc(child, 'proc');
|
||||
}
|
||||
if (activeProfile) {
|
||||
try {
|
||||
fs.rmSync(activeProfile, { recursive: true, force: true });
|
||||
} catch {}
|
||||
activeProfile = null;
|
||||
}
|
||||
freePorts([DUMMY_PORT, VITE_PORT, DEBUG_PORT]);
|
||||
}
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n[capture] Interrupted, cleaning up...');
|
||||
await cleanupEverything();
|
||||
process.exit(130);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
await cleanupEverything();
|
||||
process.exit(143);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(CHROME)) {
|
||||
throw new Error(`Chrome not found at ${CHROME} (pass --chrome=...)`);
|
||||
@@ -142,6 +181,9 @@ async function main() {
|
||||
}
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
// Ensure ports are free before starting
|
||||
freePorts([DUMMY_PORT, VITE_PORT, DEBUG_PORT]);
|
||||
|
||||
// Production bundle first — screenshots must reflect the shipped app.
|
||||
console.log('[capture] vite build…');
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -169,12 +211,12 @@ async function main() {
|
||||
'preview',
|
||||
);
|
||||
await waitForPort(VITE_PORT);
|
||||
await wait(2000);
|
||||
await wait(1500);
|
||||
|
||||
// Fresh profile per run: the production SW registers and caches dist/ —
|
||||
// never let it leak between reference/compare runs.
|
||||
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'rig-chrome-'));
|
||||
fs.mkdirSync(profile, { recursive: true });
|
||||
activeProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'rig-chrome-'));
|
||||
fs.mkdirSync(activeProfile, { recursive: true });
|
||||
const chrome = startProc(
|
||||
CHROME,
|
||||
[
|
||||
@@ -183,7 +225,7 @@ async function main() {
|
||||
'--disable-gpu',
|
||||
'--hide-scrollbars',
|
||||
`--remote-debugging-port=${DEBUG_PORT}`,
|
||||
`--user-data-dir=${profile}`,
|
||||
`--user-data-dir=${activeProfile}`,
|
||||
'about:blank',
|
||||
],
|
||||
{},
|
||||
@@ -194,44 +236,37 @@ async function main() {
|
||||
let failed = 0;
|
||||
try {
|
||||
for (const viewport of VIEWPORTS) {
|
||||
for (const route of ROUTES) {
|
||||
for (const route of ACTIVE_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,
|
||||
dialog: route.dialog,
|
||||
});
|
||||
const kb = (fs.statSync(file).size / 1024).toFixed(0);
|
||||
console.log(`[capture] ${route.path} @ ${viewport.name} → ${path.basename(file)} (${kb} KiB)`);
|
||||
console.log(`[capture] ${route.name} (${route.path}) @ ${viewport.name} → ${path.basename(file)} (${kb} KiB)`);
|
||||
} catch (err) {
|
||||
console.error(`[capture] FAIL ${route.path} @ ${viewport.name}: ${err.message}`);
|
||||
console.error(`[capture] FAIL ${route.name} (${route.path}) @ ${viewport.name}: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await stopProc(chrome, 'chrome');
|
||||
await stopProc(server, 'preview');
|
||||
await stopProc(dummy, 'dummy');
|
||||
// Remove the throwaway Chrome profile (SW + caches).
|
||||
try {
|
||||
fs.rmSync(profile, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
await cleanupEverything();
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
console.error(`[capture] ${failed} shots failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`[capture] done → ${OUT_DIR}`);
|
||||
// Explicit exit: spawned grandchildren (preview server, chrome helpers) can
|
||||
// otherwise keep the event loop alive after teardown.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
main().catch(async (err) => {
|
||||
console.error(`[capture] FAILED: ${err.message}`);
|
||||
await cleanupEverything();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user