取景(issue 04) - 新增 viewport-fitter.js:让立绘层缩放跟随背景图的 cover 缩放——背景放大多少倍、 特效就放大多少倍。原先背景走 cover、立绘层走 contain,比例一变两者就分叉。 16:9 下与作者设定逐像素一致(kv37 为 0 差异)。实测证伪了"contain 会裁掉作者视口" 这个最初的判断:contain 在任何比例下都完整含住作者视口。 - 应用方式为原地改写 config.viewport 字段后调 setViewport(),不重建播放器; 维护挂在 config.frame 上按画布比例增量进行(config.success 早于 setViewport,拿不到视口)。 WE 属性(issue 05) - project.json 新增 bgm combo(所有壁纸音源的并集 + 「随预设」);所选项不属于当前壁纸时 音频回落该壁纸默认,选择本身保留。 - fps 限流包裹实例 drawFrame 并接管排程。修掉两个实测缺陷:朴素丢帧会把 rAF 抖动放大成 整拍丢失(目标 30fps 实测只有 22.5fps);fps:0 被真值判断吞掉导致无法取消限流。 打包(issue 06) - project.json version 升至 3 并更新描述;preset 的 combo 取值保持 xilian/kv37 不变。 - preview.gif 821 KB → 347 KB(每 2 帧取 1 帧 + 128 色不抖动,逐帧与源帧像素完全相同)。 - index.html 加内联 favicon,消除 CEF 自动请求 /favicon.ico 造成的 404 报错。 其它 - dist/scripts/wallpaper-engine.js 移到 tools/:它没有任何入口引用,且环境判据依赖真实 WE 并不存在的 wallpaperRegisterListener,一旦按它自己的说明挂进 index.html 会在生产环境自我激活。 - 修正死文件检查:原检查会拿文件名匹配到文件自身的内容,使这类文件漏网。 - 新增离线算式验证与 resize / 属性 / 全量验收三套浏览器测试脚本。
136 lines
6.4 KiB
JavaScript
136 lines
6.4 KiB
JavaScript
// Issue 06 全量验收。
|
||
// ① dist 体积与死文件;② project.json 与代码一致;③ 三种比例 0 控制台报错;
|
||
// ④ 预设往返不丢用户级音源/音量设置。
|
||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||
import { basename, extname, join } from "node:path";
|
||
import { withPage, sleep } from "../tools/cdp.mjs";
|
||
|
||
let failures = 0;
|
||
const check = (ok, msg, detail) => {
|
||
if (!ok) failures++;
|
||
console.log(` ${ok ? "✓" : "✗"} ${msg}${detail !== undefined ? ` → ${detail}` : ""}`);
|
||
};
|
||
|
||
// ── ① 体积与死文件 ──
|
||
console.log("=== ① dist 体积与死文件 ===");
|
||
const walk = (dir) =>
|
||
readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
||
const p = join(dir, e.name);
|
||
return e.isDirectory() ? walk(p) : [p];
|
||
});
|
||
const files = walk("dist");
|
||
const total = files.reduce((s, f) => s + statSync(f).size, 0);
|
||
check(total <= 35 * 1024 * 1024, "总体积 ≤ 35 MB", `${(total / 1048576).toFixed(2)} MB / ${files.length} 个文件`);
|
||
|
||
const TEXT = new Set([".html", ".js", ".json", ".txt", ".css", ".atlas"]);
|
||
const texts = files.filter((f) => TEXT.has(extname(f)));
|
||
const contents = new Map(texts.map((f) => [f, readFileSync(f, "utf8")]));
|
||
// 这四个是 WE 的元数据/入口/说明,本来就不被页面引用
|
||
const ALLOWED = new Set(["project.json", "README.txt", "index.html", "preview.gif"]);
|
||
// 关键:比对时要把文件**自身**的内容排除掉。否则一个文件只要在自己开头写上自己的名字
|
||
// (比如注释里的 "// wallpaper-engine.js")就会被判为"被引用",死文件因此漏网。
|
||
const dead = files.filter((f) => {
|
||
const n = basename(f);
|
||
if (ALLOWED.has(n)) return false;
|
||
const others = texts
|
||
.filter((t) => t !== f)
|
||
.map((t) => contents.get(t))
|
||
.join("\n");
|
||
return !others.includes(n);
|
||
});
|
||
check(dead.length === 0, "0 个死文件", dead.length ? dead.join(", ") : "无孤立资源");
|
||
|
||
// ── ② project.json ──
|
||
console.log("\n=== ② project.json ===");
|
||
const project = JSON.parse(readFileSync("dist/project.json", "utf8"));
|
||
check(project.version === 3, "version = 3", String(project.version));
|
||
check(project.preview === "preview.gif", "preview 指向 preview.gif");
|
||
check(!!project.general.properties.bgm, "新增 bgm combo");
|
||
check(project.general.properties.preset.value === "xilian", "preset 默认值未变");
|
||
const presetValues = project.general.properties.preset.options.map((o) => o.value).sort();
|
||
check(
|
||
JSON.stringify(presetValues) === JSON.stringify(["kv37", "xilian"]),
|
||
"preset 的 combo 取值未变(用户已保存的设置依赖它)",
|
||
presetValues.join(", "),
|
||
);
|
||
const bgmValues = project.general.properties.bgm.options.map((o) => o.value);
|
||
check(bgmValues.includes("auto"), "bgm 含「随预设」档", bgmValues.join(", "));
|
||
const gifSize = statSync("dist/preview.gif").size;
|
||
check(gifSize < 500 * 1024, "preview.gif < 500 KB", `${(gifSize / 1024).toFixed(0)} KB`);
|
||
|
||
// ── ③ 三种比例 0 报错 ──
|
||
console.log("\n=== ③ 21:9 / 16:9 / 9:16 控制台与异常 ===");
|
||
for (const [w, h, tag] of [
|
||
[3440, 1440, "21:9"],
|
||
[1920, 1080, "16:9"],
|
||
[1080, 1920, "9:16"],
|
||
]) {
|
||
const r = await withPage({ width: w, height: h }, async ({ send, evaluate, consoleLines, exceptions }) => {
|
||
await send("Page.navigate", { url: "http://127.0.0.1:8190/?__freeze=1" });
|
||
const deadline = Date.now() + 30000;
|
||
let ready = false;
|
||
while (Date.now() < deadline) {
|
||
if (await evaluate('!!(window.__test && (window.__test.successAt || window.__test.error))')) {
|
||
ready = true;
|
||
break;
|
||
}
|
||
await sleep(150);
|
||
}
|
||
await sleep(500);
|
||
const state = await evaluate(`(function(){
|
||
var p = window.__player, v = p.currentViewport;
|
||
return { w: p.canvas.clientWidth, h: p.canvas.clientHeight,
|
||
vw: +v.width.toFixed(1), vh: +v.height.toFixed(1),
|
||
err: window.__test.error || null };
|
||
})()`);
|
||
return { ready, state, consoleLines, exceptions };
|
||
});
|
||
const noisy = r.consoleLines.filter((l) => /error|uncaught|fail/i.test(l));
|
||
const aspectOk = r.state && Math.abs(r.state.vw / r.state.vh - r.state.w / r.state.h) < 0.002;
|
||
check(r.ready && !r.state?.err, `${tag} 播放器就绪`, r.state?.err ?? "无错");
|
||
check(aspectOk, `${tag} 可见矩形比例与画布一致`, r.state ? `${r.state.vw}×${r.state.vh}` : "—");
|
||
check(noisy.length === 0 && r.exceptions.length === 0, `${tag} 0 控制台报错 / 0 异常`, noisy.concat(r.exceptions).slice(0, 3).join(" | ") || "干净");
|
||
}
|
||
|
||
// ── ④ 预设往返不丢音频设置 ──
|
||
console.log("\n=== ④ 预设往返 10 次,音频设置不得丢失 ===");
|
||
const rt = await withPage({ width: 1280, height: 720 }, async ({ send, evaluate, exceptions }) => {
|
||
await send("Page.navigate", { url: "http://127.0.0.1:8190/?__freeze=1" });
|
||
const deadline = Date.now() + 30000;
|
||
while (Date.now() < deadline) {
|
||
if (await evaluate('!!(window.__test && (window.__test.successAt || window.__test.error))')) break;
|
||
await sleep(150);
|
||
}
|
||
await sleep(500);
|
||
|
||
await evaluate(`window.wallpaperPropertyListener.applyUserProperties({
|
||
audio_file: { value: "https://example.com/custom-track.mp3" },
|
||
audio_volume: { value: 0.2 }
|
||
})`);
|
||
await sleep(300);
|
||
|
||
const rows = [];
|
||
for (let i = 0; i < 10; i++) {
|
||
const id = i % 2 === 0 ? "kv37" : "xilian";
|
||
await evaluate(`window.wallpaperPropertyListener.applyUserProperties({ preset: { value: "${id}" } })`);
|
||
await sleep(320);
|
||
rows.push(
|
||
await evaluate(`(function(){
|
||
var a = document.getElementById("background-music");
|
||
return { id: "${id}", src: a.src.split("/").pop(), vol: a.volume,
|
||
draws: window.__test.drawCalls };
|
||
})()`),
|
||
);
|
||
}
|
||
return { rows, exceptions };
|
||
});
|
||
const srcOk = rt.rows.every((r) => r.src === "custom-track.mp3");
|
||
const volOk = rt.rows.every((r) => Math.abs(r.vol - 0.2) < 0.001);
|
||
check(srcOk, "10 次切换后自定义音源仍生效", [...new Set(rt.rows.map((r) => r.src))].join(", "));
|
||
check(volOk, "10 次切换后音量仍为 0.2", [...new Set(rt.rows.map((r) => r.vol))].join(", "));
|
||
check(rt.rows[rt.rows.length - 1].draws > rt.rows[0].draws, "切换后仍在持续绘制", `${rt.rows[0].draws} → ${rt.rows[rt.rows.length - 1].draws}`);
|
||
check(rt.exceptions.length === 0, "往返过程 0 未捕获异常", rt.exceptions.join(" | ") || "无");
|
||
|
||
console.log(`\n结论:${failures === 0 ? "全部通过 ✓" : `${failures} 项失败 ✗`}`);
|
||
process.exit(failures === 0 ? 0 : 1);
|