fix: the app bar is reserved, not fitted, and a message is a toast (improvise-n6t8)
The rule is the deliverable: nothing a user expects to be fixed may move.
Four children of <header> were sized by their own content and all four
changed on the SAME event, so the bar reflowed on a mode change with no
message at all: #mode (mode_label, 4-14 chars), #hint (hint_line, one
line per mode), #doc-name (gains and loses " [+]") and #view-name, which
had :empty { display: none } and so appeared out of nothing.
Every child that changes with a keystroke now has an allocated width, and
#hint is the single elastic child, so nothing is positioned by it. The
mode pill's reserve is a FLOOR and deliberately not a cap: a cap absorbs
an over-long label inside a box that never grows, which is a reserve that
hides its own failure. AppMode's exhaustive-match methods make the
compiler demand mode_label for a new variant; nothing checked the CSS
still fit it. harness/tests/header.spec.js is that check.
The spec found what the ticket did not predict: the compounding one was
not the pill. #status was flex: 0 1 auto beside a flex: 1 1 auto hint, so
a longer hint took the shrink out of #status, which WRAPPED to two and
three lines and grew the header by a row -- 52.5px to 71.0px. Reserving
the pill alone would have left that untouched.
ViewState::status_msg is now a toast in a position: fixed dock anchored at
its bottom, so it takes part in no layout and stacks ABOVE the minibuffer:
the prompt is neither covered nor displaced. composeStatus is gone -- the
two facts go to two elements, so a repaint reaches onModel and cannot
address onShell at all. That is gotcha 46's rule made structural rather
than kept by a join's convention; its unit test was replaced, not deleted.
Dismissal keys off message IDENTITY, not a timer. status_msg is STATE and
repaint() re-offers it on every revision, so a setTimeout alone resurrects
the toast on the next keypress. Cost stated rather than hidden: two
identical messages in a row show once, because nothing carries a sequence
and the alternative reading of an ambiguous offer is exactly the bug.
Nine of seventeen modes are reachable in a browser and the spec names them
individually; three injected faults are recorded in the evidence file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
+25
-5
@@ -10,6 +10,7 @@ import {
|
||||
cellAttrs,
|
||||
classifyWorkerMessage,
|
||||
createShell,
|
||||
createToasts,
|
||||
documentTitle,
|
||||
extentOf,
|
||||
frameCostOf,
|
||||
@@ -47,6 +48,7 @@ const el = {
|
||||
pageMenu: document.getElementById("page-menu"),
|
||||
mode: document.getElementById("mode"),
|
||||
minibuffer: document.getElementById("minibuffer"),
|
||||
toast: document.getElementById("toast"),
|
||||
hint: document.getElementById("hint"),
|
||||
help: document.getElementById("help"),
|
||||
status: document.getElementById("status"),
|
||||
@@ -87,12 +89,30 @@ el.notice.addEventListener("toggle", () =>
|
||||
await init();
|
||||
const client = new Client();
|
||||
|
||||
// A peer that never answers the handshake, and the two facts `#status` shows.
|
||||
// Both are decided in shell.js; this is the wire to the element.
|
||||
const shell = createShell({
|
||||
onStatus: (text) => {
|
||||
el.status.textContent = text;
|
||||
// The document's own messages, out of flow and on a timer. `status_msg` is
|
||||
// STATE, so a repaint re-offers it: the identity check is in `createToasts`.
|
||||
const toasts = createToasts({
|
||||
onShow: (text) => {
|
||||
el.toast.textContent = text;
|
||||
el.toast.hidden = false;
|
||||
},
|
||||
// The text is deliberately LEFT in place — see index.html.
|
||||
onHide: () => {
|
||||
el.toast.hidden = true;
|
||||
},
|
||||
});
|
||||
|
||||
// A peer that never answers the handshake, and the two facts the page shows.
|
||||
// Two SINKS, never joined: the transport's is `#status` and lasts, the
|
||||
// document's is a toast. Both are decided in shell.js; this is the wire.
|
||||
const shell = createShell({
|
||||
onShell: (text) => {
|
||||
el.status.textContent = text;
|
||||
// The line never wraps, so a long peer address ellipsises. The whole of
|
||||
// it stays reachable rather than being lost to the cap.
|
||||
el.status.title = text;
|
||||
},
|
||||
onModel: (text) => toasts.offer(text),
|
||||
timeoutMs: HANDSHAKE_TIMEOUT_MS,
|
||||
});
|
||||
// The harness asks whether the watchdog is ARMED, because a negative assertion
|
||||
|
||||
+13
-4
@@ -123,11 +123,20 @@
|
||||
</main>
|
||||
|
||||
<!--
|
||||
The bottom bar. The TUI draws the minibuffer INSTEAD of the status line;
|
||||
here it is a line of its own, because #status also carries the transport's
|
||||
own failures and a repaint must not erase one (gotcha 48).
|
||||
The bottom dock (improvise-n6t8). It is `position: fixed`, so nothing
|
||||
inside it takes part in the page's layout and a message cannot push the
|
||||
header or the grid. It is anchored at its BOTTOM and stacks upward, so
|
||||
the minibuffer sits where it always sat and a toast appears above it:
|
||||
the prompt is never covered and never displaced. context/code/clients.md.
|
||||
|
||||
#toast keeps its text when it hides — the element is the record of the
|
||||
last message shown, and an emptied one could not tell "nothing was said"
|
||||
from "something else was said".
|
||||
-->
|
||||
<div id="minibuffer" role="status" hidden></div>
|
||||
<div id="bottom-dock">
|
||||
<div id="toast" role="status" aria-live="polite" hidden></div>
|
||||
<div id="minibuffer" role="status" hidden></div>
|
||||
</div>
|
||||
|
||||
<div id="help" role="dialog" aria-label="Help" hidden></div>
|
||||
|
||||
|
||||
+52
-13
@@ -10,10 +10,44 @@ export function socketUrl(peer, pageProtocol) {
|
||||
return `${secure ? "wss" : "ws"}://${peer}`;
|
||||
}
|
||||
|
||||
/** `#status` carries two independent facts, composed rather than overwritten:
|
||||
* a repaint must not quietly erase a connection error. */
|
||||
export function composeStatus(shell, model) {
|
||||
return [shell, model].filter(Boolean).join(" · ");
|
||||
/** How long a transient message stays on screen. Long enough to read a
|
||||
* sentence, short enough that it is gone before it is furniture. */
|
||||
export const TOAST_MS = 6000;
|
||||
|
||||
/** `ViewState::status_msg` is STATE, not an event: a repaint re-offers the
|
||||
* string that is already on screen, so dismissal keys off message IDENTITY
|
||||
* and never off a timer alone. What that costs: context/code/clients.md. */
|
||||
export function createToasts({
|
||||
onShow,
|
||||
onHide,
|
||||
ttlMs = TOAST_MS,
|
||||
setTimer = setTimeout,
|
||||
clearTimer = clearTimeout,
|
||||
}) {
|
||||
// What was last ACCEPTED, kept after the toast hides. That is the whole
|
||||
// mechanism: a repaint offering it again is recognised and dropped.
|
||||
let offered = "";
|
||||
let timer = null;
|
||||
|
||||
return {
|
||||
offer(text) {
|
||||
const next = text ?? "";
|
||||
if (next === offered) return;
|
||||
offered = next;
|
||||
clearTimer(timer);
|
||||
timer = null;
|
||||
if (next === "") {
|
||||
onHide();
|
||||
return;
|
||||
}
|
||||
onShow(next);
|
||||
timer = setTimer(() => {
|
||||
timer = null;
|
||||
onHide();
|
||||
}, ttlMs);
|
||||
},
|
||||
showing: () => timer !== null,
|
||||
};
|
||||
}
|
||||
|
||||
/** The title bar's one line, in the order and spelling `src/draw.rs::draw_title`
|
||||
@@ -293,22 +327,27 @@ export function isFatal(notice) {
|
||||
}
|
||||
|
||||
/** The page's two status facts and the handshake watchdog, which share a home
|
||||
* because the one rule between them is that a *failure disarms the timer*. */
|
||||
export function createShell({ onStatus, timeoutMs, setTimer = setTimeout, clearTimer = clearTimeout }) {
|
||||
let shell = "";
|
||||
let model = "";
|
||||
* because the one rule between them is that a *failure disarms the timer*.
|
||||
*
|
||||
* The two facts go to two SINKS and are never joined: a repaint reaches
|
||||
* `onModel` and cannot address `onShell` at all. That is the structural form
|
||||
* of the rule the `·` join used to keep by convention — context/code/clients.md. */
|
||||
export function createShell({
|
||||
onShell,
|
||||
onModel,
|
||||
timeoutMs,
|
||||
setTimer = setTimeout,
|
||||
clearTimer = clearTimeout,
|
||||
}) {
|
||||
let watchdog = null;
|
||||
let arms = 0;
|
||||
const paint = () => onStatus(composeStatus(shell, model));
|
||||
|
||||
return {
|
||||
setShell(text) {
|
||||
shell = text;
|
||||
paint();
|
||||
onShell(text);
|
||||
},
|
||||
setModel(text) {
|
||||
model = text;
|
||||
paint();
|
||||
onModel(text);
|
||||
},
|
||||
/** A peer that never answers the handshake. Deliberately about the *first
|
||||
* frame*, not either peer's own failure modes — clients.md. */
|
||||
|
||||
+86
-15
@@ -66,7 +66,14 @@ body {
|
||||
|
||||
/* ── The app bar ─────────────────────────────────────────────────────────
|
||||
One line, and the eye's order across it is: what this is, which document,
|
||||
which view, which mode, what keys, what the peer is doing. */
|
||||
which view, which mode, what keys, what the peer is doing.
|
||||
|
||||
RESERVED, NOT FITTED (improvise-n6t8). Every child that changes with the
|
||||
user's own keystrokes has an allocated width, so a mode change moves
|
||||
nothing after it. #hint is the single elastic child and absorbs whatever is
|
||||
left, which is why nothing is positioned by it. Why a floor rather than a
|
||||
cap on the mode pill, and why the guard is a spec and not the number:
|
||||
context/code/clients.md. */
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
@@ -93,7 +100,7 @@ header h1 {
|
||||
flex: none;
|
||||
/* A tag fits whole; a 40-char commit SHA truncates to a prefix long enough
|
||||
to name a build, and selecting the text still copies the whole thing. */
|
||||
max-width: 14ch;
|
||||
width: 14ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -101,11 +108,13 @@ header h1 {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
/* Wide enough for the shipped document's whole title, `name (stored_as)`, so
|
||||
the ellipsis is for an unusually long one rather than for the demo. A fixed
|
||||
width and not a cap: the dirty marker `[+]` appears and disappears on an
|
||||
edit, and a cap would let it shift the bar. */
|
||||
#doc-name {
|
||||
flex: none;
|
||||
/* Wide enough for the shipped document's whole title, `name (stored_as)`,
|
||||
so the ellipsis is for an unusually long one rather than for the demo. */
|
||||
max-width: 36ch;
|
||||
width: 34ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -116,20 +125,32 @@ header h1 {
|
||||
chip shape the tile bar uses rather than the mode's filled one. */
|
||||
#view-name {
|
||||
flex: none;
|
||||
width: 16ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0.05rem 0.5rem;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* `visibility`, not `display`: the chip is blank before the first frame and
|
||||
must still hold its place, or the bar jumps once on load. */
|
||||
#view-name:empty {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* The mode is the single most consequential thing on the page — every key
|
||||
means something different in each — so it is the one filled badge. */
|
||||
#mode {
|
||||
flex: none;
|
||||
/* A FLOOR and deliberately no cap, sized for sixteen characters where the
|
||||
longest `mode_label` today is fourteen. A cap would absorb an over-long
|
||||
label silently; a floor lets it grow, which is what the header-stability
|
||||
spec measures. context/code/clients.md. */
|
||||
min-width: 21ch;
|
||||
text-align: center;
|
||||
padding: 0.1rem 0.55rem;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent);
|
||||
@@ -140,13 +161,18 @@ header h1 {
|
||||
}
|
||||
|
||||
#mode:empty {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* The hint is reference, not state: one line, ellipsised exactly as the TUI
|
||||
truncates it to the terminal's width. It must never push the bar taller. */
|
||||
#hint {
|
||||
flex: 1 1 auto;
|
||||
/* Basis ZERO, not `auto`: the hint text is the longest thing on the bar and
|
||||
a content basis makes it compete with #status for the shrink, so a longer
|
||||
hint narrowed #status until the transport's line WRAPPED and the bar grew
|
||||
a whole row taller. From zero it absorbs the remainder and contributes
|
||||
nothing to overflow. context/code/clients.md. */
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -155,11 +181,21 @@ header h1 {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Two facts composed in one string (`shell.js::composeStatus`), so the dot
|
||||
marks the whole line rather than either half. */
|
||||
/* The TRANSPORT's fact alone since improvise-n6t8 — what the peer is doing,
|
||||
which is not transient and never auto-hides. The document's own messages
|
||||
are the toast's. It is the last child and right-anchored, so it is the one
|
||||
thing the reserve above does not cover: it changes only when the peer's
|
||||
state changes, and #hint absorbs the difference. */
|
||||
#status {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
/* Never shrinks and never wraps: a second line here is a taller bar, which
|
||||
is the one thing this whole section exists to prevent. The cap is the
|
||||
backstop for an unusually long peer address — app.js puts the whole text
|
||||
on `title`, so an ellipsis loses nothing. */
|
||||
flex: 0 0 auto;
|
||||
max-width: 52ch;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
color: var(--dim);
|
||||
@@ -549,12 +585,49 @@ td.total {
|
||||
`state.view.mode.minibuffer()`, the same value the TUI draws — so this is
|
||||
position, weight and contrast and nothing else. */
|
||||
|
||||
#minibuffer {
|
||||
/* ── The bottom dock ─────────────────────────────────────────────────────
|
||||
Out of flow entirely, so nothing in it can push the header or the grid
|
||||
(improvise-n6t8). Anchored at the BOTTOM and stacking upward, which is what
|
||||
keeps the minibuffer where it was when a toast appears above it. */
|
||||
|
||||
#bottom-dock {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 1.75rem;
|
||||
transform: translateX(-50%);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
max-width: calc(100vw - 2rem);
|
||||
/* It floats OVER the grid, so it must not eat a click meant for a cell. */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* The document's own messages, out of flow and on a timer. `hidden` and not
|
||||
an empty node: the text stays as the record of what was last said. */
|
||||
#toast {
|
||||
max-width: min(46rem, calc(100vw - 2rem));
|
||||
padding: 0.45rem 0.8rem;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
background: var(--raised);
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--fg);
|
||||
font-size: 0.85rem;
|
||||
overflow-wrap: anywhere;
|
||||
animation: toast-in 140ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes toast-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(0.35rem);
|
||||
}
|
||||
}
|
||||
|
||||
#minibuffer {
|
||||
max-width: min(46rem, calc(100vw - 2rem));
|
||||
min-width: min(28rem, calc(100vw - 2rem));
|
||||
padding: 0.6rem 0.9rem;
|
||||
@@ -565,8 +638,6 @@ td.total {
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
color: var(--fg);
|
||||
/* It floats OVER the grid, so it must not eat a click meant for a cell. */
|
||||
pointer-events: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
|
||||
+106
-17
@@ -6,8 +6,8 @@ import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
classifyWorkerMessage,
|
||||
composeStatus,
|
||||
createShell,
|
||||
createToasts,
|
||||
documentTitle,
|
||||
extentOf,
|
||||
frameCostOf,
|
||||
@@ -35,20 +35,109 @@ 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", () => {
|
||||
test("a repaint's model status cannot reach the transport's sink at all", () => {
|
||||
// improvise-n6t8. The old `·` join kept this by convention; two sinks make
|
||||
// it structural. A single sink would show the last write, so the fault this
|
||||
// separates from is `shell` LOSING its text, not merely being appended to.
|
||||
const seen = [];
|
||||
const shell = createShell({ onStatus: (t) => seen.push(t), timeoutMs: 10 });
|
||||
const said = [];
|
||||
const shell = createShell({
|
||||
onShell: (t) => seen.push(t),
|
||||
onModel: (t) => said.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");
|
||||
shell.setModel("");
|
||||
assert.deepEqual(seen, ["cannot reach 127.0.0.1:8787"]);
|
||||
assert.deepEqual(said, ["Filter: Product = Latte", ""]);
|
||||
});
|
||||
|
||||
/// `status_msg` is STATE: `repaint()` re-offers it on every revision, so a
|
||||
/// toast dismissed on a timer alone comes back on the next keypress.
|
||||
test("the same message offered again is a repaint, not a second event", () => {
|
||||
const clock = fakeClock();
|
||||
const shown = [];
|
||||
let visible = false;
|
||||
const toasts = createToasts({
|
||||
onShow: (t) => {
|
||||
shown.push(t);
|
||||
visible = true;
|
||||
},
|
||||
onHide: () => {
|
||||
visible = false;
|
||||
},
|
||||
ttlMs: 6000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
|
||||
toasts.offer("records mode is not available in the browser yet");
|
||||
assert.equal(visible, true);
|
||||
clock.fire();
|
||||
assert.equal(visible, false);
|
||||
|
||||
// The keypress. Nothing cleared `status_msg` in Rust, so the page is
|
||||
// offered the identical string — and must NOT resurrect the toast.
|
||||
toasts.offer("records mode is not available in the browser yet");
|
||||
assert.equal(visible, false);
|
||||
assert.equal(clock.count(), 0, "a repaint must not arm a second timer");
|
||||
assert.deepEqual(shown, ["records mode is not available in the browser yet"]);
|
||||
});
|
||||
|
||||
test("a different message shows, and replaces rather than queues", () => {
|
||||
const clock = fakeClock();
|
||||
const shown = [];
|
||||
const toasts = createToasts({
|
||||
onShow: (t) => shown.push(t),
|
||||
onHide: () => shown.push(null),
|
||||
ttlMs: 6000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
toasts.offer("drill is not available in the browser yet");
|
||||
toasts.offer("drilling into an aggregate needs the host");
|
||||
// One timer, not two: the second message's window starts fresh.
|
||||
assert.equal(clock.count(), 1);
|
||||
assert.deepEqual(shown, [
|
||||
"drill is not available in the browser yet",
|
||||
"drilling into an aggregate needs the host",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a host that clears the status hides the toast without waiting", () => {
|
||||
const clock = fakeClock();
|
||||
let visible = false;
|
||||
const toasts = createToasts({
|
||||
onShow: () => {
|
||||
visible = true;
|
||||
},
|
||||
onHide: () => {
|
||||
visible = false;
|
||||
},
|
||||
ttlMs: 6000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
});
|
||||
toasts.offer("saved");
|
||||
toasts.offer("");
|
||||
assert.equal(visible, false);
|
||||
assert.equal(clock.count(), 0, "a cleared message leaves no timer behind");
|
||||
});
|
||||
|
||||
test("an empty status on the first frame shows nothing", () => {
|
||||
// The common case: every repaint before any command offers "". A feed that
|
||||
// treated the first offer as new would flash an empty toast on load.
|
||||
const shown = [];
|
||||
const toasts = createToasts({
|
||||
onShow: (t) => shown.push(t),
|
||||
onHide: () => shown.push(null),
|
||||
setTimer: () => 1,
|
||||
clearTimer: () => {},
|
||||
});
|
||||
toasts.offer("");
|
||||
toasts.offer(undefined);
|
||||
assert.deepEqual(shown, []);
|
||||
});
|
||||
|
||||
/// A clock the test drives, so the watchdog's arming is asserted rather than
|
||||
@@ -76,7 +165,7 @@ test("the watchdog speaks when the peer never answers the handshake", () => {
|
||||
const clock = fakeClock();
|
||||
const seen = [];
|
||||
const shell = createShell({
|
||||
onStatus: (t) => seen.push(t),
|
||||
onShell: (t) => seen.push(t),
|
||||
timeoutMs: 8000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
@@ -92,7 +181,7 @@ 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),
|
||||
onShell: (t) => seen.push(t),
|
||||
timeoutMs: 8000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
@@ -110,7 +199,7 @@ 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),
|
||||
onShell: (t) => seen.push(t),
|
||||
timeoutMs: 8000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
@@ -125,7 +214,7 @@ test("a frame that arrives is the answer the watchdog was waiting for", () => {
|
||||
test("arming twice leaves one timer, not two", () => {
|
||||
const clock = fakeClock();
|
||||
const shell = createShell({
|
||||
onStatus: () => {},
|
||||
onShell: () => {},
|
||||
timeoutMs: 8000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
@@ -244,7 +333,7 @@ test("a notice DISARMS the watchdog rather than being replaced by it", () => {
|
||||
const clock = fakeClock();
|
||||
const seen = [];
|
||||
const shell = createShell({
|
||||
onStatus: (t) => seen.push(t),
|
||||
onShell: (t) => seen.push(t),
|
||||
timeoutMs: 8000,
|
||||
setTimer: clock.setTimer,
|
||||
clearTimer: clock.clearTimer,
|
||||
|
||||
Reference in New Issue
Block a user