Files
SpineWallpaper/.scratch/test-resize.mjs
T
Shuery eb56efbd9e feat: 比例适配、bgm 音源选择与 fps 限流,打包 v3
取景(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 / 属性 / 全量验收三套浏览器测试脚本。
2026-09-19 20:29:25 +08:00

127 lines
5.7 KiB
JavaScript
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.
// 验收测试:改窗口尺寸不得重建播放器,且取景要跟着画布比例走。
//
// 判据是"播放器实例身份":给加载后的实例打上标记,之后每次改视口都核对
// window.__player 是否还是同一个对象、标记是否还在。重建会换掉实例,
// 标记随之消失——这比数日志可靠得多。
import { withPage, sleep } from "../tools/cdp.mjs";
const URL = "http://127.0.0.1:8190/?__freeze=1";
const sizes = [
[1920, 1080],
[2560, 1440],
[3440, 1440],
[1080, 1920],
[1600, 900],
[2560, 1600],
[3840, 2160],
[1280, 720],
[1920, 1080],
];
const stress = [];
for (let i = 0; i < 20; i++) stress.push(i % 2 ? [1080, 1920] : [1920, 1080]);
const out = await withPage(
{ width: 1920, height: 1080, extraArgs: ["--enable-precise-memory-info", "--js-flags=--expose-gc"] },
async ({ send, evaluate, consoleLines, exceptions }) => {
await send("Page.navigate", { url: URL });
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(600);
const stamp = await evaluate(`(function(){
window.__first = window.__player;
if (window.__player) window.__player.__probe = 'A';
return { hasPlayer: !!window.__player, error: window.__test ? window.__test.error : null };
})()`);
const rows = [];
const measure = async (w, h, tag) => {
await send("Emulation.setDeviceMetricsOverride", {
width: w,
height: h,
deviceScaleFactor: 1,
mobile: false,
});
await sleep(420);
// 先强制回收再读堆,否则量到的是 V8 的延迟回收节奏而不是真实占用
await evaluate('(typeof gc === "function") ? (gc(), true) : false');
const r = await evaluate(`(function(){
var p = window.__player;
if (!p) return null;
var v = p.currentViewport;
return {
same: p === window.__first,
probe: p.__probe,
css: [p.canvas.clientWidth, p.canvas.clientHeight],
resolved: [ +v.x.toFixed(2), +v.y.toFixed(2), +v.width.toFixed(2), +v.height.toFixed(2) ],
pad: [v.padLeft, v.padRight, v.padTop, v.padBottom],
childViews: window.__player.config.viewport.animations ? Object.keys(window.__player.config.viewport.animations).length : -1,
heap: performance.memory ? performance.memory.usedJSHeapSize : 0,
drawCalls: window.__test ? window.__test.drawCalls : 0
};
})()`);
rows.push({ tag, w, h, ...(r ?? { same: false, probe: null, css: null, resolved: null, pad: null, heap: 0, drawCalls: 0 }) });
};
for (const [w, h] of sizes) await measure(w, h, `${w}x${h}`);
const heapBeforeStress = rows[rows.length - 1].heap;
for (const [w, h] of stress) await measure(w, h, `压力 ${w}x${h}`);
const heapAfterStress = rows[rows.length - 1].heap;
return { stamp, rows, heapBeforeStress, heapAfterStress, consoleLines, exceptions };
},
);
const { stamp, rows, heapBeforeStress, heapAfterStress, consoleLines, exceptions } = out;
let failures = 0;
const fail = (msg) => {
failures++;
console.log(` ✗ ${msg}`);
};
console.log(`播放器就绪: ${stamp.hasPlayer ? "是" : "否"} 驱动报错: ${stamp.error ?? "无"}`);
console.log("");
console.log("画布尺寸 实例未重建 内边距 可见矩形 比例核对");
for (const r of rows) {
if (r.tag.startsWith("压力")) continue;
const a = r.w / r.h;
const ra = r.resolved ? r.resolved[2] / r.resolved[3] : NaN;
const ratioOk = Math.abs(ra - a) < 0.002;
const padOk = r.pad && r.pad.every((p) => p === 0);
const rect = r.resolved ? r.resolved.map((n) => String(n).padStart(9)).join(" ") : "—";
console.log(
`${r.tag.padEnd(14)} ${String(r.same).padEnd(10)} ${(padOk ? "全 0 ✓" : JSON.stringify(r.pad)).padEnd(10)} ${rect} ${ratioOk ? "✓" : `✗ 期望 ${a.toFixed(4)} 实得 ${ra.toFixed(4)}`}`,
);
if (!r.same) fail(`${r.tag} 重建了播放器(标记丢失: ${r.probe})`);
if (!padOk) fail(`${r.tag} 内边距没有归零`);
if (!ratioOk) fail(`${r.tag} 可见矩形比例与画布不符`);
}
// 首尾同为 1920x1080:取景必须回到完全相同的值,证明重算可逆、无漂移
const first = rows.find((r) => r.tag === "1920x1080");
const last = [...rows].reverse().find((r) => r.tag === "1920x1080");
const reversible = first.resolved.every((n, i) => Math.abs(n - last.resolved[i]) < 0.01);
if (!reversible) fail(`回到 16:9 后取景漂移:${first.resolved} → ${last.resolved}`);
console.log(`\n回到 16:9 取景可逆: ${reversible ? "✓" : "✗"} ${first.resolved} → ${last.resolved}`);
const growth = heapAfterStress - heapBeforeStress;
console.log(`连续 20 次 resize 堆内存: ${(heapBeforeStress / 1048576).toFixed(1)} MB → ${(heapAfterStress / 1048576).toFixed(1)} MB(增长 ${(growth / 1048576).toFixed(2)} MB)`);
if (growth > 4 * 1048576) fail(`连续 resize 内存增长过大:${(growth / 1048576).toFixed(2)} MB`);
const drawCalls = rows[rows.length - 1].drawCalls;
console.log(`累计绘制帧数: ${drawCalls}`);
if (!(drawCalls > 0)) fail("没有绘制任何帧");
const noisy = consoleLines.filter((l) => /error|warn|uncaught|fail/i.test(l));
console.log(`控制台异常: ${noisy.length ? noisy.slice(0, 5).join(" | ") : "无 ✓"}`);
if (noisy.length) fail("控制台有报错/警告");
console.log(`未捕获异常: ${exceptions.length ? exceptions.slice(0, 4).join(" | ") : "无 ✓"}`);
if (exceptions.length) fail("有未捕获异常");
console.log(`\n结论:${failures === 0 ? "全部通过 ✓" : `${failures} 项失败 ✗`}`);
process.exit(failures === 0 ? 0 : 1);