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。
67 lines
3.0 KiB
JavaScript
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);
|