Files
SpineWallpaper/tools/checks/smoke-sim.mts
T
Shuery 3f11426964 feat: TS 构建管线、Spine 抓取器与 wallpapers/ 唯一真相来源
把项目从「手写 dist/」改成「wallpapers/ 是唯一真相来源,dist/ 由 pnpm build 生成」,
并补上配套的类型、门禁与抓取器。一次提交落地整条管线,因为拆开会留下不能构建的中间态。

- src/:运行时与模拟器源码(TS,strict),编译到 build/ 再拷进各分发
- tools/:build / dev / check-{syntax,paths,dist},以及抓取器与回归门禁 tools/checks/
  (.scratch/ 下那批一次性脚本移入 tools/checks/ 并入库为长期门禁)
- wallpapers/:七档壁纸的源数据 + README.md(id/音频/预设的完整规范)
- docs/adr/0005-0008:构建管线与分发拓扑、模拟器契约、自包含 sim、每骨架资源布局
- .gitignore:排除 .scratch/ 的参考资料副本(上游 spine 整仓克隆 ~1.2 GB、
  抓取侦查数据 ~680 MB)与调试转储;这些是本地调查材料,补偿会让仓库无法克隆
- 归一化 .gitignore/CONTEXT.md 行尾(工作区 CRLF、索引 LF 造成的整文件假 diff)

同时修掉三档卡住构建的未完工壁纸:
- kv45 的 meta.json 里 id 还是抓取期场景名 scene_main,经 downloader promote 正名为 kv45
- shajin / zhigengniao_juheye 的 meta.json 误用了骨架描述文件(name/spine/animations/pages)、
  且都缺 preset.template.json;现按规范重建:骨架沉到 spines/<名>/(spine-ts 按 atlas 所在
  目录解析贴图页)、补上元数据与单骨架预设,并清掉 zhigengniao 骨架里指向作者机的绝对路径
- 顺带 promote 已在 sources.yml 里的 kv46(月升之前,与兽共舞)

pnpm check 五道门全绿:10 个分发 / 7 档壁纸 / 141 处引用自包含。
2026-10-02 01:27:02 +08:00

