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
178 lines
6.1 KiB
JavaScript
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 [+]");
|
|
});
|