// 视觉等价验收:当前 dist 与**重构前**的存档截图逐像素比对。 // // 为什么需要它:`tools/checks/verify-sim-page.mts` 只证明"自包含包能跑",不证明"画面没变"。 // 内联、模块合成、URL 重写这些改动都可能悄悄改掉渲染结果。 // // 判据:mean|diff| = 0 且 max|diff| = 0(逐像素相同)。 // 注意 `tools/capture.mjs` 打印的 "spine 层贡献" 是**同一轮里 ref 帧 vs 主帧**的差 // (即"spine 层占了多少像素"),不是与基线的比对——我一开始把它读成了后者, // 白紧张了一场。真正的基线比对只有这里。 // // 用法: // node tools/serve.mjs 8190 --root dist/releases/collection-all // node tools/capture.mjs visual-current --base http://127.0.0.1:8190 // node tools/checks/verify-visual-equivalence.mts import { execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; const BASELINE = "tools/shots/baseline-pre-refactor"; const CURRENT = process.argv[2] ?? "tools/shots/visual-current"; const combos: { preset: string; size: string }[] = []; for (const preset of ["kv37", "xilian"]) { for (const size of ["3440x1440", "1920x1080", "1080x1920"]) combos.push({ preset, size }); } function diff(a, b) { const out = execFileSync("node", ["tools/diff.mjs", a, b], { encoding: "utf8" }); return out.trim().split("\n").pop() ?? ""; } /* * 缺文件时**直接失败**,不跳过。 * * 这里原来写的是"缺少文件就跳过",于是有一次当前截图目录被清掉之后, * 它把 0 组比对报成了"0 组全部逐像素相同"——一句绿色的空话。 * 一个不检查前置条件的验证等于没有验证:先把该在的文件都确认在,再谈结论。 */ const missing: string[] = []; for (const { preset, size } of combos) { for (const file of [`${BASELINE}/${preset}-${size}.png`, `${CURRENT}/${preset}-${size}.png`]) { if (!existsSync(file)) missing.push(file); } } if (missing.length > 0) { console.error(`✗ 缺少 ${missing.length} 个截图,无法比对:`); for (const file of missing.slice(0, 4)) console.error(` ${file}`); if (missing.length > 4) console.error(` …还有 ${missing.length - 4} 个`); console.error(` 基线在 ${BASELINE}(重构前存档,不能重新生成)。 当前截图需要现拍: node tools/serve.mjs 8190 --root dist/releases/collection-all node tools/capture.mjs visual-current --base http://127.0.0.1:8190 `); process.exit(1); } let failed = 0; let checked = 0; for (const { preset, size } of combos) { const a = `${BASELINE}/${preset}-${size}.png`; const b = `${CURRENT}/${preset}-${size}.png`; checked++; const line = diff(a, b); const identical = /mean\|diff\|=0\/255\s+max\|diff\|=0/.test(line); if (!identical) failed++; console.log(` ${identical ? "✓" : "✗"} ${preset.padEnd(7)} ${size.padEnd(11)} ${line}`); } // 走到这里 checked 必然等于 combos.length(上面缺文件就退了),这条是防回归的兜底。 if (checked !== combos.length) { console.error(`✗ 只比对了 ${checked}/${combos.length} 组,结论不可信`); process.exit(1); } console.log( failed === 0 ? `\n视觉等价:${checked} 组全部与重构前逐像素相同` : `\n视觉等价:${failed}/${checked} 组与基线不同`, ); process.exit(failed === 0 ? 0 : 1);