149 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 浏览器端冒烟:在真实无头 Edge 里验证
// ① 分发能正常渲染(spine canvas 出现、无异常)
// ② WE 模拟器确实生效(window.__weSim、面板、替身横幅)
// ③ 属性下发链路真的打通(经模拟器改 preset → 背景与骨架跟着换)
// ④ ?propsAt=dom 这条"load 之前下发"的竞态路径不会把配置丢掉
// ⑤ 单档分发(属性表里没有 preset/bgm)不白屏
//
// 判定只用页面**真实可观察**的量:canvas 是否出现、body 背景、播放器 config。
// 不要用 window.__player —— 那是 tools/serve.mjs 的对拍驱动注入的探针,不是产品行为。
//
// 用法:node tools/checks/smoke-sim.mts [base]
// 前提:调试服在跑(node tools/dev.ts --no-build)。
// 不需要 `--with-sim`:模拟器由调试服自己从 build/scripts/wallpaper-engine.js 提供
// (它默认不进发布产物,见 ADR 0006 §1 与 dev.ts 的路由说明)。
import { withPage, sleep } from "../cdp.mjs";
const BASE = process.argv[2] ?? "http://127.0.0.1:5173";
let failures = 0;
const check = (ok: unknown, label: string, detail?: unknown) => {
if (!ok) failures++;
console.log(` ${ok ? "✓" : "✗"} ${label}${detail !== undefined ? ` → ${String(detail)}` : ""}`);
};
const RELEASE = "collection-all";
/** 就绪判定:spine-container 里出现 canvas(= SpinePlayer 真的创建成功)。 */
async function waitCanvas(evaluate, tag) {
const deadline = Date.now() + 40000;
while (Date.now() < deadline) {
const state = await evaluate(`(function(){
var c = document.getElementById("spine-container");
return { canvas: c ? c.querySelectorAll("canvas").length : 0, children: c ? c.children.length : -1 };
})()`);
if (state && state.canvas > 0) return { ok: true, ...state };
await sleep(200);
}
return { ok: false, canvas: 0, error: `${tag} 超时` };
}
function assertClean(tag, exceptions, consoleLines) {
check(exceptions.length === 0, `${tag} 0 未捕获异常`, exceptions.slice(0, 2).join(" | ") || "无");
const noisy = consoleLines.filter((l) => /error|uncaught/i.test(l));
check(noisy.length === 0, `${tag} 0 控制台报错`, noisy.slice(0, 3).join(" | ") || "干净");
}
// ── ① 正常路径 ──
console.log("=== ① 正常路径:模拟器生效 + 属性下发 ===");
const normal = await withPage({ width: 1920, height: 1080 }, async ({ send, evaluate, consoleLines, exceptions }) => {
await send("Page.navigate", { url: `${BASE}/release/${RELEASE}/` });
const ready = await waitCanvas(evaluate, "normal");
await sleep(600);
const sim = await evaluate(`(function(){
var w = window.__weSim;
if (!w) return null;
return { isSimulated: w.__isSimulated, substituted: w.substituted.length,
release: w.release, deliveries: w.deliveries.map(function(d){ return d.type; }),
panel: !!document.getElementById("wesim"),
panels: document.querySelectorAll("#wesim").length,
edges: document.querySelectorAll("#wesim-edge").length,
banner: document.getElementById("wesim") ? document.querySelector("#wesim .banner").textContent : null,
hasEngineGlobal: typeof window.wallpaperEngine,
hasRegisterListener: typeof window.wallpaperRegisterListener };
})()`);
const view = await evaluate(`(function(){
return { bg: document.body.style.backgroundImage,
canvas: document.querySelectorAll("canvas").length,
canvasSize: (function(){ var c = document.querySelector("canvas"); return c ? c.clientWidth + "x" + c.clientHeight : null; })() };
})()`);
return { ready, sim, view, consoleLines, exceptions };
});
check(normal.ready.ok, "Spine 画布已创建(渲染就绪)", normal.ready.error ?? `canvas=${normal.ready.canvas}`);
check(normal.sim?.isSimulated === true, "window.__weSim.__isSimulated = true");
check((normal.sim?.substituted ?? 0) > 0, "替身清单非空(做不到的 API 被显式标记)", `${normal.sim?.substituted} 项`);
check(normal.sim?.panel === true, "模拟器面板已挂载(#wesim)");
// 回归断言:`--with-sim` 构建的分发自带驱动,调试服又会注入一次。
// 没有幂等闸的话模拟器会被 mount 两遍,页面上出现两个面板。
check(normal.sim?.panels === 1, "面板恰好一个(驱动幂等,没有被注入两次)", `${normal.sim?.panels} 个`);
check(normal.sim?.edges === 1, "右边缘热区恰好一个", `${normal.sim?.edges} 个`);
check(/模拟环境/.test(normal.sim?.banner ?? ""), "面板有常驻的「模拟环境」横幅", normal.sim?.banner?.trim());
check(normal.sim?.deliveries?.includes("applyUserProperties"), "已下发 applyUserProperties", normal.sim?.deliveries?.join(", "));
check(normal.sim?.deliveries?.includes("applyGeneralProperties"), "已下发 applyGeneralProperties", normal.sim?.deliveries?.join(", "));
check(/ava\.jpg/.test(normal.view.bg ?? ""), "默认预设(xilian)的背景已应用", normal.view.bg?.slice(-32));
// 严格还原 WE:这两个 API 在真实 WE 里都不存在,模拟器绝不能凭空提供
check(normal.sim?.hasEngineGlobal === "undefined", "未凭空暴露 window.wallpaperEngine(WE 没有这个全局)", normal.sim?.hasEngineGlobal);
check(normal.sim?.hasRegisterListener === "undefined", "未暴露 window.wallpaperRegisterListener(WE 没有这个 API)", normal.sim?.hasRegisterListener);
assertClean("正常路径", normal.exceptions, normal.consoleLines);
// ── ② 属性下发真的改变了画面 ──
console.log("\n=== ② 经模拟器改 preset → 背景与骨架跟着换 ===");
const switched = await withPage({ width: 1920, height: 1080 }, async ({ send, evaluate, exceptions }) => {
await send("Page.navigate", { url: `${BASE}/release/${RELEASE}/` });
await waitCanvas(evaluate, "switch");
await sleep(600);
const bgBefore = await evaluate("document.body.style.backgroundImage");
// setProperties 收的是**值**,不是 WE 的 { value } 信封(信封由模拟器自己套)
await evaluate('window.__weSim.setProperties({ preset: "kv37" })');
await sleep(2000);
const after = await evaluate(`(function(){
return { bg: document.body.style.backgroundImage,
canvas: document.querySelectorAll("canvas").length,
// 骨架换了就意味着播放器被重建过:容器里应当只有一张新 canvas
children: document.getElementById("spine-container").children.length };
})()`);
return { bgBefore, ...after, exceptions };
});
check(switched.bgBefore !== switched.bg, "背景图已更换", `${switched.bgBefore?.slice(-22)} → ${switched.bg?.slice(-22)}`);
check(/kv37_xilian/.test(switched.bg ?? ""), "换成了 kv37 的背景", switched.bg?.slice(-30));
check(switched.canvas === 1, "换骨架后只剩一张 canvas(旧播放器已 dispose)", `canvas=${switched.canvas}`);
check(switched.children === 1, "容器里没有残留节点(dispose 生效)", `children=${switched.children}`);
check(switched.exceptions.length === 0, "切换过程 0 异常", switched.exceptions.slice(0, 2).join(" | ") || "无");
// ── ③ load 之前的属性下发 ──
console.log("\n=== ③ ?__propsAt=dom:load 之前下发不丢配置 ===");
const race = await withPage({ width: 1920, height: 1080 }, async ({ send, evaluate, exceptions }) => {
const props = encodeURIComponent(JSON.stringify({ preset: "kv37" }));
await send("Page.navigate", { url: `${BASE}/release/${RELEASE}/?__propsAt=dom&__props=${props}` });
const ready = await waitCanvas(evaluate, "race");
await sleep(800);
const state = await evaluate(`(function(){
return { bg: document.body.style.backgroundImage, canvas: document.querySelectorAll("canvas").length,
propsAt: window.__weSim ? window.__weSim.deliveries[0] && window.__weSim.deliveries[0].at : null };
})()`);
return { ready, ...state, exceptions };
});
check(race.ready.ok, "播放器仍被创建(配置没被丢弃)", race.ready.error ?? "ok");
check(/kv37_xilian/.test(race.bg ?? ""), "load 前下发的 preset 已生效", race.bg?.slice(-30));
check(race.canvas === 1, "只有一张 canvas", `canvas=${race.canvas}`);
check(race.exceptions.length === 0, "0 异常", race.exceptions.slice(0, 2).join(" | ") || "无");
// ── ④ 单档分发 ──
console.log("\n=== ④ 单档分发(属性表里没有 preset/bgm) ===");
const single = await withPage({ width: 1920, height: 1080 }, async ({ send, evaluate, consoleLines, exceptions }) => {
await send("Page.navigate", { url: `${BASE}/release/single-hsr-kv37/` });
const ready = await waitCanvas(evaluate, "single");
await sleep(800);
const bg = await evaluate("document.body.style.backgroundImage");
const props = await evaluate("JSON.stringify(Object.keys(window.__weProperties))");
return { ready, bg, props, consoleLines, exceptions };
});
check(single.ready.ok, "单档分发渲染就绪(未白屏)", single.ready.error ?? "ok");
check(/kv37_xilian/.test(single.bg ?? ""), "单档分发背景已应用", single.bg?.slice(-30));
check(!/"preset"/.test(single.props ?? ""), "单档分发确实没有 preset 属性", single.props);
check(!/"bgm"/.test(single.props ?? ""), "单档分发确实没有 bgm 属性", single.props);
assertClean("单档分发", single.exceptions, single.consoleLines);
console.log(failures === 0 ? "\n模拟器浏览器冒烟通过" : `\n模拟器浏览器冒烟失败:${failures} 处`);
process.exit(failures === 0 ? 0 : 1);