Files
improvise/web/tests/shell.test.js
T
Edward Langley 628f43121d feat(web): the title bar is painted, a click survives an editor, and the twin door is gone
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
2026-09-20 20:37:44 -07:00

178 lines
6.1 KiB
JavaScript

// What the page decides between the boundaries: transport selection, the two
// status facts, the watchdog and the extent arithmetic. No DOM is involved.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
composeStatus,
createShell,
documentTitle,
extentOf,
frameCostOf,
indicator,
socketUrl,
} from "../shell.js";
test("a bare peer takes the page's own scheme, and https never falls back", () => {
assert.equal(socketUrl("127.0.0.1:8787", "http:"), "ws://127.0.0.1:8787");
assert.equal(socketUrl("example.test", "https:"), "wss://example.test");
// An explicit `ws://` on an https page is UPGRADED, not honoured: the
// browser would refuse the mixed-content connection either way.
assert.equal(socketUrl("ws://example.test", "https:"), "wss://example.test");
assert.equal(socketUrl("ws://example.test", "http:"), "ws://example.test");
// `wss://` is honoured on both, and the match is case-insensitive.
assert.equal(socketUrl("WSS://example.test", "http:"), "WSS://example.test");
});
test("a peer whose host merely starts with ws is not a scheme", () => {
assert.equal(socketUrl("wsserver.test:9000", "http:"), "ws://wsserver.test:9000");
});
test("the two status facts compose, and neither can erase the other", () => {
assert.equal(composeStatus("connected", "Filter: Product = Latte"), "connected · Filter: Product = Latte");
// An empty fact contributes nothing rather than a stray separator.
assert.equal(composeStatus("connected", ""), "connected");
assert.equal(composeStatus("", "saved"), "saved");
assert.equal(composeStatus("", ""), "");
});
test("a repaint's model status does not overwrite a connection failure", () => {
const seen = [];
const shell = createShell({ onStatus: (t) => seen.push(t), timeoutMs: 10 });
shell.fail("cannot reach 127.0.0.1:8787");
shell.setModel("Filter: Product = Latte");
assert.equal(seen.at(-1), "cannot reach 127.0.0.1:8787 · Filter: Product = Latte");
});
/// A clock the test drives, so the watchdog's arming is asserted rather than
/// waited for. `fire()` runs what is pending, as the event loop would.
function fakeClock() {
let next = 1;
const pending = new Map();
return {
setTimer: (fn) => {
const id = next++;
pending.set(id, fn);
return id;
},
clearTimer: (id) => pending.delete(id),
fire: () => {
const due = [...pending.values()];
pending.clear();
for (const fn of due) fn();
},
count: () => pending.size,
};
}
test("the watchdog speaks when the peer never answers the handshake", () => {
const clock = fakeClock();
const seen = [];
const shell = createShell({
onStatus: (t) => seen.push(t),
timeoutMs: 8000,
setTimer: clock.setTimer,
clearTimer: clock.clearTimer,
});
shell.setShell("connecting…");
shell.arm("sent no document");
assert.equal(shell.armed(), true);
clock.fire();
assert.equal(seen.at(-1), "sent no document");
});
test("a loud failure DISARMS the watchdog rather than being replaced by it", () => {
const clock = fakeClock();
const seen = [];
const shell = createShell({
onStatus: (t) => seen.push(t),
timeoutMs: 8000,
setTimer: clock.setTimer,
clearTimer: clock.clearTimer,
});
shell.arm("sent no document");
shell.fail("disconnected");
assert.equal(shell.armed(), false);
assert.equal(clock.count(), 0);
clock.fire();
// The precise message is what stands, eight seconds later and forever.
assert.equal(seen.at(-1), "disconnected");
});
test("a frame that arrives is the answer the watchdog was waiting for", () => {
const clock = fakeClock();
const seen = [];
const shell = createShell({
onStatus: (t) => seen.push(t),
timeoutMs: 8000,
setTimer: clock.setTimer,
clearTimer: clock.clearTimer,
});
shell.setShell("connected");
shell.arm("sent no document");
shell.sawFrame();
clock.fire();
assert.equal(seen.at(-1), "connected");
});
test("arming twice leaves one timer, not two", () => {
const clock = fakeClock();
const shell = createShell({
onStatus: () => {},
timeoutMs: 8000,
setTimer: clock.setTimer,
clearTimer: clock.clearTimer,
});
shell.arm("first");
shell.arm("second");
assert.equal(clock.count(), 1);
});
test("the frame is charged for before the extent is announced", () => {
// 10 rows and 4 columns fit; one header level and one row-label column are
// not data, so the peer is asked for 9 by 3.
const box = { height: 10 * 26, width: 4 * 110 };
assert.deepEqual(extentOf(box, { rows: 1, cols: 1 }), { rows: 9, cols: 3 });
});
test("a window smaller than its own frame still asks for one cell", () => {
assert.deepEqual(extentOf({ height: 0, width: 0 }, { rows: 3, cols: 2 }), {
rows: 1,
cols: 1,
});
});
test("the frame costs a line per column level, plus one for a group line", () => {
const model = (levels, groups, rowLevels) => ({
col_levels: levels,
col_groups: groups,
row_levels: rowLevels,
});
assert.deepEqual(frameCostOf(model([["a"]], [], 1)), { rows: 1, cols: 1 });
// Two category levels on the column axis, and a group line above them.
assert.deepEqual(frameCostOf(model([["a"], ["b"]], [{ span: 1 }], 2)), {
rows: 3,
cols: 2,
});
});
test("the collapse glyph points right when collapsed", () => {
assert.equal(indicator(true), "▶");
assert.equal(indicator(false), "▼");
});
test("the title bar spells the document the way the TUI's own title does", () => {
// `src/draw.rs::draw_title`: name, then what it is stored as in parens, then
// the dirty marker. The harness cannot pin `[+]` — the standalone deployment
// persists on every accepted batch, so the marker is transient there.
assert.equal(
documentTitle("Corner Coffee", true, "demo.improv"),
"Corner Coffee (demo.improv) [+]",
);
assert.equal(documentTitle("Corner Coffee", false, "demo.improv"), "Corner Coffee (demo.improv)");
// A host with nowhere to write it: `stored_as` is `None` over the wire, which
// arrives as `null`, and an empty pair of parens would be a lie about storage.
assert.equal(documentTitle("Untitled", true, null), "Untitled [+]");
});