36ade35240
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>
65 lines
2.1 KiB
JavaScript
65 lines
2.1 KiB
JavaScript
// The worker half of the standalone deployment (improvise-9x6): strings only,
|
|
// every decision in Rust. The two rules it must not break: context/code/clients.md.
|
|
|
|
import init, { start } from "./worker-pkg/improvise_worker_server.js";
|
|
import { serialize } from "./serial.js";
|
|
|
|
const params = new URLSearchParams(self.location.search);
|
|
const DOC = params.get("doc") || "demo.improv";
|
|
// Where the first copy of the document comes from on a fresh origin: fetched
|
|
// and WRITTEN through the VFS, never injected past storage.
|
|
const SEED = params.get("seed") || "demo.improv";
|
|
|
|
// Fetch the seed if there is one to fetch. A missing seed is not an error:
|
|
// with none, the worker creates an empty model under DOC instead.
|
|
async function seedBytes() {
|
|
try {
|
|
const res = await fetch(SEED);
|
|
if (!res.ok) return undefined;
|
|
return new Uint8Array(await res.arrayBuffer());
|
|
} catch (_) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
let server = null;
|
|
|
|
function post(replies) {
|
|
for (const reply of replies) self.postMessage(reply);
|
|
}
|
|
|
|
async function handle(data) {
|
|
const replies = server.on_message(data);
|
|
post(replies);
|
|
await server.flush();
|
|
}
|
|
|
|
// Startup, and the head of the chain: frames arrive before this settles, and
|
|
// chaining onto it holds them in arrival order with no queue to drain.
|
|
const started = (async () => {
|
|
await init();
|
|
server = await start(DOC, await seedBytes());
|
|
})();
|
|
started.catch((err) => {
|
|
console.error("improvise worker: failed to start", err);
|
|
});
|
|
|
|
// One frame at a time, in arrival order, and the listener is deliberately NOT
|
|
// `async`: the chaining is `serial.js`'s and is tested there.
|
|
const onFrame = serialize(
|
|
started,
|
|
async (data) => {
|
|
// `start()` failed. There is nothing to answer with, and the main
|
|
// thread's handshake watchdog is what tells the user.
|
|
if (!server) return;
|
|
await handle(data);
|
|
},
|
|
// An unhandled rejection here reaches the devtools console and nothing
|
|
// else. Caught, so one bad frame does not poison every later one.
|
|
(err, data) => console.error("improvise worker: frame rejected", data, err),
|
|
);
|
|
|
|
self.onmessage = (event) => {
|
|
onFrame(event.data);
|
|
};
|