#!/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); assert.deepEqual(WebAssembly.Module.imports(module), [], "the freestanding core must have no JS or libc imports"); const { exports: wasm } = await WebAssembly.instantiate(module, {}); for (const name of [ "memory", "pardes_init", "pardes_frame", "pardes_frame_ptr", "pardes_key", "pardes_mouse", "pardes_output", "pardes_lsp_response", "pardes_file_changed", "pardes_effect_next", "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(), 80 * 24); assert.ok(wasm.pardes_frame_ptr() > 0); const { DomRenderer } = await import(pathToFileURL(path.join(webDir, "app.mjs"))); const document = new FakeDocument(); const root = document.createElement("main"); root.ownerDocument = document; const renderer = new DomRenderer(root, wasm); const first = renderer.render(); 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 firstCell = renderer.cells[0]; const second = renderer.render(); assert.equal(renderer.cells[0], firstCell, "stable frames should reuse DOM nodes"); assert.equal(second.cells.length, 80 * 24); wasm.pardes_mouse(2, 0, 5, 3, 0); wasm.pardes_mouse(2, 1, 5, 3, 0); const looked = renderer.render(); assert.match(looked.cells.map((cell) => cell.text).join(""), /build\.zig Save New Del/, "right-click LOOK did not open the embedded source"); 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 build.zig"); let sawWatch = false; for (let index = 0; index < 32; index += 1) { const kind = wasm.pardes_effect_next(); if (!kind) break; if (kind === 9) sawWatch = true; } assert.ok(sawWatch, "JS effect drain did not receive the opened file watch request"); wasm.pardes_resize(41, 13); const resized = renderer.render(); 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();