Files
SpineWallpaper/tools/shot.mjs
T
Shuery 399f99edd7 test: 零依赖对拍测试台(可在不改 dist/ 的前提下复现 WE 行为)
tools/serve.mjs    本地静态服务 + 测试驱动注入(模拟 WE 下发用户属性、冻结动画相位、藏 canvas 取参考帧)
tools/cdp.mjs      最小 CDP 客户端(真实时间等待;--screenshot 的虚拟时间会在骨骼加载完成前就耗尽)
tools/shot.mjs     截图/取状态,就绪判据可传入
tools/diff.mjs     ffmpeg 差分:PSNR / SSIM / 平均亮度差
tools/capture.mjs  对拍矩阵:壁纸 × 分辨率 × (正常, 藏 canvas)

实测:xilian 1920x1080 spine 层贡献 mean|diff|=26.1/255,kv37 为 10.2/255,
同配置两次截图噪声底 0.68/255。
2026-09-19 19:13:57 +08:00

67 lines
3.0 KiB
JavaScript

// 用 CDP 截图 / 取状态(真实时间等待,见 cdp.mjs 顶部注释)。
// 用法:node tools/shot.mjs <out.png|-> <url> <W> <H> [waitMs] [options]
// --wait "<js 表达式>" 就绪判据,默认为"骨骼已加载或已报错"
// --eval "<js>" 截图前执行的表达式(结果会打印出来)
// --settle <ms> 就绪后额外等待,确保至少画过一帧(默认 500)
// --dom 不截图,打印 document.title 里的测试状态
// --extra "<edge 旗标>" 追加 Edge 旗标
import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { withPage, sleep } from "./cdp.mjs";
const argv = process.argv.slice(2);
const [out, url, w, h, waitMs = "30000"] = argv;
if (!out || !url || !w || !h) {
console.error('usage: node tools/shot.mjs <out.png|-> <url> <W> <H> [waitMs] [--wait js] [--eval js] [--settle ms] [--dom]');
process.exit(2);
}
const opt = (name, dflt) => {
const i = argv.indexOf(`--${name}`);
return i >= 0 ? argv[i + 1] : dflt;
};
const DEFAULT_WAIT = '(window.__test ? !!(window.__test.successAt || window.__test.error) : document.readyState === "complete")';
const waitFor = opt("wait", DEFAULT_WAIT);
const settle = Number(opt("settle", "500"));
const domMode = argv.includes("--dom");
const extraArgs = [];
if (opt("extra")) extraArgs.push(opt("extra"));
const result = await withPage({ width: Number(w), height: Number(h), extraArgs }, async ({ send, evaluate, consoleLines, exceptions }) => {
await send("Page.navigate", { url });
const deadline = Date.now() + Number(waitMs);
let ready = false;
while (Date.now() < deadline) {
if (await evaluate(waitFor)) { ready = true; break; }
await sleep(150);
}
const state = await evaluate('(function(){ try { return JSON.parse((document.title||"").replace(/^TESTSTATE /,"")) } catch(e){ return null } })()');
await sleep(settle);
if (domMode) {
const evalOut = opt("eval") ? await evaluate(opt("eval")) : undefined;
return { ready, state, evalOut, consoleLines, exceptions };
}
if (opt("eval")) await evaluate(opt("eval"));
const shot = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
await writeFile(resolve(out), Buffer.from(shot.data, "base64"));
return { ready, state, consoleLines, exceptions };
});
const tag = result.ready ? "ready" : "TIMEOUT";
if (domMode) {
console.log(`state=${tag}`);
if (result.evalOut !== undefined) console.log(`eval=${JSON.stringify(result.evalOut)}`);
console.log(JSON.stringify(result.state, null, 2));
} else {
console.log(`${out} ${tag}`);
if (result.state) console.log(` state: ${JSON.stringify(result.state)}`);
}
const noisy = result.consoleLines.filter((l) => /error|warn|uncaught|fail/i.test(l));
if (noisy.length) console.log(" console: " + noisy.slice(0, 6).join(" | "));
if (result.exceptions.length) console.log(" exceptions: " + result.exceptions.slice(0, 4).join(" | "));
if (!result.ready) process.exit(1);