Files
improvise/web/tests/render_surface.test.js
T
Edward Langley 36ade35240 test(web): a Node suite between the boundaries, Playwright only on them
E2E and integration tests are rare and pin boundary contracts; unit tests
assume those contracts and test between them (user, 2026-09-20). The browser
half has four boundaries — RenderModel→DOM, JS→wasm, page→peer, worker→OPFS —
and everything between them needed no browser at all.

`web/shell.js` and `web/serial.js` are the seams: pure modules that RETURN A
DESCRIPTION rather than take a DOM double. `gridPlan(model)` answers every
question `gridTable` used to answer while creating elements — the data-row
index across group headers, what a group header spans, a null group label
against an empty one, an empty edit buffer against no editor — and `app.js` is
now a loop that builds elements from it. Painting stays untested by Node; what
to paint stopped being untestable.

26 tests in `node --test web/tests` (0.12s, no Rust toolchain, no wasm bundle,
no npm install), plus three in `web/tests/bundle/` that read the generated
.d.ts and so run in the browser job. Playwright goes from 29 tests/36.9s to
16/15.5s on one machine: `widgets.spec.js` and `pointer.spec.js` are gone,
replaced by `contract.spec.js`, which asserts that EVERY field the render model
projects reaches the screen. The four widgets those specs were written for were
one failure at one boundary, and one assertion catches the fifth.

It caught it: `doc_name`/`doc_dirty`/`doc_stored_as` reach no element
(improvise-5jna, already open). They are named in `UNPAINTED` with the ticket,
so the list is the debt written down rather than an exemption.

Thirteen faults injected into the Node suite and eight into the served page,
each watched to fail the test it names; the last of those was strengthened
because the first version passed. `web/assemble.sh` grew an import-completeness
guard whose own first version was blind to the multi-line import it exists for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:33:49 -07:00

53 lines
2.4 KiB
JavaScript

// The RenderModel→DOM boundary, statically: a projected field that nothing
// paints is the bug four widgets shared. See context/code/clients.md.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const read = (rel) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
/// Fields the page deliberately does not read, each with the reason. A field
/// belongs here only with an argument; the list is the debt, written down.
// improvise-z754 put the title bar's facts in one shared cache and taught
// `src/draw.rs::draw_title` to fold it; this page never learned to. That is
// improvise-5jna, open, and this list is what stops it being forgotten.
const UNPAINTED = {
doc_name: "improvise-5jna: the title bar is drawn by the TUI and not here",
doc_dirty: "improvise-5jna",
doc_stored_as: "improvise-5jna",
};
/// The `pub` fields of `RenderModel`, read off the Rust that defines it.
function renderModelFields() {
const src = read("../../crates/improvise-wasm-client/src/render.rs");
const start = src.indexOf("pub struct RenderModel {");
assert.ok(start > 0, "RenderModel moved — this test reads the struct itself");
const body = src.slice(start, src.indexOf("\n}", start));
const fields = [...body.matchAll(/\n {4}pub ([a-z_0-9]+):/g)].map((m) => m[1]);
assert.ok(fields.length > 10, `found only ${fields.length} fields`);
return fields;
}
test("every field the render model projects is painted, or named as debt", () => {
// Both halves of the page: `app.js` paints, `shell.js` plans the grid.
const page = read("../app.js") + read("../shell.js");
const unpainted = renderModelFields().filter((field) => !page.includes(`model.${field}`));
assert.deepEqual(
unpainted.sort(),
Object.keys(UNPAINTED).sort(),
"a projected field reaches no element — paint it, or add it to UNPAINTED with the reason",
);
});
test("every element the page looks up exists in the page", () => {
const app = read("../app.js");
const html = read("../index.html");
const ids = [...app.matchAll(/getElementById\("([^"]+)"\)/g)].map((m) => m[1]);
assert.ok(ids.length >= 8, `found only ${ids.length} element lookups`);
for (const id of ids) {
assert.ok(html.includes(`id="${id}"`), `app.js looks up #${id}, which index.html has not got`);
}
});