/** * 浏览器实测:刷新速度按链路各存一份(DOM + localStorage 往返)。 * 单测覆盖不到的部分:输入框回填、切链路自动换、刷新页面后仍各自独立。 */ const CDP_BASE = "http://127.0.0.1:9333"; async function ws() { const list = await (await fetch(CDP_BASE + "/json/list")).json(); const page = list.find((t) => t.type === "page"); return page.webSocketDebuggerUrl; } const { WebSocket } = await import("node:http").then(() => import("ws")).catch(() => ({})); // 没有 ws 依赖时用原生 WebSocket(Node 22+) const WSImpl = globalThis.WebSocket || WebSocket; const url = await ws(); const sock = new WSImpl(url); let id = 0; const pending = new Map(); await new Promise((r, j) => { sock.onopen = r; sock.onerror = j; }); sock.onmessage = (ev) => { const m = JSON.parse(ev.data); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } }; function send(method, params = {}) { const myId = ++id; return new Promise((res) => { pending.set(myId, res); sock.send(JSON.stringify({ id: myId, method, params })); }); } async function evaluate(expr) { const r = await send("Runtime.evaluate", { expression: expr, awaitPromise: true, returnByValue: true }); if (r.result && r.result.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails)); return r.result && r.result.result && r.result.result.value; } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let pass = 0, fail = 0; const out = []; function check(name, cond, detail) { if (cond) { pass++; out.push(" \u2713 " + name); } else { fail++; out.push(" \u2717 " + name + (detail ? " \u2014 " + detail : "")); } } await send("Runtime.enable"); await send("Page.enable"); /* 页面服务端口可传参:node scripts/xxx.mjs [port](默认 2625) */ const PORT = Number(process.argv[2] || 2625); const PAGE = `http://127.0.0.1:${PORT}/pages/motor.html`; async function goto(u) { await send("Page.navigate", { url: u }); await sleep(2500); } /* 干净起点:清掉本页 localStorage 再载入 */ await goto(PAGE); await evaluate(`localStorage.removeItem("dcu_motor_ui_v2"); localStorage.removeItem("dcu_motor_transport"); "ok"`); await goto(PAGE); out.push("\n[A] 首次进入(无历史数据)"); let s = await evaluate(`JSON.stringify({tp: motorDbg.state.tpId, input: document.querySelector("#refreshRate").value, eff: motorDbg.state.refreshIntervalMs, map: motorDbg.state.refreshByTp})`); let o = JSON.parse(s); check("默认链路 wifi", o.tp === "wifi", s); check("输入框显示 250", o.input === "250", s); check("生效值 250", o.eff === 250, s); check("两份都在(wifi250/ecat50)", o.map.wifi === 250 && o.map.ecat === 50, s); out.push("\n[B] 切到 EtherCAT → 自动换成 50ms"); await evaluate(`(() => { const s=document.querySelector("#transportSelect"); s.value="ecat"; s.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(1200); s = await evaluate(`JSON.stringify({tp: motorDbg.state.tpId, input: document.querySelector("#refreshRate").value, eff: motorDbg.state.refreshIntervalMs})`); o = JSON.parse(s); check("链路已切到 ecat", o.tp === "ecat", s); check("输入框自动变 50(不是留在 250)", o.input === "50", s); check("生效值 50", o.eff === 50, s); out.push("\n[C] 在 ECAT 下改成 120ms,只改这一份"); await evaluate(`(() => { const i=document.querySelector("#refreshRate"); i.value="120"; i.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(600); s = await evaluate(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, map: motorDbg.state.refreshByTp, ls: JSON.parse(localStorage.getItem("dcu_motor_ui_v2")||"{}").refreshByTp})`); o = JSON.parse(s); check("ecat 生效 120", o.eff === 120, s); check("wifi 那一份仍是 250(没被污染)", o.map.wifi === 250, s); check("已写入 localStorage", o.ls && o.ls.ecat === 120 && o.ls.wifi === 250, s); out.push("\n[D] 切回 WiFi → 自动换回 250ms"); await evaluate(`(() => { const s=document.querySelector("#transportSelect"); s.value="wifi"; s.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(1200); s = await evaluate(`JSON.stringify({tp: motorDbg.state.tpId, input: document.querySelector("#refreshRate").value, eff: motorDbg.state.refreshIntervalMs, map: motorDbg.state.refreshByTp})`); o = JSON.parse(s); check("链路回到 wifi", o.tp === "wifi", s); check("输入框换回 250(没带着 ECAT 的 120)", o.input === "250", s); check("生效值 250", o.eff === 250, s); check("ecat 那一份仍记着 120", o.map.ecat === 120, s); out.push("\n[E] 在 WiFi 下改成 800ms,再刷新页面"); await evaluate(`(() => { const i=document.querySelector("#refreshRate"); i.value="800"; i.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(600); await goto(PAGE); s = await evaluate(`JSON.stringify({tp: motorDbg.state.tpId, input: document.querySelector("#refreshRate").value, eff: motorDbg.state.refreshIntervalMs, map: motorDbg.state.refreshByTp})`); o = JSON.parse(s); check("刷新后仍是 wifi 链路", o.tp === "wifi", s); check("刷新后输入框恢复 800", o.input === "800", s); check("刷新后生效值 800", o.eff === 800, s); check("刷新后 ecat 那一份仍是 120", o.map.ecat === 120, s); out.push("\n[F] 刷新后切到 ECAT,仍取回自己的 120"); await evaluate(`(() => { const s=document.querySelector("#transportSelect"); s.value="ecat"; s.dispatchEvent(new Event("change")); return "ok"; })()`); await sleep(1200); s = await evaluate(`JSON.stringify({input: document.querySelector("#refreshRate").value, eff: motorDbg.state.refreshIntervalMs})`); o = JSON.parse(s); check("输入框 120(跨刷新持久化 + 切换自动换都成立)", o.input === "120", s); check("生效值 120", o.eff === 120, s); out.push("\n[G] 旧数据迁移(模拟老版本只存单个 refreshRate)"); await evaluate(`(() => { localStorage.setItem("dcu_motor_ui_v2", JSON.stringify({refreshRate:"400"})); localStorage.setItem("dcu_motor_transport","wifi"); return "ok"; })()`); await goto(PAGE); s = await evaluate(`JSON.stringify({input: document.querySelector("#refreshRate").value, map: motorDbg.state.refreshByTp})`); o = JSON.parse(s); check("旧单值 400 迁到 wifi 并生效", o.input === "400", s); check("ecat 不继承旧值,仍是默认 50", o.map.ecat === 50, s); console.log(out.join("\n")); console.log("\n" + "=".repeat(56)); console.log(`通过 ${pass} 项,失败 ${fail} 项`); sock.close(); process.exit(fail === 0 ? 0 : 1);