// 验收测试:改窗口尺寸不得重建播放器,且取景要跟着画布比例走。 // // 判据是"播放器实例身份":给加载后的实例打上标记,之后每次改视口都核对 // window.__player 是否还是同一个对象、标记是否还在。重建会换掉实例, // 标记随之消失——这比数日志可靠得多。 import { withPage, sleep } from "../cdp.mjs"; // 服务地址可用 SIM_BASE 覆盖(默认 8190)。写死端口会在换端口时静默连错地方, // 表现得像功能坏了,其实只是测错了对象。 const URL = (process.env.SIM_BASE ?? "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);