// tests/test_page.js -- behaviour tests for the controller page's script. // // node tests/test_page.js // // The page is the primary interface, and it lives inside a C++ raw string where no // compiler can see it: a typo there does not fail a build, it fails silently in // somebody's browser while a target is live. So the script is extracted from // src/web_page.h and run against a minimal DOM shim -- enough to exercise the rules // that matter, which are what the log is allowed to contain, which characters may // leave the tab, and when a run of pasted text stops. // // It is a behaviour test, not a rendering test: no CSS, no layout, no real keyboard. // tests/run_tests.sh skips it when node is not installed. "use strict"; const fs = require("fs"); const path = require("path"); const vm = require("vm"); // --------------------------------------------------------------------------- // A DOM small enough to read, complete enough to run the page. // --------------------------------------------------------------------------- function makeEl(tagName) { const el = { tagName: (tagName || "div").toUpperCase(), dataset: {}, className: "", textContent: "", innerHTML: "", value: "", checked: false, disabled: false, readOnly: false, children: [], listeners: {}, scrollTop: 0, scrollHeight: 0, classList: { toggle() {}, add() {}, remove() {}, contains() { return false; } }, get firstChild() { return this.children[0] || null; }, get firstElementChild() { return this.children.find(c => c.tagName) || null; }, get lastElementChild() { const e = this.children.filter(c => c.tagName); return e.length ? e[e.length - 1] : null; }, get childElementCount() { return this.children.filter(c => c.tagName).length; }, appendChild(c) { this.children.push(c); return c; }, removeChild(c) { this.children = this.children.filter(x => x !== c); return c; }, addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); }, // Element handlers are reachable too: the mouse pad is clicked, and a real click is // what the browser turns into a Pointer Lock request. fire(t, ev) { for (const fn of (this.listeners[t] || [])) fn(ev || { preventDefault() {} }); }, getAttribute() { return null; }, setAttribute() {}, focus() {}, }; return el; } // Document- and window-level listeners are recorded rather than dropped. Every // fail-safe this page has is triggered by something the browser does to the document -- // blur, visibilitychange, pointerlockchange -- so a shim that swallows them cannot // prove them. function makeListeners() { const map = {}; return { map, add(t, fn) { (map[t] = map[t] || []).push(fn); }, fire(t, ev) { for (const fn of (map[t] || [])) fn(ev || { preventDefault() {}, target: null }); }, }; } const docListeners = makeListeners(); const winListeners = makeListeners(); // The shim hands out an element for any id, which is far less work than a real DOM. // The weakness in that is obvious, so it is compensated for: every id the script asks // for is recorded and checked against the ids that are actually in the markup at the // end. A mistyped id is a null dereference in a browser, and silence here. const registry = new Map(); const requestedIds = new Set(); const byId = id => { requestedIds.add(id); if (!registry.has(id)) registry.set(id, makeEl("div")); return registry.get(id); }; const sentFrames = []; const idleStatus = () => x.onFrame('S{"armed":1,"usb":1,"typing":0}'); const busyStatus = n => x.onFrame(`S{"armed":1,"usb":1,"typing":${n}}`); class FakeWebSocket { constructor() { this.readyState = 1; // OPEN FakeWebSocket.instances.push(this); setTimeout(() => this.onopen && this.onopen(), 0); } send(text) { sentFrames.push(text); } close() { this.readyState = 3; } } FakeWebSocket.OPEN = 1; FakeWebSocket.instances = []; const doc = { getElementById: byId, createElement: t => makeEl(t), querySelectorAll: () => [], addEventListener: docListeners.add, body: makeEl("body"), hidden: false, // Pointer Lock, as much of it as the page touches. exitPointerLock fires the change // event because a real browser does, which is what makes the release path testable. pointerLockElement: null, exitPointerLock() { if (!doc.pointerLockElement) return; doc.pointerLockElement = null; docListeners.fire("pointerlockchange", {}); }, }; const sandbox = { document: doc, window: { addEventListener: winListeners.add }, location: { protocol: "http:", host: "keyrelay.local" }, sessionStorage: { getItem: () => null, setItem() {} }, WebSocket: FakeWebSocket, setInterval: () => 0, // the reconnect/keepalive loop stays shut in a test setTimeout, clearTimeout, Date, JSON, Math, Promise, TextEncoder, console, Number, String, Object, Array, Buffer, parseInt, }; // The page keeps its state in script-scope `let`s, so the harness reaches into that // scope once, here, instead of duplicating the page's logic in the test. const expose = ` ;globalThis.__x = { log, logEl, untypeableChars, hexify, describePress, inPageField, pasteText, sendPaste, refreshSendBox, setArmed, abortTyping, onFrame, connect, TEXT_FRAME_MAX, TEXT_JOB_MAX, TEXT_PACE_MS, takeMouse, dropMouse, flushMouse, mouseFrame, paintMouse, refreshMouseUi, MOUSE_DELTA_MAX, MOUSE_FRAME_MS, MOUSE_MAX_FRAMES, MOUSE_WHEEL_PX, get mCaptured() { return mCaptured; }, set mCaptured(v) { mCaptured = v; }, get mButtons() { return mButtons; }, get pendX() { return pendX; }, get pendY() { return pendY; }, get ws() { return ws; }, get armed() { return armed; }, set armed(v) { armed = v; }, get authed() { return authed; }, set authed(v) { authed = v; }, get typing() { return typing; }, };`; const headerPath = path.join(__dirname, "..", "src", "web_page.h"); const headerText = fs.readFileSync(headerPath, "utf8"); const from = headerText.indexOf("