/* ============================================================ * debug_physics_balls_dom_smoke.js · 物理弹球 · 前端 DOM 桩冒烟 * 无浏览器环境下用最小 DOM/Canvas 桩加载全部前端脚本, * 驱动主循环 + 模拟拖拽发射/暂停/记录等交互,捕获运行时错误。 * 运行:node dev_test_scripts/debug/debug_physics_balls_dom_smoke.js * ============================================================ */ 'use strict'; const fs = require('fs'); const path = require('path'); const vm = require('vm'); const LAB_DIR = path.join(__dirname, '..', '..', 'public', 'tools', 'thought_lab', 'labs', 'physics_balls'); const FILES = ['pb_physics.js', 'pb_rules.js', 'pb_storage.js', 'pb_audio.js', 'pb_render.js', 'pb_game.js', 'pb_ui.js']; function makeCtx() { const store = {}; return new Proxy(store, { get(t, k) { if (k === 'createRadialGradient') return () => ({ addColorStop() {} }); if (k === 'measureText') return () => ({ width: 10 }); if (k in t) return t[k]; return function () {}; }, set(t, k, v) { t[k] = v; return true; } }); } function makeEl(id) { const listeners = {}; return { id, style: {}, dataset: {}, classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } }, children: [], _html: '', _text: '', set innerHTML(v) { this._html = v; this.children = []; }, get innerHTML() { return this._html; }, set textContent(v) { this._text = String(v); }, get textContent() { return this._text; }, appendChild(c) { this.children.push(c); }, addEventListener(t, f) { (listeners[t] = listeners[t] || []).push(f); }, dispatch(t, ev) { (listeners[t] || []).forEach(f => f(ev || {})); }, getBoundingClientRect() { return { left: 0, top: 0, width: 420, height: 880 }; }, getContext() { return makeCtx(); }, checked: true, width: 420, height: 880 }; } const els = {}; function getEl(id) { if (!els[id]) els[id] = makeEl(id); return els[id]; } let rafCb = null; const winListeners = {}; const sandbox = { console, Math, Date, setTimeout, clearTimeout, JSON, localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} }, navigator: { clipboard: { writeText: () => ({ then: (a) => a && a() }) } }, requestAnimationFrame: (cb) => { rafCb = cb; return 1; }, document: { getElementById: getEl, createElement: (tag) => makeEl('dyn_' + tag) }, addEventListener: (t, f) => { (winListeners[t] = winListeners[t] || []).push(f); }, dispatchWin: (t, ev) => { (winListeners[t] || []).forEach(f => f(ev || {})); }, window: null }; sandbox.window = sandbox; sandbox.globalThis = sandbox; vm.createContext(sandbox); for (const f of FILES) { vm.runInContext(fs.readFileSync(path.join(LAB_DIR, f), 'utf-8'), sandbox, { filename: f }); } function frames(n, t0) { for (let i = 0; i < n; i++) { const cb = rafCb; rafCb = null; if (!cb) throw new Error('rAF chain broken'); cb(t0 + i * 16.7); } } console.log('== 物理弹球 DOM 冒烟 =='); // init(load 事件不会在 vm 里触发,手动调) sandbox.PBUi.init(); frames(30, 0); console.log(' ✓ init + 30 帧主菜单渲染'); // 模拟拖拽发射(pointermove/up 注册在 window 级) const canvas = getEl('pb-canvas'); canvas.dispatch('pointerdown', { clientX: 260, clientY: 600 }); frames(5, 600); sandbox.dispatchWin('pointermove', { clientX: 300, clientY: 650 }); sandbox.dispatchWin('pointerup', {}); frames(60 * 6, 700); console.log(' ✓ 拖拽发射 + 6s 模拟'); // 暂停 / 继续 getEl('pb-pause-btn').dispatch('click'); getEl('pb-resume').dispatch('click'); frames(60, 60 * 7 * 16.7); console.log(' ✓ 暂停/继续'); // 记录页 / 指南 / 设置 / 每日领取 getEl('pb-side-records').dispatch('click'); getEl('pb-records-close').dispatch('click'); getEl('pb-help').dispatch('click'); getEl('pb-guide-ok').dispatch('click'); getEl('pb-gear').dispatch('click'); getEl('pb-settings-close').dispatch('click'); getEl('pb-daily').dispatch('click'); console.log(' ✓ 记录/指南/设置/每日领取交互'); // 长时模拟 60s,覆盖回合推进/上升/回收 for (let s = 0; s < 60; s++) { if (s % 7 === 0) { canvas.dispatch('pointerdown', { clientX: 150 + (s * 13) % 200, clientY: 620 }); sandbox.dispatchWin('pointermove', { clientX: 190 + (s * 13) % 160, clientY: 660 }); sandbox.dispatchWin('pointerup', {}); } frames(60, (10 + s) * 1000); } console.log(' ✓ 60s 长时模拟(含多次发射)'); console.log('\n冒烟通过,无运行时异常');