#!/usr/bin/env node // Dependency-free JS harness: instantiate the real freestanding core in Node, // then render its packed frame through app.mjs against a deliberately tiny DOM // double. Chrome is reserved for the separate browser/e2e snapshot harness. import assert from "node:assert/strict"; import fs from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; if (process.argv.length !== 3) { console.error("usage: node test/web_harness.mjs "); process.exit(2); } async function main() { const webDir = path.resolve(process.argv[2]); const wasmBytes = await fs.readFile(path.join(webDir, "pardes.wasm")); const module = await WebAssembly.compile(wasmBytes); const { DomRenderer, browserHost } = await import(pathToFileURL(path.join(webDir, "app.mjs"))); assert.deepEqual( [...new Set(WebAssembly.Module.imports(module).map((entry) => entry.module))], ["pardes"], "the freestanding core must import nothing but the page's own host", ); // The page's real import object, with every call recorded: the browser host // is only what a page can actually do, and a replay must ask it for nothing. const host = browserHost(); const asked = []; for (const [name, fn] of Object.entries(host.imports.pardes)) { host.imports.pardes[name] = (...args) => { asked.push(name); return fn(...args); }; } const { exports: wasm } = await WebAssembly.instantiate(module, host.imports); host.wasm = wasm; for (const name of [ "memory", "pardes_init", "pardes_frame", "pardes_frame_ptr", "pardes_key", "pardes_mouse", "pardes_pointer_leave", "pardes_pointer_shape", "pardes_gui_tagline_font_percent", "pardes_paste", "pardes_tick", "pardes_should_quit", "pardes_deinit", ]) assert.ok(wasm[name], `missing wasm export ${name}`); assert.equal(wasm.pardes_init(80, 24), 0, `wasm init error ${wasm.pardes_last_error()}`); assert.equal(wasm.pardes_frame(), 0, "a frame exists only once the core has presented one"); wasm.pardes_tick(); assert.equal(wasm.pardes_frame(), 80 * 24); assert.ok(wasm.pardes_frame_ptr() > 0); const document = new FakeDocument(); const root = document.createElement("main"); root.ownerDocument = document; const renderer = new DomRenderer(root, wasm); const draw = () => { wasm.pardes_tick(); return renderer.render(); }; const first = renderer.render(); assert.equal(root.style.cursor, "text"); renderer.wasm = { ...wasm, pardes_pointer_shape: () => 1 }; renderer.render(); assert.equal(root.style.cursor, "pointer", "link hover should display the hand pointer"); renderer.wasm = wasm; renderer.render(); assert.equal(root.style.cursor, "text", "leaving a link should restore the text pointer"); assert.equal(first.cols, 80); assert.equal(first.rows, 24); assert.equal(root.children.length, 24); assert.equal(root.children[0].children.length, 80); assert.ok(first.cells.some((cell) => cell.text !== " "), "dump rendered an empty frame"); assert.ok(root.walk().every((node) => node.tagName !== "CANVAS"), "renderer created a canvas"); const taglineIndex = first.cells.findIndex((cell) => cell.attrs & (1 << 7)); const bodyIndex = first.cells.findIndex((cell) => !(cell.attrs & (1 << 7)) && cell.text !== " "); assert.ok(taglineIndex >= 0, "frame ABI did not mark a tagline font role"); assert.ok(bodyIndex >= 0, "fixture has no body glyph to compare with tagline"); assert.ok(renderer.cells[taglineIndex].classList.names.has("is-tagline"), "DOM tagline class was not applied"); assert.ok(!renderer.cells[bodyIndex].classList.names.has("is-tagline"), "DOM inferred tagline role for body text"); assert.equal( root.style["--pardes-tagline-font-size"], `${wasm.pardes_gui_tagline_font_percent()}%`, "DOM did not use the build-time tagline percentage", ); const firstCell = renderer.cells[0]; const second = draw(); assert.equal(renderer.cells[0], firstCell, "stable frames should reuse DOM nodes"); assert.equal(second.cells.length, 80 * 24); // The embedded dump is test/web-snapshots/touch.dump.zon, a fixed source // fixture (no live project paths). Over-scroll first (the core clamps at row // zero) so the Look below always lands on the fixture's own first rows. for (let tick = 0; tick < 128; tick += 1) wasm.pardes_mouse(3, 0, 5, 3, 0); const listingTop = draw(); const topLines = Array.from({ length: listingTop.rows }, (_, row) => listingTop.cells.slice(row * listingTop.cols, (row + 1) * listingTop.cols).map((cell) => cell.text).join("")); assert.ok( topLines.some((text) => /^\s*1 pub fn choose/.test(text)), "fixture source did not scroll to its first row", ); const link = "/virtual/pane/1/body"; const linkRow = topLines.findIndex((text) => text.includes(link)); assert.ok(linkRow >= 0, "fixture has no Look target"); const linkCol = topLines[linkRow].indexOf(link) + 5; wasm.pardes_mouse(2, 0, linkCol, linkRow, 0); wasm.pardes_mouse(2, 1, linkCol, linkRow, 0); const looked = draw(); const lookedText = looked.cells.map((cell) => cell.text).join(""); assert.ok( lookedText.includes(`${link} `) && lookedText.split("pub fn choose").length - 1 >= 2, "right-click LOOK did not open the reflected source body", ); const sourceRows = Array.from({ length: looked.rows }, (_, row) => looked.cells.slice(row * looked.cols, (row + 1) * looked.cols)); const codeRows = sourceRows.filter((row) => row.map((cell) => cell.text).join("").match(/^\s*\d+\s/)); const codeColors = new Set(codeRows.flatMap((row) => row.slice(5).map((cell) => `${cell.fg}:${cell.attrs & 1}`))); assert.ok(codeColors.size >= 3, "Tree-sitter did not apply distinct syntax styles to the fixture"); assert.deepEqual(asked, [], "a plain replay asked the page for host IO"); wasm.pardes_resize(41, 13); const resized = draw(); assert.equal(resized.cols, 41); assert.equal(resized.rows, 13); assert.equal(root.children.length, 13); assert.equal(root.children[0].children.length, 41); wasm.pardes_deinit(); console.error("web harness ok: freestanding wasm + Tree-sitter + packed DOM renderer"); } class FakeDocument { constructor() { this.body = new FakeElement("body", this); } createElement(tagName) { return new FakeElement(tagName, this); } } class FakeElement { constructor(tagName, ownerDocument) { this.tagName = tagName.toUpperCase(); this.ownerDocument = ownerDocument; this.children = []; this.attributes = new Map(); this.dataset = {}; this.style = new FakeStyle(); this.classList = new FakeClassList(this); this.className = ""; this.textContent = ""; } setAttribute(name, value) { this.attributes.set(name, String(value)); } replaceChildren(...children) { this.children = children; } *walk() { yield this; for (const child of this.children) yield* child.walk(); } } class FakeStyle { setProperty(name, value) { this[name] = value; } } class FakeClassList { constructor(element) { this.element = element; this.names = new Set(); } toggle(name, force) { if (force) this.names.add(name); else this.names.delete(name); this.element.className = [...this.names].join(" "); } } await main();