// 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); }, getAttribute() { return null; }, setAttribute() {}, focus() {}, }; return el; } // 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 sandbox = { document: { getElementById: byId, createElement: t => makeEl(t), querySelectorAll: () => [], addEventListener() {}, body: makeEl("body"), hidden: false, }, window: { addEventListener() {} }, 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, 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("