This commit is contained in:
seb
2026-10-08 04:51:28 +02:00
parent 7829addbdd
commit be02e531c5
3 changed files with 61 additions and 10 deletions

View File

@@ -23,6 +23,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { ROUTES, VIEWPORTS } from './routes.mjs'; import { ROUTES, VIEWPORTS } from './routes.mjs';
import { capturePage } from './cdp.mjs'; import { capturePage } from './cdp.mjs';
import { compareBuffers } from './compare.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FRONT_ROOT = path.resolve(__dirname, '..'); const FRONT_ROOT = path.resolve(__dirname, '..');
@@ -233,20 +234,55 @@ async function main() {
); );
await waitForPort(DEBUG_PORT); await waitForPort(DEBUG_PORT);
const isReference = OUT_DIR.endsWith('reference') || Boolean(args['diff-only']);
const force = Boolean(args.force);
let failed = 0; let failed = 0;
try { try {
for (const viewport of VIEWPORTS) { for (const viewport of VIEWPORTS) {
for (const route of ACTIVE_ROUTES) { for (const route of ACTIVE_ROUTES) {
const file = path.join(OUT_DIR, `${route.name}.${viewport.name}.png`); const file = path.join(OUT_DIR, `${route.name}.${viewport.name}.png`);
try { try {
await capturePage(DEBUG_PORT, `${BASE}${route.path}`, file, { const buf = await capturePage(DEBUG_PORT, `${BASE}${route.path}`, null, {
width: viewport.width, width: viewport.width,
height: viewport.height, height: viewport.height,
settleMs: SETTLE_MS, settleMs: SETTLE_MS,
dialog: route.dialog, dialog: route.dialog,
}); });
const kb = (fs.statSync(file).size / 1024).toFixed(0); const kb = (buf.length / 1024).toFixed(0);
console.log(`[capture] ${route.name} (${route.path}) @ ${viewport.name} → ${path.basename(file)} (${kb} KiB)`);
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) { } catch (err) {
console.error(`[capture] FAIL ${route.name} (${route.path}) @ ${viewport.name}: ${err.message}`); console.error(`[capture] FAIL ${route.name} (${route.path}) @ ${viewport.name}: ${err.message}`);
failed++; failed++;

View File

@@ -270,7 +270,9 @@ export async function capturePage(debugPort, url, file, { width, height, settleM
{ format: 'png', captureBeyondViewport: true, fromSurface: true }, { format: 'png', captureBeyondViewport: true, fromSurface: true },
30000, 30000,
); );
fs.writeFileSync(file, Buffer.from(data, 'base64')); const buf = Buffer.from(data, 'base64');
if (file) fs.writeFileSync(file, buf);
return buf;
} finally { } finally {
session.close(); session.close();
await fetch(`http://127.0.0.1:${debugPort}/json/close/${target.id}`).catch(() => {}); await fetch(`http://127.0.0.1:${debugPort}/json/close/${target.id}`).catch(() => {});

View File

@@ -159,13 +159,17 @@ function toRgba(parsed) {
return out; return out;
} }
function compareFiles(refFile, curFile) { export function compareBuffers(refBuf, curBuf, { tolerance = TOLERANCE, channelTol = CHANNEL_TOL } = {}) {
const ref = parsePng(fs.readFileSync(refFile)); const ref = parsePng(refBuf);
const cur = parsePng(fs.readFileSync(curFile)); const cur = parsePng(curBuf);
if (ref.width !== cur.width || ref.height !== cur.height) { if (ref.width !== cur.width || ref.height !== cur.height) {
return { return {
ok: false, ok: false,
reason: `size mismatch ref=${ref.width}x${ref.height} cur=${cur.width}x${cur.height}`, 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); const a = toRgba(ref);
@@ -182,7 +186,7 @@ function compareFiles(refFile, curFile) {
Math.abs(a[o + 2] - b[o + 2]), Math.abs(a[o + 2] - b[o + 2]),
); );
if (d > maxDelta) maxDelta = d; if (d > maxDelta) maxDelta = d;
if (d > CHANNEL_TOL) { if (d > channelTol) {
diff++; diff++;
diffImg[o] = 255; diffImg[o] = 255;
diffImg[o + 1] = 0; diffImg[o + 1] = 0;
@@ -199,7 +203,7 @@ function compareFiles(refFile, curFile) {
} }
const frac = diff / total; const frac = diff / total;
return { return {
ok: frac <= TOLERANCE, ok: frac <= tolerance,
diffPixels: diff, diffPixels: diff,
totalPixels: total, totalPixels: total,
frac, 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() { function main() {
if (!fs.existsSync(REF_DIR)) { if (!fs.existsSync(REF_DIR)) {
console.error(`[compare] no reference dir ${REF_DIR} — run: npm run rig:reference`); 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})`); 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();
}