// 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, 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), "▼"); });