summaryrefslogtreecommitdiff
path: root/test/web_harness.mjs
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-24 10:56:05 -0300
committerGabriel Schneider <[email protected]>2026-08-25 09:42:07 -0300
commitc3d0b84b7961ae26d2d654e7120821cc2d83d20d (patch)
tree0b9a060a8ff0ee7d83c8df5ca797a99e0d66c53e /test/web_harness.mjs
parent70bde600793ea70bd68832018a154671c6bf1512 (diff)
downloadpardes-c3d0b84b7961ae26d2d654e7120821cc2d83d20d.tar.gz
pardes-c3d0b84b7961ae26d2d654e7120821cc2d83d20d.zip
host: the core owns the event loop; every platform becomes a vtable of optional methods
Diffstat (limited to 'test/web_harness.mjs')
-rw-r--r--test/web_harness.mjs56
1 files changed, 38 insertions, 18 deletions
diff --git a/test/web_harness.mjs b/test/web_harness.mjs
index 087dd91b..7bfc2d60 100644
--- a/test/web_harness.mjs
+++ b/test/web_harness.mjs
@@ -17,25 +17,47 @@ 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, {});
+ const { DomRenderer, animationTicks, 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_gui_tagline_font_percent", "pardes_output",
- "pardes_lsp_response", "pardes_pipe_response", "pardes_file_changed",
- "pardes_effect_next", "pardes_deinit",
+ "pardes_mouse", "pardes_pointer_leave", "pardes_gui_tagline_font_percent",
+ "pardes_paste", "pardes_tick", "pardes_animation_tick", "pardes_animating",
+ "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 { DomRenderer, animationTicks } = 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 draw = () => {
+ wasm.pardes_tick();
+ return renderer.render();
+ };
const first = renderer.render();
assert.equal(first.cols, 80);
@@ -69,7 +91,7 @@ async function main() {
assert.equal(animationTicks(stalled, 60_000), 4, "suspended tab catch-up was not bounded");
const firstCell = renderer.cells[0];
- const second = renderer.render();
+ const second = draw();
assert.equal(renderer.cells[0], firstCell, "stable frames should reuse DOM nodes");
assert.equal(second.cells.length, 80 * 24);
@@ -77,7 +99,7 @@ async function main() {
// over-scroll (the core clamps at row zero) so adding more source files does
// not silently move the hard-coded build.zig click onto some later path.
for (let tick = 0; tick < 128; tick += 1) wasm.pardes_mouse(3, 0, 5, 3, 0);
- const listingTop = renderer.render();
+ const listingTop = draw();
assert.match(
listingTop.cells.map((cell) => cell.text).join(""),
/git ls-files --cached --others --exclude-standard.*build\.zig/,
@@ -85,23 +107,21 @@ async function main() {
);
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 looked = draw();
+ assert.match(
+ looked.cells.map((cell) => cell.text).join(""),
+ /build\.zig\s+Save New Newtty 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");
+ assert.deepEqual(asked, [], "a plain replay asked the page for host IO");
wasm.pardes_resize(41, 13);
- const resized = renderer.render();
+ const resized = draw();
assert.equal(resized.cols, 41);
assert.equal(resized.rows, 13);
assert.equal(root.children.length, 13);