summaryrefslogtreecommitdiff
path: root/test/web_harness.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'test/web_harness.mjs')
-rw-r--r--test/web_harness.mjs136
1 files changed, 136 insertions, 0 deletions
diff --git a/test/web_harness.mjs b/test/web_harness.mjs
new file mode 100644
index 00000000..bcd45746
--- /dev/null
+++ b/test/web_harness.mjs
@@ -0,0 +1,136 @@
+#!/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 <zig-out/web>");
+ 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 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();