diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-01 22:44:19 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | d59397d4146ffa23adbbe6dc99c64ebb22a93b2d (patch) | |
| tree | a06984bf172a6c894c1586d58aad02ec302b6c2d /test/web_harness.mjs | |
| parent | cbcdf01764e2e144a821643c3a8e9a5ad39eda2e (diff) | |
| download | pardes-d59397d4146ffa23adbbe6dc99c64ebb22a93b2d.tar.gz pardes-d59397d4146ffa23adbbe6dc99c64ebb22a93b2d.zip | |
add vanilla DOM web backend
Diffstat (limited to 'test/web_harness.mjs')
| -rw-r--r-- | test/web_harness.mjs | 136 |
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(); |
