diff --git a/test/README.md b/test/README.md index cdac00b..b7d7d8d 100644 --- a/test/README.md +++ b/test/README.md @@ -28,7 +28,7 @@ npm run rig:compare | `test/collect.mjs` | Fixture collector (mints JWT via dashback env + signing lib) | | `test/dummyBackend.mjs` | Always-authed stub serving `test/fixtures/snapshot.json` + `images/` | | `test/cdp.mjs` | Zero-dep CDP client (fresh page target per shot, settle wait, PNG capture) | -| `test/capture.mjs` | Orchestrates dummy + vite + Chrome | +| `test/capture.mjs` | Builds prod bundle, serves via preview + dummy, drives Chrome | | `test/compare.mjs` | Zero-dep PNG decoder + comparator (0.1% tolerance, red diff PNGs) | | `test/fixtures/` | Committed snapshot + binary thumbs (2.5 MiB) | | `test/screenshots/reference/` | Committed reference PNGs (7.5 MiB) | diff --git a/test/capture.mjs b/test/capture.mjs index bfce082..efece5f 100644 --- a/test/capture.mjs +++ b/test/capture.mjs @@ -4,15 +4,21 @@ * 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, + * Flow: builds the production bundle (`vite build`), serves `dist/` via + * `vite preview` (default :4229) with RIG_API_TARGET pointing at the dummy + * backend (default :4218), 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. * + * Production (not dev) so screenshots reflect the shipped app: no HMR client, + * no AutomatInspector, minified bundles. Each run uses a fresh Chrome profile + * so the PWA service worker / caches never leak between runs. + * * The dummy backend is always-authed, so no login step is needed. */ import { spawn } from 'node:child_process'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ROUTES, VIEWPORTS } from './routes.mjs'; @@ -74,8 +80,8 @@ function startProc(cmd, procArgs, env, tag) { 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; + // 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.stdout.unref?.(); @@ -136,19 +142,38 @@ async function main() { } fs.mkdirSync(OUT_DIR, { recursive: true }); + // Production bundle first — screenshots must reflect the shipped app. + console.log('[capture] vite build…'); + await new Promise((resolve, reject) => { + const build = spawn('npx', ['vite', 'build'], { + cwd: FRONT_ROOT, + env: { ...process.env }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + build.stdout.on('data', () => process.stdout.write('[build] ')); + build.stderr.on('data', (d) => process.stderr.write(`[build] ${d}`)); + build.on('exit', (code) => { + if (code === 0) resolve(); + else reject(new Error(`vite build exited with ${code}`)); + }); + build.on('error', reject); + }); + const dummy = startProc('node', ['test/dummyBackend.mjs', `--port=${DUMMY_PORT}`], {}, 'dummy'); await waitForPort(DUMMY_PORT); - const vite = startProc( + const server = startProc( 'npx', - ['vite', '--port', String(VITE_PORT), '--strictPort'], + ['vite', 'preview', '--port', String(VITE_PORT), '--strictPort'], { RIG_API_TARGET: `http://localhost:${DUMMY_PORT}` }, - 'vite', + 'preview', ); await waitForPort(VITE_PORT); - await wait(3000); + await wait(2000); - const profile = '/tmp/rig-chrome-profile'; + // 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 }); const chrome = startProc( CHROME, @@ -187,15 +212,21 @@ async function main() { } } finally { await stopProc(chrome, 'chrome'); - await stopProc(vite, 'vite'); + 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 + } } if (failed > 0) { console.error(`[capture] ${failed} shots failed`); process.exit(1); } console.log(`[capture] done → ${OUT_DIR}`); - // Explicit exit: spawned grandchildren (vite/esbuild, chrome helpers) can + // Explicit exit: spawned grandchildren (preview server, chrome helpers) can // otherwise keep the event loop alive after teardown. process.exit(0); } diff --git a/test/fixtures/snapshot.json b/test/fixtures/snapshot.json index a5d2065..f1dd625 100644 --- a/test/fixtures/snapshot.json +++ b/test/fixtures/snapshot.json @@ -1,6 +1,6 @@ { "meta": { - "collectedAt": "2026-10-07T19:27:26.707Z", + "collectedAt": "2026-10-07T20:26:08.334Z", "backend": "http://localhost:4208", "companyId": 1, "googleClientId": "928121624463-m9du45ek2d5f6p9cke8qpnf89ii9no20.apps.googleusercontent.com"