// 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);