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 处引用自包含。
This commit is contained in:
1 parent
b8eee05d78
commit
3f11426964
297 files changed
+216627
-1926
No files matched your search
@@ -0,0 +1,549 @@
|
||||
// 面板还原度验收:模拟器的设置面板必须长得像 Wallpaper Engine 官方面板。
|
||||
//
|
||||
// 断言的是**官方面板的可观察特征**,不是"我们的实现细节":
|
||||
// · 标题栏是 "Wallpaper Settings" + 重置(官方就是这么写的,只有重置跟着界面语言走)
|
||||
// · 属性按 order 排成"图标 + 标签 + 右侧控件"的行,而不是分区堆叠的调试列表
|
||||
// · WE 自带的颜色块:主题配色 → 翻转 → 显示颜色选项 → [亮度/对比度/饱和度/色调偏移]
|
||||
// · 点色块弹出取色器(调色板 + 明度/饱和度方块 + 色相条 + 十六进制 + 确认/取消)
|
||||
// · type:"text" 渲染成真正的富文本说明块(<ul> 列表 + <a> 链接),不是截断的一行提示
|
||||
// · condition 生效(显示作者信息、使用自定义音乐两个开关都各管一摊)
|
||||
// · 底部是 确认 / 取消,且取消能回滚、重置能恢复默认
|
||||
//
|
||||
// 用法:node tools/checks/verify-panel.mts [base]
|
||||
|
||||
import { withPage, sleep } from "../cdp.mjs";
|
||||
|
||||
const BASE = process.argv[2] ?? "http://127.0.0.1:5173";
|
||||
const RELEASE = "collection-all";
|
||||
|
||||
let failures = 0;
|
||||
const check = (ok: unknown, label: string, detail?: unknown) => {
|
||||
if (!ok) failures++;
|
||||
console.log(` ${ok ? "✓" : "✗"} ${label}${detail !== undefined ? ` → ${String(detail)}` : ""}`);
|
||||
};
|
||||
|
||||
const probe = `(function(){
|
||||
var root = document.getElementById("wesim");
|
||||
if (!root) return null;
|
||||
var q = function (s) { return root.querySelector(s); };
|
||||
var rows = Array.prototype.map.call(root.querySelectorAll(".body .row"), function (r) {
|
||||
var c = r.querySelector(".ctl");
|
||||
var input = c && c.querySelector("input, select");
|
||||
var kind = "none";
|
||||
if (input) kind = input.type === "select-one" ? "select" : input.type;
|
||||
else if (c && c.querySelector(".combo")) kind = "combo";
|
||||
else if (c && c.querySelector(".swatch")) kind = "swatch";
|
||||
return {
|
||||
icon: (r.querySelector(".ico") || {}).textContent || "",
|
||||
label: (r.querySelector(".label") || {}).textContent || "",
|
||||
control: kind,
|
||||
value: input ? input.value : (c && c.querySelector(".combo") ? c.querySelector(".combo").dataset.value : null),
|
||||
comboLabel: c && c.querySelector(".combo-label") ? c.querySelector(".combo-label").textContent : null
|
||||
};
|
||||
});
|
||||
var notes = Array.prototype.map.call(root.querySelectorAll(".body .note"), function (n) {
|
||||
return {
|
||||
text: n.textContent.replace(/\\s+/g, " ").trim(),
|
||||
items: n.querySelectorAll("li").length,
|
||||
links: Array.prototype.map.call(n.querySelectorAll("a"), function (a) { return a.getAttribute("href"); })
|
||||
};
|
||||
});
|
||||
return {
|
||||
title: (q(".title .name") || {}).textContent || null,
|
||||
reset: (q(".title .reset") || {}).textContent || null,
|
||||
rows: rows,
|
||||
notes: notes,
|
||||
foot: Array.prototype.map.call(root.querySelectorAll(".foot button"), function (b) { return b.textContent; }),
|
||||
hasDebug: !!q(".dbg"),
|
||||
debugOpen: q(".dbg") ? q(".dbg").open : null,
|
||||
banner: (q(".banner") || {}).textContent || null,
|
||||
outsideBody: root.parentElement === document.documentElement,
|
||||
bodyTransform: document.body.style.transform,
|
||||
bodyFilter: document.body.style.filter
|
||||
};
|
||||
})()`;
|
||||
|
||||
/** 按标签点一个控件的 change(复刻用户在面板上的操作)。 */
|
||||
const clickByLabel = (label, action) => `(function(){
|
||||
var rows = document.querySelectorAll("#wesim .body .row");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var l = rows[i].querySelector(".label");
|
||||
if (!l || l.textContent !== ${JSON.stringify(label)}) continue;
|
||||
${action}
|
||||
}
|
||||
return false;
|
||||
})()`;
|
||||
|
||||
const toggle = (label) =>
|
||||
clickByLabel(label, `var cb = rows[i].querySelector("input[type=checkbox]");
|
||||
cb.checked = !cb.checked;
|
||||
cb.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return true;`);
|
||||
|
||||
const setSlider = (label, value) =>
|
||||
clickByLabel(label, `var r = rows[i].querySelector("input[type=range]");
|
||||
r.value = ${JSON.stringify(String(value))};
|
||||
r.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
return true;`);
|
||||
|
||||
const result = await withPage({ width: 1920, height: 1080, profile: "tools/.cache/edge-panel" }, async ({ send, evaluate, consoleLines, exceptions }) => {
|
||||
await send("Page.navigate", { url: `${BASE}/release/${RELEASE}/` });
|
||||
await sleep(3500);
|
||||
|
||||
const initial = await evaluate(probe);
|
||||
|
||||
// ── 显示颜色选项 → 四个滑杆 ──
|
||||
await evaluate(toggle("显示颜色选项"));
|
||||
await sleep(300);
|
||||
const withColorOptions = await evaluate(probe);
|
||||
// 这一批新冒出来的行应当带 .row-enter,而原本就在的行不该带——
|
||||
// 否则每改一个属性整个面板都会重播一遍入场动画。
|
||||
const enterFlags = await evaluate(`(function(){
|
||||
return Array.prototype.map.call(document.querySelectorAll("#wesim .body .row"), function (r) {
|
||||
return { label: (r.querySelector(".label") || {}).textContent || "", enter: r.classList.contains("row-enter") };
|
||||
});
|
||||
})()`);
|
||||
await evaluate(setSlider("亮度", 80));
|
||||
await sleep(200);
|
||||
const afterBrightness = await evaluate(`document.body.style.filter`);
|
||||
|
||||
// ── 翻转 ──
|
||||
await evaluate(toggle("翻转"));
|
||||
await sleep(200);
|
||||
const afterFlip = await evaluate(`document.body.style.transform`);
|
||||
await evaluate(toggle("翻转"));
|
||||
await sleep(200);
|
||||
|
||||
// ── 取色器 ──
|
||||
await evaluate(`document.querySelector("#wesim .swatch").click()`);
|
||||
await sleep(400);
|
||||
const pickerProbe = `(function(){
|
||||
var p = document.getElementById("wesim-picker");
|
||||
if (!p) return null;
|
||||
var swatches = p.querySelectorAll(".pk-palette button:not(.pk-dropper)");
|
||||
var checked = p.querySelector(".pk-palette button.on");
|
||||
var dropper = p.querySelector(".pk-dropper");
|
||||
return {
|
||||
outsideBody: p.parentElement === document.documentElement,
|
||||
name: (p.querySelector(".pk-name") || {}).textContent || null,
|
||||
palette: swatches.length,
|
||||
colors: Array.prototype.map.call(swatches, function (b) { return b.title; }),
|
||||
checked: checked ? checked.title : null,
|
||||
hasSv: !!p.querySelector(".pk-sv"),
|
||||
hasHue: !!p.querySelector(".pk-hue"),
|
||||
hex: (p.querySelector(".pk-hex") || {}).value || null,
|
||||
dropper: !!dropper,
|
||||
dropperDisabled: dropper ? dropper.disabled : null,
|
||||
dropperTitle: dropper ? dropper.title : null,
|
||||
foot: Array.prototype.map.call(p.querySelectorAll(".pk-foot button"), function (b) { return b.textContent; })
|
||||
};
|
||||
})()`;
|
||||
|
||||
const picker = await evaluate(pickerProbe);
|
||||
// 把颜色改成某个预设色,对勾应当落到那个色块上
|
||||
await evaluate(`(function(){
|
||||
var hex = document.querySelector("#wesim-picker .pk-hex");
|
||||
hex.value = "#ff0000";
|
||||
hex.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
})()`);
|
||||
await sleep(200);
|
||||
const pickerChecked = await evaluate(pickerProbe);
|
||||
// 用取色器改颜色并确认
|
||||
await evaluate(`(function(){
|
||||
var hex = document.querySelector("#wesim-picker .pk-hex");
|
||||
hex.value = "#123456";
|
||||
hex.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
document.querySelector("#wesim-picker .pk-foot button.primary").click();
|
||||
})()`);
|
||||
await sleep(300);
|
||||
const colorAfterPick = await evaluate(`window.__weSim.props.schemecolor`);
|
||||
const pickerGone = await evaluate(`!document.getElementById("wesim-picker")`);
|
||||
|
||||
// ── 下拉:自绘控件,弹出列表也要能验 ──
|
||||
const openPresetCombo = `(function(){
|
||||
var rows = document.querySelectorAll("#wesim .body .row");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var l = rows[i].querySelector(".label");
|
||||
if (l && l.textContent === "壁纸预设切换") { rows[i].querySelector(".combo-btn").click(); return true; }
|
||||
}
|
||||
return false;
|
||||
})()`;
|
||||
const comboProbe = `(function(){
|
||||
var box = document.getElementById("wesim-combo");
|
||||
if (!box) return null;
|
||||
return {
|
||||
outsideBody: box.parentElement === document.documentElement,
|
||||
items: Array.prototype.map.call(box.querySelectorAll(".combo-item"), function (b) {
|
||||
return { label: b.textContent, value: b.dataset.value, on: b.classList.contains("on") };
|
||||
})
|
||||
};
|
||||
})()`;
|
||||
|
||||
await evaluate(openPresetCombo);
|
||||
await sleep(250);
|
||||
const comboOpen = await evaluate(comboProbe);
|
||||
// 选第二项 → 属性应当跟着变、弹层收起、按钮文字换掉
|
||||
await evaluate(`document.querySelectorAll("#wesim-combo .combo-item")[1].click()`);
|
||||
await sleep(400);
|
||||
const comboClosed = await evaluate(`!document.getElementById("wesim-combo")`);
|
||||
const presetAfter = await evaluate(`window.__weSim.props.preset`);
|
||||
const comboLabelAfter = await evaluate(`(function(){
|
||||
var rows = document.querySelectorAll("#wesim .body .row");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var l = rows[i].querySelector(".label");
|
||||
if (l && l.textContent === "壁纸预设切换") return rows[i].querySelector(".combo-label").textContent;
|
||||
}
|
||||
return null;
|
||||
})()`);
|
||||
|
||||
// ── condition:关掉「显示作者信息」,作者信息说明块应当消失 ──
|
||||
await evaluate(toggle("显示作者信息"));
|
||||
await sleep(300);
|
||||
const hidden = await evaluate(probe);
|
||||
|
||||
// ── 使用自定义音乐:audio_file 与 bgm 互换 ──
|
||||
await evaluate(toggle("使用自定义音乐"));
|
||||
await sleep(300);
|
||||
const customAudio = await evaluate(probe);
|
||||
await evaluate(toggle("使用自定义音乐"));
|
||||
await sleep(300);
|
||||
const presetAudio = await evaluate(probe);
|
||||
|
||||
// ── 重置 / 取消 / 确认 ──
|
||||
await evaluate(setSlider("音频音量调整", 0.3));
|
||||
await sleep(200);
|
||||
const afterEdit = await evaluate(`window.__weSim.props.audio_volume`);
|
||||
await evaluate(`document.querySelector("#wesim .title .reset").click()`);
|
||||
await sleep(300);
|
||||
const afterReset = await evaluate(`window.__weSim.props.audio_volume`);
|
||||
|
||||
await evaluate(setSlider("音频音量调整", 0.2));
|
||||
await sleep(200);
|
||||
const beforeCancel = await evaluate(`window.__weSim.props.audio_volume`);
|
||||
await evaluate(`document.querySelector("#wesim .foot button:not(.primary)").click()`);
|
||||
await sleep(300);
|
||||
const afterCancel = await evaluate(`window.__weSim.props.audio_volume`);
|
||||
|
||||
await evaluate(setSlider("音频音量调整", 0.6));
|
||||
await sleep(200);
|
||||
await evaluate(`document.querySelector("#wesim .foot button.primary").click()`);
|
||||
await sleep(300);
|
||||
const afterConfirm = await evaluate(`window.__weSim.props.audio_volume`);
|
||||
|
||||
// ── 动效:真的滑出一次才看得到 data-enter ──
|
||||
// 先移到远处再移到边缘:前面点过「取消」,那会把 armed 置 false,
|
||||
// 而 armed 只有"指针离开边缘"才会复位——不先走开一次,合成指针是打不开面板的。
|
||||
await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: 200, y: 540, button: "none", buttons: 0 });
|
||||
await sleep(120);
|
||||
await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: 1912, y: 540, button: "none", buttons: 0 });
|
||||
await sleep(260);
|
||||
const enterProbe = `(function(){
|
||||
var root = document.getElementById("wesim");
|
||||
var rows = root.querySelectorAll(".body .row");
|
||||
return {
|
||||
open: root.dataset.open,
|
||||
dataEnter: root.dataset.enter || null,
|
||||
seq: Array.prototype.map.call(rows, function (r) { return r.style.getPropertyValue("--i"); }).slice(0, 5),
|
||||
rowAnim: rows.length ? getComputedStyle(rows[0]).animationName : null,
|
||||
rowDelay: rows.length ? getComputedStyle(rows[0]).animationDelay : null,
|
||||
hoverTransition: rows.length ? getComputedStyle(rows[0]).transitionProperty : null,
|
||||
pickerAnim: (function () {
|
||||
document.querySelector("#wesim .swatch").click();
|
||||
var p = document.getElementById("wesim-picker");
|
||||
return p ? getComputedStyle(p).animationName : null;
|
||||
})()
|
||||
};
|
||||
})()`;
|
||||
const anim = await evaluate(enterProbe);
|
||||
await evaluate(`(function(){ var p = document.getElementById("wesim-picker"); if (p) p.remove(); })()`);
|
||||
await sleep(600);
|
||||
const afterEnter = await evaluate(`document.getElementById("wesim").dataset.enter || null`);
|
||||
|
||||
// 复选框是自绘的:appearance 关掉、勾靠背景图长出来
|
||||
const checkbox = await evaluate(`(function(){
|
||||
var cb = document.querySelector("#wesim .body input[type=checkbox]");
|
||||
if (!cb) return null;
|
||||
var before = getComputedStyle(cb).backgroundSize;
|
||||
cb.checked = !cb.checked;
|
||||
cb.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return { appearance: getComputedStyle(cb).appearance, before: before };
|
||||
})()`);
|
||||
|
||||
// 下拉箭头靠 aria-expanded 翻转
|
||||
await evaluate(`(function(){
|
||||
var rows = document.querySelectorAll("#wesim .body .row");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var l = rows[i].querySelector(".label");
|
||||
if (l && l.textContent === "背景音乐选择") { rows[i].querySelector(".combo-btn").click(); return true; }
|
||||
}
|
||||
return false;
|
||||
})()`);
|
||||
await sleep(250);
|
||||
const arrowOpen = await evaluate(`(function(){
|
||||
var b = document.querySelector("#wesim .combo-btn[aria-expanded]");
|
||||
return { expanded: b ? b.getAttribute("aria-expanded") : null,
|
||||
rotated: b ? getComputedStyle(b.querySelector(".combo-arrow")).transform : null };
|
||||
})()`);
|
||||
await evaluate(`(function(){ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); })()`);
|
||||
await sleep(250);
|
||||
const arrowClosed = await evaluate(`(function(){
|
||||
var b = document.querySelector("#wesim .combo-btn[aria-expanded]");
|
||||
return b ? b.getAttribute("aria-expanded") : null;
|
||||
})()`);
|
||||
|
||||
// 系统开了"减少动态效果"时应当全部关掉
|
||||
await send("Emulation.setEmulatedMedia", {
|
||||
features: [{ name: "prefers-reduced-motion", value: "reduce" }],
|
||||
});
|
||||
await sleep(200);
|
||||
const reduced = await evaluate(`(function(){
|
||||
var r = document.querySelector("#wesim .body .row");
|
||||
var p = document.getElementById("wesim-picker");
|
||||
return { rowAnim: r ? getComputedStyle(r).animationDuration : null,
|
||||
rowTransition: r ? getComputedStyle(r).transitionDuration : null };
|
||||
})()`);
|
||||
await send("Emulation.setEmulatedMedia", { features: [] });
|
||||
|
||||
// ── 预览图:设置面板上方 ──
|
||||
const previewProbe = `(function(){
|
||||
var p = document.querySelector("#wesim .preview");
|
||||
if (!p) return { present: false };
|
||||
var img = p.querySelector("img");
|
||||
var src = img ? img.getAttribute("src") : null;
|
||||
return {
|
||||
present: true,
|
||||
src: src,
|
||||
isData: src ? src.indexOf("data:") === 0 : null,
|
||||
natural: img ? img.naturalWidth + "x" + img.naturalHeight : null,
|
||||
navs: Array.prototype.map.call(p.querySelectorAll(".nav"), function (b) {
|
||||
return { cls: b.className, disabled: b.disabled, glyph: b.textContent };
|
||||
})
|
||||
};
|
||||
})()`;
|
||||
const preview = await evaluate(previewProbe);
|
||||
|
||||
// ── 单档分发里的「背景音乐选择」──
|
||||
//
|
||||
// 这条是回归断言:bgm 曾经跟着 preset 一起被删掉("只剩一档壁纸,切换没有意义"),
|
||||
// 但那个理由只对**预设切换**成立——一档壁纸照样能带好几首曲子(xilian 就是两首)。
|
||||
// 结果 single-hsr-xilian 里根本没有切换入口,而当时没有任何断言盯着这件事。
|
||||
const bgmRowProbe = `(function(){
|
||||
var rows = document.querySelectorAll("#wesim .body .row");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var l = rows[i].querySelector(".label");
|
||||
if (l && l.textContent === "背景音乐选择") {
|
||||
var combo = rows[i].querySelector(".combo");
|
||||
var label = rows[i].querySelector(".combo-label");
|
||||
return { present: true, value: combo ? combo.dataset.value : null,
|
||||
label: label ? label.textContent : null };
|
||||
}
|
||||
}
|
||||
return { present: false };
|
||||
})()`;
|
||||
|
||||
await send("Page.navigate", { url: `${BASE}/release/single-hsr-xilian/` });
|
||||
await sleep(3200);
|
||||
const singleTwoTracks = await evaluate(bgmRowProbe);
|
||||
// 展开看有几个选项
|
||||
await evaluate(`(function(){
|
||||
var rows = document.querySelectorAll("#wesim .body .row");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var l = rows[i].querySelector(".label");
|
||||
if (l && l.textContent === "背景音乐选择") { rows[i].querySelector(".combo-btn").click(); return true; }
|
||||
}
|
||||
return false;
|
||||
})()`);
|
||||
await sleep(250);
|
||||
const singleTwoTrackItems = await evaluate(
|
||||
`Array.prototype.map.call(document.querySelectorAll("#wesim-combo .combo-item"), function (b) { return b.textContent; })`,
|
||||
);
|
||||
|
||||
await send("Page.navigate", { url: `${BASE}/release/single-hsr-kv37/` });
|
||||
await sleep(3200);
|
||||
const singleOneTrack = await evaluate(bgmRowProbe);
|
||||
|
||||
return {
|
||||
initial, withColorOptions, enterFlags, anim, afterEnter, checkbox, arrowOpen, arrowClosed, reduced,
|
||||
preview,
|
||||
singleTwoTracks, singleTwoTrackItems, singleOneTrack,
|
||||
afterBrightness, afterFlip, picker, pickerChecked, colorAfterPick, pickerGone,
|
||||
comboOpen, comboClosed, presetAfter, comboLabelAfter,
|
||||
hidden, customAudio, presetAudio, afterEdit, afterReset, beforeCancel, afterCancel, afterConfirm,
|
||||
consoleLines, exceptions,
|
||||
};
|
||||
});
|
||||
|
||||
const r = result;
|
||||
const labelsOf = (state) => (state?.rows ?? []).map((x) => x.label);
|
||||
|
||||
console.log("=== ① 标题栏(官方:" + '"Wallpaper Settings" + 重置' + ")===");
|
||||
check(r.initial?.title === "Wallpaper Settings", '标题是 "Wallpaper Settings"', r.initial?.title);
|
||||
check(/重置/.test(r.initial?.reset ?? ""), "右侧有「重置」", r.initial?.reset?.trim());
|
||||
check(/模拟环境/.test(r.initial?.banner ?? ""), "顶部保留「模拟环境」横幅(ADR 0006 要求,官方没有)");
|
||||
|
||||
console.log("\n=== ② 属性行:图标 + 标签 + 右侧控件,按 order 排 ===");
|
||||
console.log(` 实际顺序:${labelsOf(r.initial).join(" / ")}`);
|
||||
const expectOrder = ["主题配色", "翻转", "显示颜色选项", "显示作者信息", "壁纸预设切换", "音频音量调整", "使用自定义音乐", "背景音乐选择"];
|
||||
check(JSON.stringify(labelsOf(r.initial)) === JSON.stringify(expectOrder), "行顺序与 order 字段一致", `${labelsOf(r.initial).length} 行`);
|
||||
check(r.initial?.rows?.[0]?.control === "swatch", "主题配色渲染成色块", r.initial?.rows?.[0]?.control);
|
||||
check((r.initial?.rows ?? []).filter((x) => x.control === "combo").length === 2, "两个 combo(壁纸预设切换 / 背景音乐选择)都渲染成自绘下拉");
|
||||
check((r.initial?.rows ?? []).some((x) => x.label === "音频音量调整" && x.control === "range"), "slider 渲染成 range");
|
||||
const icons = (r.initial?.rows ?? []).filter((x) => x.icon).length;
|
||||
check(icons >= 3, "属性文本开头的 emoji 被当作行图标", `${icons} 行有图标`);
|
||||
|
||||
console.log("\n=== ③ WE 自带的颜色块 ===");
|
||||
check(r.initial?.rows?.[1]?.label === "翻转" && r.initial?.rows?.[1]?.control === "checkbox", "「翻转」是 WE 自带的一行");
|
||||
check(labelsOf(r.initial).includes("亮度") === false, "默认不显示四个颜色滑杆(显示颜色选项未勾)");
|
||||
check(
|
||||
JSON.stringify(labelsOf(r.withColorOptions).slice(2, 7)) === JSON.stringify(["显示颜色选项", "亮度", "对比度", "饱和度", "色调偏移"]),
|
||||
"勾上「显示颜色选项」后出现四个滑杆",
|
||||
labelsOf(r.withColorOptions).slice(2, 7).join(" / "),
|
||||
);
|
||||
check((r.withColorOptions?.rows ?? []).filter((x) => x.control === "range").length === 5, "共 5 个滑杆(4 颜色 + 1 音量)");
|
||||
check(/brightness/.test(r.afterBrightness ?? ""), "亮度落到 body 的 CSS 滤镜上", r.afterBrightness);
|
||||
check(r.afterFlip === "scaleX(-1)", "翻转落到 body 的 transform 上", r.afterFlip);
|
||||
check(r.initial?.outsideBody === true, "面板挂在 <html> 下(body 上的 transform/filter 不会波及它)");
|
||||
|
||||
console.log("\n=== ④ 取色器弹层 ===");
|
||||
check(r.picker !== null, "点色块弹出取色器");
|
||||
check(r.picker?.outsideBody === true, "取色器也在 body 之外(不被滤镜调色)");
|
||||
check(/主题配色/.test(r.picker?.name ?? ""), "弹层标题是「主题配色」", r.picker?.name);
|
||||
check(r.picker?.palette === 15, "调色板 15 个色块(3 列 × 5 行)", `${r.picker?.palette} 个`);
|
||||
const official = [
|
||||
"#ffffff", "#c0c0c0", "#000000",
|
||||
"#ff0000", "#ffa500", "#ffff00",
|
||||
"#00ff00", "#008000", "#254117",
|
||||
"#add8e6", "#0000ff", "#00008b",
|
||||
"#00ffff", "#800080", "#ff00ff",
|
||||
];
|
||||
check(
|
||||
JSON.stringify(r.picker?.colors) === JSON.stringify(official),
|
||||
"15 个预设色与官方逐个一致",
|
||||
JSON.stringify(r.picker?.colors) === JSON.stringify(official) ? "一致" : (r.picker?.colors ?? []).join(" "),
|
||||
);
|
||||
check(r.picker?.checked === "#63269e" || r.picker?.checked === null, "初始颜色不是预设色时没有对勾", String(r.picker?.checked));
|
||||
check(r.pickerChecked?.checked === "#ff0000", "颜色改成预设色后,对勾落到那个色块上", String(r.pickerChecked?.checked));
|
||||
check(r.picker?.dropper === true, "调色板下方有吸管按钮");
|
||||
check(
|
||||
r.picker?.dropperDisabled === false || r.picker?.dropperDisabled === true,
|
||||
r.picker?.dropperDisabled ? "吸管已禁用并说明原因(本机 CEF 无 EyeDropper API)" : "吸管可用(EyeDropper API 存在)",
|
||||
r.picker?.dropperTitle,
|
||||
);
|
||||
check(r.picker?.hasSv === true && r.picker?.hasHue === true, "有明度/饱和度方块与色相条");
|
||||
check(/^#[0-9a-f]{6}$/i.test(r.picker?.hex ?? ""), "十六进制输入框有值", r.picker?.hex);
|
||||
check(JSON.stringify(r.picker?.foot) === JSON.stringify(["确认", "取消"]), "弹层底部是 确认 / 取消", (r.picker?.foot ?? []).join(" / "));
|
||||
check(/^0\.\d+ 0\.\d+ 0\.\d+$/.test(r.colorAfterPick ?? ""), "确认后写回 schemecolor(r g b 浮点)", r.colorAfterPick);
|
||||
check(r.pickerGone === true, "确认后弹层关闭");
|
||||
|
||||
console.log("\n=== ⑤ type:text 渲染成富文本说明块 ===");
|
||||
const notes = r.initial?.notes ?? [];
|
||||
check(notes.length >= 3, "说明块都在", `${notes.length} 个`);
|
||||
check(notes.filter((n) => n.items > 0).length >= 2, "说明块里有 <ul><li> 列表");
|
||||
const links = notes.flatMap((n) => n.links ?? []);
|
||||
check(links.length > 0 && links.every((h) => /^https?:/.test(h ?? "")), "链接被保留且只放行 http(s)", links.join(" | "));
|
||||
|
||||
console.log("\n=== ⑥ condition:两个开关各管一摊 ===");
|
||||
check(!(r.hidden?.notes ?? []).some((n) => n.text.includes("作者:")), "关掉「显示作者信息」→ 作者信息块消失");
|
||||
check(!labelsOf(r.customAudio).includes("背景音乐选择"), "开「使用自定义音乐」→ 背景音乐选择隐藏");
|
||||
check(labelsOf(r.customAudio).includes("音频文件路径"), "开「使用自定义音乐」→ 音频文件路径出现");
|
||||
check(labelsOf(r.presetAudio).includes("背景音乐选择"), "关「使用自定义音乐」→ 背景音乐选择回来");
|
||||
check(!labelsOf(r.presetAudio).includes("音频文件路径"), "关「使用自定义音乐」→ 音频文件路径隐藏");
|
||||
|
||||
console.log("\n=== ⑦ 底部 确认 / 取消 + 重置语义 ===");
|
||||
check(JSON.stringify(r.initial?.foot) === JSON.stringify(["确认", "取消"]), "底部是 确认 / 取消", (r.initial?.foot ?? []).join(" / "));
|
||||
check(r.afterEdit === 0.3, "改音量已实时下发", String(r.afterEdit));
|
||||
check(r.afterReset === 1, "重置恢复默认值 1", String(r.afterReset));
|
||||
check(r.beforeCancel === 0.2 && r.afterCancel === 1, "取消回滚到上次确认的值", `${r.beforeCancel} → ${r.afterCancel}`);
|
||||
check(r.afterConfirm === 0.6, "确认保留改动", String(r.afterConfirm));
|
||||
|
||||
console.log("\n=== ⑧ 模拟器专属内容收进折叠区 ===");
|
||||
check(r.initial?.hasDebug === true && r.initial?.debugOpen === false, "「模拟器调试」存在且默认收起");
|
||||
|
||||
console.log("\n=== ⑨ 下拉框(自绘,样式与官方同步)===");
|
||||
check(r.comboOpen !== null, "点下拉按钮弹出列表");
|
||||
check(r.comboOpen?.outsideBody === true, "弹层挂在 documentElement 下(不被 body 的滤镜波及)");
|
||||
const comboItems = r.comboOpen?.items ?? [];
|
||||
console.log(` 选项:${comboItems.map((x) => `${x.label}${x.on ? "(当前)" : ""}`).join(" / ")}`);
|
||||
check(comboItems.length === 2, "两个预设都在列表里", `${comboItems.length} 项`);
|
||||
check(comboItems.filter((x) => x.on).length === 1, "当前项恰好一个被高亮", comboItems.filter((x) => x.on).map((x) => x.label).join(""));
|
||||
check(r.comboClosed === true, "选完之后弹层收起");
|
||||
check(r.presetAfter === comboItems[1]?.value, "选中的值下发给壁纸", `${r.presetAfter}`);
|
||||
check(r.comboLabelAfter === comboItems[1]?.label, "按钮上的文字跟着换成所选项", String(r.comboLabelAfter));
|
||||
|
||||
console.log("\n=== ⑩ 动效(只解释状态变化,且受 prefers-reduced-motion 管辖)===");
|
||||
check(r.anim?.open === "1", "面板已滑出", String(r.anim?.open));
|
||||
check(r.anim?.dataEnter === "1", "刚滑出时打了入场标记", String(r.anim?.dataEnter));
|
||||
check(/wesim-row-in/.test(r.anim?.rowAnim ?? ""), "行上跑的是入场动画", r.anim?.rowAnim);
|
||||
check(
|
||||
(r.anim?.seq ?? []).join(",") === "0,1,2,3,4",
|
||||
"行带递增的 --i(用来错开入场)",
|
||||
(r.anim?.seq ?? []).join(","),
|
||||
);
|
||||
check(r.afterEnter === null, "入场标记播完就摘掉(不会每次重画都重播)", String(r.afterEnter));
|
||||
check(/background-color|transform/.test(r.anim?.hoverTransition ?? ""), "行有悬停过渡", r.anim?.hoverTransition);
|
||||
check(/wesim-pop/.test(r.anim?.pickerAnim ?? ""), "取色器弹层有出现动画", r.anim?.pickerAnim);
|
||||
|
||||
const entered = (r.enterFlags ?? []).filter((x) => x.enter).map((x) => x.label);
|
||||
console.log(` 带入场标记的行:${entered.join(" / ") || "(无)"}`);
|
||||
check(
|
||||
JSON.stringify(entered) === JSON.stringify(["亮度", "对比度", "饱和度", "色调偏移"]),
|
||||
"只有因条件变化新冒出来的行才播入场",
|
||||
entered.join(" / "),
|
||||
);
|
||||
|
||||
check(r.checkbox?.appearance === "none", "复选框是自绘的(原生外观已关掉)", String(r.checkbox?.appearance));
|
||||
check(r.arrowOpen?.expanded === "true", "展开时按钮 aria-expanded=true");
|
||||
check(/matrix\(-1/.test(r.arrowOpen?.rotated ?? ""), "箭头翻了过来", r.arrowOpen?.rotated);
|
||||
check(r.arrowClosed === "false", "收起后 aria-expanded 复位", String(r.arrowClosed));
|
||||
|
||||
check(
|
||||
parseFloat(r.reduced?.rowAnim ?? "1") < 0.01,
|
||||
"prefers-reduced-motion: reduce 时入场动画被关掉",
|
||||
String(r.reduced?.rowAnim),
|
||||
);
|
||||
check(
|
||||
parseFloat(r.reduced?.rowTransition ?? "1") < 0.01,
|
||||
"prefers-reduced-motion: reduce 时过渡也被关掉",
|
||||
String(r.reduced?.rowTransition),
|
||||
);
|
||||
|
||||
console.log("\n=== ⑪ 预览图(官方在设置上方就展示它)===");
|
||||
check(r.preview?.present === true, "预览区已渲染", String(r.preview?.present));
|
||||
check(/preview\.gif$/.test(r.preview?.src ?? ""), "指向分发根的 preview.gif", String(r.preview?.src));
|
||||
check(r.preview?.natural === "160x160", "图片真的解码出来了(不是坏图)", String(r.preview?.natural));
|
||||
// 官方那对左右箭头是切换"已安装的壁纸",模拟器没有那个列表——**不做**。
|
||||
// 这条断言是防回归的:它们曾经存在过(当时拿"切预设"当替身,但那是另一件事)。
|
||||
check(
|
||||
(r.preview?.navs ?? []).length === 0,
|
||||
"预览上没有左右箭头(WE 那个是切换壁纸用的,我们不需要)",
|
||||
`${(r.preview?.navs ?? []).length} 个`,
|
||||
);
|
||||
|
||||
console.log("\n=== ⑫ 单档分发里的「背景音乐选择」===");
|
||||
// 规则:一个分发里可选的音源(不含「随预设」)≥2 才给下拉,否则两个选项效果一样、是废话。
|
||||
check(r.singleTwoTracks?.present === true, "single-hsr-xilian(两首)有「背景音乐选择」", JSON.stringify(r.singleTwoTracks));
|
||||
check(
|
||||
JSON.stringify(r.singleTwoTrackItems) === JSON.stringify(["随预设", "「再度和你」", "昔涟"]),
|
||||
"下拉里就是 xilian 的两首(+ 随预设)",
|
||||
(r.singleTwoTrackItems ?? []).join(" / "),
|
||||
);
|
||||
// 默认值是 "auto"(随预设)——与合集分发一致,不是"第一首"。
|
||||
// 运行时拿到 auto 就回落该壁纸的默认音源,也就是第一首,所以效果相同;
|
||||
// 但值本身必须是 auto,否则用户从合集切到单档时选择会被"钉死"。
|
||||
check(
|
||||
r.singleTwoTracks?.value === "auto" && r.singleTwoTracks?.label === "随预设",
|
||||
"默认是「随预设」(auto),与合集分发一致",
|
||||
`${r.singleTwoTracks?.value} / ${r.singleTwoTracks?.label}`,
|
||||
);
|
||||
check(
|
||||
r.singleOneTrack?.present === false,
|
||||
"single-hsr-kv37(只有一首)不给这个下拉——两个选项效果完全一样",
|
||||
JSON.stringify(r.singleOneTrack),
|
||||
);
|
||||
|
||||
const noisy = (r.consoleLines ?? []).filter((l) => /error|uncaught/i.test(l));
|
||||
check((r.exceptions ?? []).length === 0, "0 未捕获异常", (r.exceptions ?? []).slice(0, 2).join(" | ") || "无");
|
||||
check(noisy.length === 0, "0 控制台报错", noisy.slice(0, 3).join(" | ") || "干净");
|
||||
|
||||
console.log(failures === 0 ? "\n面板还原度:通过" : `\n面板还原度:失败 ${failures} 处`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
Reference in new issue
Block a user