// 用最小 DOM 桩把 renderer/clock.js + resize.js 真的跑一遍(不需要 Electron、不需要显示器) // 注意:桩终究不是真浏览器,它能证明「接线和算法对」,证明不了「鼠标手感」。 const fs = require('fs'); const path = require('path'); const vm = require('vm'); 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 el = { id, textContent: '', hidden: false, style: {}, dataset: {}, children: [], attrs: {}, handlers: {}, classList: { add() {}, remove() {}, toggle() {}, contains: () => false }, appendChild(c) { this.children.push(c); }, setAttribute(k, v) { this.attrs[k] = v; }, getAttribute(k) { return this.attrs[k]; }, getBoundingClientRect: () => ({ width: 520, height: 280, top: 0, left: 0 }), addEventListener(type, fn) { (this.handlers[type] ||= []).push(fn); }, setPointerCapture() {}, releasePointerCapture() {}, fire(type, ev) { (this.handlers[type] || []).forEach((fn) => fn(ev)); } }; return el; } const els = new Map(); const byId = (id) => { if (!els.has(id)) els.set(id, makeEl(id)); return els.get(id); }; const handles = ['n','s','w','e','nw','ne','sw','se'].map((dir) => { const el = makeEl(`rz-${dir}`); el.dataset.dir = dir; return el; }); const body = makeEl('body'); const calls = { setBounds: [], closed: 0, rates: [] }; let rafCb = null; let stateCb = null; const RealDate = Date; let fakeNow = new RealDate(2026, 8, 14, 3, 0, 0, 0).getTime(); class FakeDate extends RealDate { constructor(...a) { if (a.length === 0) super(fakeNow); else super(...a); } static now() { return fakeNow; } } const sandbox = { console, Date: FakeDate, requestAnimationFrame: (cb) => { rafCb = cb; return 1; }, ResizeObserver: class { observe() {} }, getComputedStyle: () => ({ webkitAppRegion: 'drag' }), addEventListener: () => {}, document: { body, getElementById: byId, querySelectorAll: (sel) => (sel === '.rz' ? handles : []), createElementNS: () => makeEl('svg-node') }, window: {} }; sandbox.window = sandbox; sandbox.window.clockApi = { getState: () => Promise.resolve({ baseMs: fakeNow, baseRealMs: fakeNow, rate: 1, lastRate: 1, style: 'digital' }), onState: (cb) => { stateCb = cb; }, setRate: (r) => { calls.rates.push(r); return Promise.resolve({}); }, setBounds: (b) => calls.setBounds.push(b), getBoundsSync: () => ({ x: 400, y: 300, width: 520, height: 280 }), closeClockWindow: () => { calls.closed++; return Promise.resolve({}); }, togglePause: () => Promise.resolve({}), fastForward: () => Promise.resolve({}), setTime: () => Promise.resolve({}), setStyle: () => Promise.resolve({}), setAlwaysOnTop: () => Promise.resolve({}), toggleClockWindow: () => Promise.resolve({}), closeControlWindow: () => Promise.resolve({}) }; console.log('--- 加载 ---'); let ctx; try { ctx = vm.createContext(sandbox); vm.runInContext(read('renderer/resize.js'), ctx, { filename: 'resize.js' }); vm.runInContext(read('renderer/presented-time.js'), ctx, { filename: 'presented-time.js' }); vm.runInContext(read('renderer/clock.js'), ctx, { filename: 'clock.js' }); check('resize.js + clock.js 在同一全局作用域下加载无异常(无重复声明冲突)', true); } catch (e) { check('resize.js + clock.js 在同一全局作用域下加载无异常', false, e.message); console.log('\n致命:渲染进程脚本无法加载'); process.exit(1); } setTimeout(() => { console.log('--- 走时显示 ---'); check('数字视图渲染 HH:MM:SS', byId('digitalTime').textContent === '03:00:00', byId('digitalTime').textContent); const expectDate = new RealDate(fakeNow); const wk = ['周日','周一','周二','周三','周四','周五','周六'][expectDate.getDay()]; check('日期行正确', byId('digitalDate').textContent === `2026-09-14 ${wk}`, byId('digitalDate').textContent); fakeNow += 3661 * 1000; // 前进 1 小时 1 分 1 秒 rafCb(); check('走 1 小时后数字变成 04:01:01', byId('digitalTime').textContent === '04:01:01', byId('digitalTime').textContent); check('画面读数与模型公式一致', Number(body.dataset.displayMs) === fakeNow, body.dataset.displayMs); console.log('--- 倍率 / 倒转 / 暂停(通过 IPC 下发状态) ---'); fakeNow = new RealDate(2026, 8, 14, 3, 0, 0, 0).getTime(); stateCb({ baseMs: fakeNow, baseRealMs: fakeNow, rate: 60, lastRate: 60, style: 'digital' }); fakeNow += 1000; rafCb(); check('倍率 60:1 秒真实时间后画面走了 60 秒', byId('digitalTime').textContent === '03:01:00', byId('digitalTime').textContent); check('倍率角标显示 ×60', byId('badge').hidden === false && byId('badge').textContent === '×60', byId('badge').textContent); // 每次下发状态都会把「当前显示时间」重新锚成基准,所以期望值要用同一公式算 const revBase = fakeNow; stateCb({ baseMs: revBase, baseRealMs: revBase, rate: -1, lastRate: -1, style: 'digital' }); const revText0 = byId('digitalTime').textContent; fakeNow += 5000; rafCb(); const expectRev = new RealDate(revBase - 5000); const expectRevText = [expectRev.getHours(), expectRev.getMinutes(), expectRev.getSeconds()] .map((n) => String(n).padStart(2, '0')).join(':'); check('倒转:画面时间往回走(5 秒真实时间 = 倒退 5 秒)', byId('digitalTime').textContent === expectRevText && byId('digitalTime').textContent < revText0, `${revText0} → ${byId('digitalTime').textContent}(期望 ${expectRevText})`); check('倍率角标显示「倒转 ×-1」', byId('badge').textContent === '倒转 ×-1', byId('badge').textContent); const pauseBase = fakeNow; stateCb({ baseMs: pauseBase, baseRealMs: pauseBase, rate: 0, lastRate: -1, style: 'digital' }); rafCb(); const frozen = byId('digitalTime').textContent; fakeNow += 9000; rafCb(); check('暂停:9 秒真实时间过去,画面完全不动', byId('digitalTime').textContent === frozen, `${frozen} → ${byId('digitalTime').textContent}`); check('暂停角标显示「暂停」', byId('badge').textContent === '暂停', byId('badge').textContent); stateCb({ baseMs: fakeNow, baseRealMs: fakeNow, rate: 1, lastRate: 1, style: 'digital' }); rafCb(); check('倍率回到 1 时角标隐藏', byId('badge').hidden === true); console.log('--- 样式切换 ---'); stateCb({ baseMs: fakeNow, baseRealMs: fakeNow, rate: 1, lastRate: 1, style: 'analog' }); check('切到表盘:analog 显示、digital 隐藏', byId('analogView').hidden === false && byId('digitalView').hidden === true); fakeNow = new RealDate(2026, 8, 14, 3, 0, 0, 0).getTime(); stateCb({ baseMs: fakeNow, baseRealMs: fakeNow, rate: 1, lastRate: 1, style: 'analog' }); fakeNow += 0; rafCb(); check('03:00:00 时时针指向 90°(3 点方向)', byId('hourHand').attrs.transform === 'rotate(90 0 0)', byId('hourHand').attrs.transform); check('分针指向 0°', byId('minuteHand').attrs.transform === 'rotate(0 0 0)', byId('minuteHand').attrs.transform); check('秒针指向 0°', byId('secondHand').attrs.transform === 'rotate(0 0 0)', byId('secondHand').attrs.transform); fakeNow += 15000; rafCb(); // 15 秒 check('15 秒后秒针指向 90°', byId('secondHand').attrs.transform === 'rotate(90 0 0)', byId('secondHand').attrs.transform); check('秒针按毫秒连续扫(15.5 秒 ≈ 93°)', (() => { fakeNow += 500; rafCb(); return byId('secondHand').attrs.transform === 'rotate(93 0 0)'; })(), byId('secondHand').attrs.transform); stateCb({ baseMs: fakeNow, baseRealMs: fakeNow, rate: 1, lastRate: 1, style: 'digital' }); check('切回数字:digital 显示、analog 隐藏', byId('digitalView').hidden === false && byId('analogView').hidden === true); console.log('--- 拖边缘改大小(模拟真实鼠标事件序列) ---'); const east = handles.find((h) => h.dataset.dir === 'e'); east.fire('pointerdown', { button: 0, screenX: 916, screenY: 440, pointerId: 1, preventDefault() {} }); east.fire('pointermove', { screenX: 996, screenY: 440, pointerId: 1 }); east.fire('pointermove', { screenX: 1036, screenY: 440, pointerId: 1 }); east.fire('pointerup', { pointerId: 1 }); const b1 = calls.setBounds.at(-1); check('拖东边:宽度 520 → 640,左上角不动', b1.width === 640 && b1.x === 400 && b1.y === 300 && b1.height === 280, JSON.stringify(b1)); const south = handles.find((h) => h.dataset.dir === 's'); south.fire('pointerdown', { button: 0, screenX: 660, screenY: 576, pointerId: 1, preventDefault() {} }); south.fire('pointermove', { screenX: 660, screenY: 636, pointerId: 1 }); south.fire('pointerup', { pointerId: 1 }); const b2 = calls.setBounds.at(-1); check('拖南边:高度 280 → 340', b2.height === 340 && b2.y === 300, JSON.stringify(b2)); const west = handles.find((h) => h.dataset.dir === 'w'); west.fire('pointerdown', { button: 0, screenX: 400, screenY: 440, pointerId: 1, preventDefault() {} }); west.fire('pointermove', { screenX: 900, screenY: 440, pointerId: 1 }); west.fire('pointerup', { pointerId: 1 }); const b3 = calls.setBounds.at(-1); check('拖西边超过最小宽度:停在 260,右边界不动(窗口不会被推走)', b3.width === 260 && b3.x + b3.width === 920, JSON.stringify(b3)); const before = calls.setBounds.length; south.fire('pointermove', { screenX: 660, screenY: 700, pointerId: 1 }); check('抬起鼠标后再移动不会继续改大小', calls.setBounds.length === before); console.log('--- 关闭按钮 ---'); byId('closeBtn').fire('click', {}); check('点 × 调用了 closeClockWindow', calls.closed === 1, String(calls.closed)); console.log(''); if (!failures.length) { console.log(`渲染进程逻辑:全部通过 ${passed}/${passed}`); process.exit(0); } console.log(`通过 ${passed},失败 ${failures.length}:${failures.join(';')}`); process.exit(1); }, 30);