// 用最小 DOM 桩把 renderer/control.js 跑一遍,重点验证「暂停/继续」按钮的状态文案 // 注意:桩不是真浏览器,只能证明接线与状态同步正确。 const fs = require('fs'); const vm = require('vm'); const path = require('path'); const root = path.join(__dirname, '..'); const read = (p) => fs.readFileSync(path.join(root, p), 'utf8'); let passed = 0; const failures = []; const check = (n, ok, d) => { if (ok) { passed++; console.log(`PASS ${n}${d ? ` [${d}]` : ''}`); } else { failures.push(n); console.log(`FAIL ${n}${d ? ` [${d}]` : ''}`); } }; function makeEl(id) { const classes = new Set(); return { id, textContent: '', value: '', checked: false, hidden: false, style: {}, dataset: {}, handlers: {}, children: [], classList: { add: (c) => classes.add(c), remove: (c) => classes.delete(c), toggle: (c, on) => (on ? classes.add(c) : classes.delete(c)), contains: (c) => classes.has(c) }, _classes: classes, appendChild(c) { this.children.push(c); }, setAttribute() {}, getAttribute() {}, getBoundingClientRect: () => ({ width: 800, height: 600 }), addEventListener(t, fn) { (this.handlers[t] ||= []).push(fn); }, fire(t, ev = {}) { (this.handlers[t] || []).forEach((fn) => fn({ target: this, ...ev })); } }; } const els = new Map(); const byId = (id) => { if (!els.has(id)) els.set(id, makeEl(id)); return els.get(id); }; const segButtons = ['digital', 'analog'].map((s) => { const el = makeEl(`seg-${s}`); el.dataset.style = s; return el; }); const styleGroup = makeEl('styleGroup'); styleGroup.querySelectorAll = () => segButtons; const rateChips = ['-1','-2','0','0.5','1','2','10','60'].map((r) => { const el = makeEl(`chip-${r}`); el.dataset.rate = r; return el; }); const fastChips = ['5000','60000','300000','600000','1800000'].map((d) => { const el = makeEl(`fast-${d}`); el.dataset.delta = d; return el; }); const allChips = [...rateChips, ...fastChips]; const body = makeEl('body'); const calls = { setRate: [], fastForward: [], setTime: [], setStyle: [], aot: [], toggleClock: 0, togglePause: 0 }; let stateCb = null; const sandbox = { console, requestAnimationFrame: () => 1, document: { body, activeElement: null, getElementById: (id) => (id === 'styleGroup' ? styleGroup : byId(id)), querySelectorAll: () => [], createElementNS: () => makeEl('x') }, window: {} }; sandbox.window = sandbox; sandbox.window.clockApi = { getState: () => Promise.resolve({ baseMs: Date.now(), baseRealMs: Date.now(), rate: 1, lastRate: 1, style: 'digital', clockWindowOpen: false, alwaysOnTop: { clock: true, control: false } }), onState: (cb) => { stateCb = cb; }, setRate: (r) => { calls.setRate.push(r); return Promise.resolve({}); }, fastForward: (d) => { calls.fastForward.push(d); return Promise.resolve({}); }, setTime: (h, m, s) => { calls.setTime.push([h, m, s]); return Promise.resolve({}); }, setStyle: (s) => { calls.setStyle.push(s); return Promise.resolve({}); }, setAlwaysOnTop: (t, v) => { calls.aot.push([t, v]); return Promise.resolve({}); }, toggleClockWindow: () => { calls.toggleClock++; return Promise.resolve({}); }, togglePause: () => { calls.togglePause++; return Promise.resolve({}); }, closeClockWindow: () => Promise.resolve({}), closeControlWindow: () => Promise.resolve({}), getBoundsSync: () => null, setBounds: () => {} }; const ctx = vm.createContext(sandbox); try { vm.runInContext(read('renderer/control.js'), ctx, { filename: 'control.js' }); check('control.js 加载无异常', true); } catch (e) { check('control.js 加载无异常', false, e.message); process.exit(1); } setTimeout(() => { console.log('--- 初始状态 ---'); check('时钟窗口按钮初始为「打开时钟窗口」', byId('toggleClockWindow').textContent === '打开时钟窗口', byId('toggleClockWindow').textContent); check('暂停按钮初始文案为「暂停」', byId('togglePause').textContent === '暂停', byId('togglePause').textContent); check('时钟窗口置顶默认勾选', byId('aotClock').checked === true); check('控制窗口置顶默认不勾选', byId('aotControl').checked === false); console.log('--- 暂停 / 继续按钮 ---'); stateCb({ baseMs: Date.now(), baseRealMs: Date.now(), rate: 0, lastRate: 2, style: 'digital', clockWindowOpen: true, alwaysOnTop: { clock: true, control: false } }); check('暂停后按钮变成「继续(回到 ×2)」', byId('togglePause').textContent === '继续(回到 ×2)', byId('togglePause').textContent); check('暂停时按钮有高亮状态 is-on', byId('togglePause')._classes.has('is-on')); check('暂停时倍率输入框显示 0', byId('rate').value === '0', byId('rate').value); check('时钟窗口按钮反映已打开', byId('toggleClockWindow').textContent === '关闭时钟窗口', byId('toggleClockWindow').textContent); stateCb({ baseMs: Date.now(), baseRealMs: Date.now(), rate: 2, lastRate: 2, style: 'digital', clockWindowOpen: true, alwaysOnTop: { clock: true, control: false } }); check('继续后按钮文案回到「暂停」', byId('togglePause').textContent === '暂停', byId('togglePause').textContent); check('继续后高亮消失', !byId('togglePause')._classes.has('is-on')); check('继续后倍率输入框显示 2', byId('rate').value === '2', byId('rate').value); stateCb({ baseMs: Date.now(), baseRealMs: Date.now(), rate: -1, lastRate: -1, style: 'analog', clockWindowOpen: true, alwaysOnTop: { clock: false, control: true } }); check('负倍率时按钮仍是「暂停」', byId('togglePause').textContent === '暂停', byId('togglePause').textContent); check('样式按钮高亮跟随状态(analog)', segButtons[1]._classes.has('is-active') && !segButtons[0]._classes.has('is-active')); check('置顶勾选框跟随状态', byId('aotClock').checked === false && byId('aotControl').checked === true); console.log('--- 点击行为 ---'); byId('togglePause').fire('click'); check('点暂停按钮调用 togglePause', calls.togglePause === 1); byId('toggleClockWindow').fire('click'); check('点时钟窗口按钮调用 toggleClockWindow', calls.toggleClock === 1); segButtons[0].fire('click'); check('点「纯数字」调用 setStyle(digital)', calls.setStyle.at(-1) === 'digital', String(calls.setStyle.at(-1))); body.fire('click', { target: rateChips.find((c) => c.dataset.rate === '-1') }); check('点「-1 倒转」调用 setRate(-1)', calls.setRate.at(-1) === -1, String(calls.setRate.at(-1))); body.fire('click', { target: rateChips.find((c) => c.dataset.rate === '0.5') }); check('点「0.5」调用 setRate(0.5)', calls.setRate.at(-1) === 0.5, String(calls.setRate.at(-1))); body.fire('click', { target: rateChips.find((c) => c.dataset.rate === '0') }); check('点「0 暂停」调用 setRate(0)', calls.setRate.at(-1) === 0, String(calls.setRate.at(-1))); for (const d of ['5000','60000','300000','600000','1800000']) body.fire('click', { target: fastChips.find((c) => c.dataset.delta === d) }); check('五个快进按钮分别调用 fastForward(5000/60000/300000/600000/1800000)', JSON.stringify(calls.fastForward) === JSON.stringify([5000, 60000, 300000, 600000, 1800000]), JSON.stringify(calls.fastForward)); byId('rate').value = '-3.5'; byId('applyRate').fire('click'); check('手动输入倍率 -3.5 生效', calls.setRate.at(-1) === -3.5, String(calls.setRate.at(-1))); byId('rate').value = 'abc'; byId('applyRate').fire('click'); check('倍率输入非法时回退到当前值,不发送 NaN', calls.setRate.at(-1) === -3.5, String(calls.setRate.at(-1))); byId('hh').value = '25'; byId('mm').value = '70'; byId('ss').value = '-3'; byId('applyTime').fire('click'); check('时间输入越界被夹到 23:59:00', JSON.stringify(calls.setTime.at(-1)) === JSON.stringify([23, 59, 0]), JSON.stringify(calls.setTime.at(-1))); check('夹紧后的值回填到输入框', byId('hh').value === '23' && byId('mm').value === '59' && byId('ss').value === '0'); byId('fillNow').fire('click'); check('「填入当前显示时间」把输入框填成合法时间', /^\d{2}$/.test(byId('hh').value) && /^\d{2}$/.test(byId('mm').value) && /^\d{2}$/.test(byId('ss').value), `${byId('hh').value}:${byId('mm').value}:${byId('ss').value}`); byId('aotClock').checked = false; byId('aotClock').fire('change'); check('取消时钟置顶调用 setAlwaysOnTop(clock,false)', JSON.stringify(calls.aot.at(-1)) === JSON.stringify(['clock', false]), JSON.stringify(calls.aot.at(-1))); byId('aotControl').checked = true; byId('aotControl').fire('change'); check('打开控制窗口置顶调用 setAlwaysOnTop(control,true)', JSON.stringify(calls.aot.at(-1)) === JSON.stringify(['control', true]), JSON.stringify(calls.aot.at(-1))); console.log(''); if (!failures.length) { console.log(`控制窗口逻辑:全部通过 ${passed}/${passed}`); process.exit(0); } console.log(`通过 ${passed},失败 ${failures.length}:${failures.join(';')}`); process.exit(1); }, 30);