diff --git a/test/capture.mjs b/test/capture.mjs index 862ba66..0fd42f5 100644 --- a/test/capture.mjs +++ b/test/capture.mjs @@ -23,6 +23,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ROUTES, VIEWPORTS } from './routes.mjs'; import { capturePage } from './cdp.mjs'; +import { compareBuffers } from './compare.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FRONT_ROOT = path.resolve(__dirname, '..'); @@ -233,20 +234,55 @@ async function main() { ); await waitForPort(DEBUG_PORT); + const isReference = OUT_DIR.endsWith('reference') || Boolean(args['diff-only']); + const force = Boolean(args.force); + let failed = 0; try { for (const viewport of VIEWPORTS) { 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, { + const buf = await capturePage(DEBUG_PORT, `${BASE}${route.path}`, null, { 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.name} (${route.path}) @ ${viewport.name} → ${path.basename(file)} (${kb} KiB)`); + const kb = (buf.length / 1024).toFixed(0); + + if (fs.existsSync(file)) { + if (isReference && !force) { + const existingBuf = fs.readFileSync(file); + let diff; + try { + diff = compareBuffers(existingBuf, buf); + } catch (err) { + diff = { ok: false, reason: err.message }; + } + + if (diff.ok) { + console.log( + `[capture] ${route.name} (${route.path}) @ ${viewport.name} → unchanged (no visible difference)`, + ); + continue; + } + const pct = diff.frac != null ? `${(diff.frac * 100).toFixed(2)}%` : diff.reason; + console.log( + `[capture] ${route.name} (${route.path}) @ ${viewport.name} → UPDATED reference (visible diff ${pct}, maxΔ=${diff.maxDelta})`, + ); + } else { + console.log( + `[capture] ${route.name} (${route.path}) @ ${viewport.name} → ${path.basename(file)} (${kb} KiB)`, + ); + } + } else { + console.log( + `[capture] ${route.name} (${route.path}) @ ${viewport.name} → ${path.basename(file)} (${kb} KiB) [NEW]`, + ); + } + + fs.writeFileSync(file, buf); } catch (err) { console.error(`[capture] FAIL ${route.name} (${route.path}) @ ${viewport.name}: ${err.message}`); failed++; diff --git a/test/cdp.mjs b/test/cdp.mjs index 40aad18..4df44ec 100644 --- a/test/cdp.mjs +++ b/test/cdp.mjs @@ -270,7 +270,9 @@ export async function capturePage(debugPort, url, file, { width, height, settleM { format: 'png', captureBeyondViewport: true, fromSurface: true }, 30000, ); - fs.writeFileSync(file, Buffer.from(data, 'base64')); + const buf = Buffer.from(data, 'base64'); + if (file) fs.writeFileSync(file, buf); + return buf; } finally { session.close(); await fetch(`http://127.0.0.1:${debugPort}/json/close/${target.id}`).catch(() => {}); diff --git a/test/compare.mjs b/test/compare.mjs index 2f2ec32..6891fad 100644 --- a/test/compare.mjs +++ b/test/compare.mjs @@ -159,13 +159,17 @@ function toRgba(parsed) { return out; } -function compareFiles(refFile, curFile) { - const ref = parsePng(fs.readFileSync(refFile)); - const cur = parsePng(fs.readFileSync(curFile)); +export function compareBuffers(refBuf, curBuf, { tolerance = TOLERANCE, channelTol = CHANNEL_TOL } = {}) { + const ref = parsePng(refBuf); + const cur = parsePng(curBuf); if (ref.width !== cur.width || ref.height !== cur.height) { return { ok: false, reason: `size mismatch ref=${ref.width}x${ref.height} cur=${cur.width}x${cur.height}`, + frac: 1, + maxDelta: 255, + diffPixels: ref.width * ref.height, + totalPixels: ref.width * ref.height, }; } const a = toRgba(ref); @@ -182,7 +186,7 @@ function compareFiles(refFile, curFile) { Math.abs(a[o + 2] - b[o + 2]), ); if (d > maxDelta) maxDelta = d; - if (d > CHANNEL_TOL) { + if (d > channelTol) { diff++; diffImg[o] = 255; diffImg[o + 1] = 0; @@ -199,7 +203,7 @@ function compareFiles(refFile, curFile) { } const frac = diff / total; return { - ok: frac <= TOLERANCE, + ok: frac <= tolerance, diffPixels: diff, totalPixels: total, frac, @@ -208,6 +212,12 @@ function compareFiles(refFile, curFile) { }; } +export function compareFiles(refFile, curFile, opts = {}) { + return compareBuffers(fs.readFileSync(refFile), fs.readFileSync(curFile), opts); +} + +export { parsePng, toRgba, encodePng }; + function main() { if (!fs.existsSync(REF_DIR)) { console.error(`[compare] no reference dir ${REF_DIR} — run: npm run rig:reference`); @@ -260,4 +270,7 @@ function main() { console.log(`[compare] all ${files.length} screenshots match (tolerance ${(TOLERANCE * 100).toFixed(2)}%, channel±${CHANNEL_TOL})`); } -main(); +const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); +if (isMain) { + main(); +}