628f43121d
improvise-5jna, improvise-0x9u, improvise-seik — three browser-page defects with one shape: a thing that looks done from every check except running it. improvise-5jna. `doc_name`/`doc_dirty`/`doc_stored_as` were projected by z754 and painted by nothing. `render_surface.test.js` could not see it — it is a SUBSTRING SEARCH over the page's source, so a field named by a function nothing calls reads as painted, which is also how `model.minibuffer` shipped dead. So the guard is a harness assertion that loads the page and reads `#doc-name`, proved by writing it first (`element(s) not found`). The joining is `shell.js::documentTitle`, the TUI's own `name (stored_as) [+]` minus the chrome; the `[+]` is a node test, not a harness one, because this deployment persists on every accepted batch and clears the flag in the same round trip, so the marker is transient. UNPAINTED is now empty. improvise-0x9u. From an EDITING state a cell click did nothing at all: the mousedown moved focus, the input's blur ran `close`, `close` repainted, `replaceChildren` destroyed the `<td>` the mousedown landed on, and no `click` was ever dispatched. The old spec pressed Escape between the two clicks, so it only ever clicked from a non-editing page — this repo's characteristic defect. Worth recording: moving to `mousedown` is NECESSARY AND NOT SUFFICIENT, and the first attempt failed the same assertion with the same symptom. Removing a focused `<input>` fires `blur` SYNCHRONOUSLY, so `close` rebuilt the table from inside itself and detached the `<td>` again. `close` therefore no longer repaints, the `blur` arm checks `closed` first, and the handler `preventDefault()`s so no focus change happens at all — otherwise the default action focuses `#grid` after `input.focus()` and blurs the new editor. `close` restores the DRAWN text, since nothing repaints behind it any more. improvise-seik. `Client::viewport` was a wasm-bindgen door with no caller, kept for a reason `web/app.js` falsified in the same wave. It is one letter from `set_viewport` and fails SILENTLY — identical frame, forgotten window, every cursor key an upstream black hole. Deleted, along with the exported `init()` free function that duplicated `new Client()`. The guard is not the deletion: `wasm_surface.test.js` now reads the generated `.d.ts` and requires every declared method to be one `app.js` calls or to be named in `UNCALLED` with its reason. Proved by injecting a spare `static viewport(...)` into the built `.d.ts` and watching it fail by name. Tests: 1405 Rust (unchanged), 27 node + 4 bundle (was 26 + 3), 24 chromium harness (was 23). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
56 lines
2.5 KiB
JavaScript
56 lines
2.5 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");
|
|
|
|
/// Comments name model fields routinely, so a field mentioned only in one
|
|
/// would read as painted. Strip them before asking what the code touches.
|
|
const code = (rel) =>
|
|
read(rel)
|
|
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
.replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
|
|
|
|
/// 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.
|
|
// Empty since improvise-5jna painted the title bar. Note what this check CANNOT
|
|
// see, and why the harness asserts the painted text too: it is a substring
|
|
// search, so a field named by a function nothing calls reads as painted.
|
|
const UNPAINTED = {};
|
|
|
|
/// 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 = code("../app.js") + code("../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`);
|
|
}
|
|
});
|