import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { runInNewContext } from 'node:vm'; const source = await readFile(new URL('../assets/remote-input/app.js', import.meta.url), 'utf8'); const html = await readFile(new URL('../assets/remote-input/index.html', import.meta.url), 'utf8'); function fakeElement() { const classes = new Set(); return { listeners: {}, style: {}, hidden: false, checked: true, value: '', textContent: '', classList: { add: (...names) => names.forEach((name) => classes.add(name)), remove: (...names) => names.forEach((name) => classes.delete(name)), toggle: (name, enabled) => (enabled ? classes.add(name) : classes.delete(name)), contains: (name) => classes.has(name), }, addEventListener(type, listener) { this.listeners[type] = listener; }, querySelectorAll() { return []; }, focus() {}, select() {}, }; } const fixtureRecoveryKey = '40112233-4455-4677-8899-aabbccddeeff'; async function openRemotePage({ defaultMode, savedMode, savedWakeLock, savedRecovery, wakeLockMode = 'supported', } = {}) { const elements = new Map(); const documentListeners = {}; const sent = []; let socket; let worklet; let audioContext; const windowListeners = {}; const wakeLocks = []; const tracks = []; const wakeResolvers = []; let wakeRequests = 0; const timers = new Map(); let now = 0; let timerId = 0; const flush = async () => { for (let i = 0; i < 12; i += 1) await Promise.resolve(); }; const element = (id) => { if (!elements.has(id)) elements.set(id, fakeElement()); return elements.get(id); }; const storage = (entries = []) => { const values = new Map(entries); return { getItem: (key) => values.get(key) ?? null, setItem: (key, value) => values.set(key, String(value)), removeItem: (key) => values.delete(key), }; }; class FakeWebSocket { constructor() { this.readyState = 1; socket = this; } send(value) { sent.push(value); } close() { this.readyState = 3; } } class FakeAudioWorkletNode { constructor() { this.port = { onmessage: null }; worklet = this; } connect() {} disconnect() {} } class FakeAudioContext { constructor() { audioContext = this; this.state = 'running'; this.sampleRate = 48_000; this.audioWorklet = { addModule: () => Promise.resolve() }; } resume() { this.state = 'running'; return Promise.resolve(); } suspend() { this.state = 'suspended'; } createMediaStreamSource() { return { connect() {}, disconnect() {} }; } } const document = { hidden: false, title: '', body: { appendChild() {}, removeChild() {} }, getElementById: element, querySelectorAll: () => [], createElement: fakeElement, addEventListener(type, listener) { documentListeners[type] = listener; }, removeEventListener() {}, execCommand() {}, }; const context = { ArrayBuffer, Blob, DataView, Error, Math, Promise, Uint8Array, URL: { createObjectURL: () => 'blob:worklet' }, AudioContext: FakeAudioContext, AudioWorkletNode: FakeAudioWorkletNode, WebSocket: FakeWebSocket, clearTimeout: (id) => timers.delete(id), console, document, isNaN, localStorage: storage([ ['ol_remote_pin', '123456'], ...(savedMode === undefined ? [] : [['ol_remote_mode', savedMode]]), ...(savedWakeLock === undefined ? [] : [['ol_remote_wake_lock', savedWakeLock]]), ...(savedRecovery === undefined ? [] : [ [ 'ol_remote_recovery_session', JSON.stringify({ sessionId: savedRecovery, key: fixtureRecoveryKey }), ], ]), ]), location: { host: 'localhost:8443', origin: 'https://localhost:8443', reload() {} }, navigator: { language: 'zh-CN', wakeLock: wakeLockMode === 'unsupported' ? undefined : { request: (type) => { assert.equal(type, 'screen'); wakeRequests++; if (wakeLockMode === 'rejected') return Promise.reject(new Error('system denied')); const sentinel = { released: false, releaseCount: 0, listener: null, addEventListener(type, listener) { assert.equal(type, 'release'); this.listener = listener; }, release() { this.released = true; this.releaseCount++; this.listener?.(); return Promise.resolve(); }, }; wakeLocks.push(sentinel); return wakeLockMode === 'deferred' ? new Promise((resolve) => wakeResolvers.push(() => resolve(sentinel))) : Promise.resolve(sentinel); }, }, mediaDevices: { getUserMedia: () => { const track = { stopped: false, stop() { this.stopped = true; }, }; tracks.push(track); return Promise.resolve({ getTracks: () => [track] }); }, }, }, performance: { now: () => 100 }, sessionStorage: storage([['ol_reloaded_once', '1']]), setTimeout: (callback, delay) => { const id = ++timerId; timers.set(id, { callback, at: now + delay }); return id; }, addEventListener: (type, listener) => { windowListeners[type] = listener; }, }; context.window = context; // Exercise the embedded HTML script too, so a missing template variable cannot // be hidden by setting window properties directly in the test harness. const injectedScript = html .match(/