// 无浏览器依赖的页面逻辑回归;使用 DOM/网络替身,不操作用户界面或真实串口。 const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); async function main() { const elements = new Map(); function element(id) { if (!elements.has(id)) { const classes = new Set(); elements.set(id, { value: '', textContent: '', checked: true, style: {}, dataset: {}, scrollTop: 0, scrollHeight: 1000, clientHeight: 200, selectionStart: 0, selectionEnd: 0, handlers: {}, classList: { toggle(name, active) { if (active) classes.add(name); else classes.delete(name); }, remove(name) { classes.delete(name); }, contains(name) { return classes.has(name); } }, addEventListener(name, callback) { this.handlers[name] = callback; }, focus() {}, setSelectionRange(start, end) { this.selectionStart = start; this.selectionEnd = end; } }); } return elements.get(id); } const windows = {}; const sources = []; const calls = []; const timeouts = new Map(); let nextTimer = 1; let log = '[2026-10-21 16:00:00] localhost login:'; let hangStatusBody = false; let selectedLog = false; let selectedElsewhere = false; let failLog = false; let onLogBody = null; let deferFrame = false; const frames = []; const bytes = text => new TextEncoder().encode(text).buffer; const sandbox = { console, TextDecoder, TextEncoder, AbortController, document: { body: { dataset: { token: 'test-token', port: 'COM-TEST' } }, querySelector: element, querySelectorAll: () => [] }, window: { crypto: { randomUUID: () => 'test-page' }, getSelection: () => ({ isCollapsed: !selectedLog && !selectedElsewhere, rangeCount: 1, getRangeAt: () => ({ intersectsNode: node => selectedLog && node === element('#log') }) }), addEventListener(name, callback) { windows[name] = callback; } }, navigator: { sendBeacon: () => true }, EventSource: class { constructor(url) { this.url = url; this.closed = false; sources.push(this); } close() { this.closed = true; } }, requestAnimationFrame(callback) { if (deferFrame) frames.push(callback); else callback(); }, setInterval() {}, setTimeout(callback) { const id = nextTimer++; timeouts.set(id, callback); return id; }, clearTimeout(id) { timeouts.delete(id); }, async fetch(url, options = {}) { calls.push({ url, options }); if (options.method === 'POST') return { ok: true, json: async () => ({ status: 'written' }) }; if (url.startsWith('/api/status')) { return { ok: true, arrayBuffer: () => hangStatusBody ? new Promise((resolve, reject) => options.signal.addEventListener('abort', () => reject(new Error('aborted')))) : Promise.resolve(bytes(JSON.stringify({ isOpen: true, port: 'COM-TEST', receivedBytes: 10 }))) }; } if (failLog) throw new Error('test log failure'); return { ok: true, arrayBuffer: async () => { if (onLogBody) onLogBody(); return bytes(log); } }; } }; const context = vm.createContext(sandbox); const html = fs.readFileSync(path.join(__dirname, '../assets/serial-console.html'), 'utf8'); const source = html.match(/