把项目从「手写 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 处引用自包含。
149 lines
9.5 KiB
TypeScript
149 lines
9.5 KiB
TypeScript
// 浏览器端冒烟:在真实无头 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);
|