Merge remote-tracking branch 'gh/main' into fix/web-panel-clicks
This commit is contained in:
+99
-11
@@ -1302,9 +1302,50 @@ it about", and the browser half has four:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `RenderModel` → DOM | every field the render model projects reaches an element | `web/tests/render_surface.test.js` (statically) **and** `harness/tests/contract.spec.js` (on the screen) |
|
| `RenderModel` → DOM | every field the render model projects reaches an element | `web/tests/render_surface.test.js` (statically) **and** `harness/tests/contract.spec.js` (on the screen) |
|
||||||
| JS → wasm | the calls the page makes exist, with the arity the bundle declares, and their answers are painted | `web/tests/bundle/wasm_surface.test.js` (the surface) **and** `contract.spec.js`'s pointer test (the wiring) |
|
| JS → wasm | the calls the page makes exist, with the arity the bundle declares, and their answers are painted | `web/tests/bundle/wasm_surface.test.js` (the surface) **and** `contract.spec.js`'s pointer test (the wiring) |
|
||||||
| page → peer | the handshake goes out first, a frame is answered whole, and a failure is named | `harness/tests/shell.spec.js`, `harness/tests/drill.spec.js` |
|
| page → peer | the handshake goes out first, a frame is answered whole, a failure is named, **and the page routes each failure through the call that disarms the watchdog** | `harness/tests/shell.spec.js`, `harness/tests/drill.spec.js` |
|
||||||
| worker → OPFS | `storage_contract`, run against a real `OpfsFs` | `harness/tests/opfs.spec.js` |
|
| worker → OPFS | `storage_contract`, run against a real `OpfsFs` | `harness/tests/opfs.spec.js` |
|
||||||
|
|
||||||
|
**A unit test of a rule does not test the wiring that reaches it**, and the
|
||||||
|
sharpest case in this file is the watchdog. `shell.test.js` proves that
|
||||||
|
`createShell.fail()` disarms rather than pre-empts; it cannot prove that
|
||||||
|
`app.js`'s socket `close` and `error` listeners call `fail()` rather than
|
||||||
|
`setShell()`. That routing is PR #21's other half, the one gotcha 48 says keeps
|
||||||
|
getting lost — and PR #61 lost it a third time by keeping the unit test and
|
||||||
|
dropping the browser assertion, with 26/26 and 13/13 green. Restored, and the
|
||||||
|
rule generalises: **when a unit test replaces a boundary test, ask which of the
|
||||||
|
two the regression would have been in.** Here it is the routing, so the guard
|
||||||
|
stays in Playwright.
|
||||||
|
|
||||||
|
**The faked clock is faithful outside the window and silent inside it.**
|
||||||
|
`shell.spec.js` walks past the 8s handshake watchdog with `page.clock`, and the
|
||||||
|
question that owes an answer is whether collapsing ten seconds into one
|
||||||
|
synchronous step *flattens* an ordering rather than merely skipping the wait.
|
||||||
|
Probed for the sharpest case — a peer that opens, stays silent past the window
|
||||||
|
and is closed afterwards — the `#status` transcript is **byte-identical** under
|
||||||
|
the fake clock and under a real nine-second wait: four states, same order, same
|
||||||
|
text. What that does not cover is an event landing **strictly inside** the
|
||||||
|
collapsed window, which a synchronous `runFor` gives no opportunity to deliver;
|
||||||
|
that is a race whose outcome the page does not promise, so there is nothing to
|
||||||
|
assert. State it rather than re-derive it.
|
||||||
|
|
||||||
|
**A negative assertion must demonstrate the denied event is reachable on that
|
||||||
|
exact code path** — not on a sibling path, and not in the rule's own unit test.
|
||||||
|
This file's worked example cost four review rounds. `shell.arm` runs only after
|
||||||
|
`await transport.ready`, so on a socket that never opens the watchdog is never
|
||||||
|
armed; a test denying "sent no document" there denies nothing, and passed
|
||||||
|
against both the routing revert and the disarm regression. `outlastTheWatchdog`
|
||||||
|
now asks the page `timesArmed() > 0` first, which turns that review finding into
|
||||||
|
a failure by name. Note *ever armed* and not *armed now*: a correct page has
|
||||||
|
already disarmed by the time the disconnect test jumps — that is the property —
|
||||||
|
so the obvious spelling would have denied the passing case.
|
||||||
|
|
||||||
|
**And a constant a test must exceed has one home.** `HANDSHAKE_TIMEOUT_MS` lived
|
||||||
|
in `web/app.js` and again in the spec, under a comment saying a silent
|
||||||
|
disagreement "would pass for the wrong reason". It did: the spec at 1000 against
|
||||||
|
the page's 8000 made the suite green over the reverted routing, because the jump
|
||||||
|
landed inside the window. It is exported from `web/shell.js` and imported by
|
||||||
|
both, so the disagreement is unconstructible rather than discouraged.
|
||||||
|
|
||||||
Everything **between** those seams is `node --test web/tests`, and it may assume
|
Everything **between** those seams is `node --test web/tests`, and it may assume
|
||||||
the boundary holds rather than re-establishing it: transport selection from
|
the boundary holds rather than re-establishing it: transport selection from
|
||||||
`?ws=`, the two facts `#status` composes, the watchdog's arm and disarm, the
|
`?ws=`, the two facts `#status` composes, the watchdog's arm and disarm, the
|
||||||
@@ -1349,18 +1390,65 @@ the repaint is the boundary; the other seven were the same instance again.
|
|||||||
|
|
||||||
### Two numbers, and neither is the other
|
### Two numbers, and neither is the other
|
||||||
|
|
||||||
Measured on one machine at `1af51c9` versus this branch, both on a private
|
Measured on one machine, in one session, on a private
|
||||||
`IMPROVISE_HARNESS_PORT`:
|
`IMPROVISE_HARNESS_PORT`, with **both trees clean** — `1af51c9` reached by a
|
||||||
|
full checkout rather than by restoring paths over a working branch (see the
|
||||||
|
trap below). Two runs each; the pair is the variance.
|
||||||
|
|
||||||
| suite | tests | execution | whole job |
|
| suite | chromium | all projects | execution | whole job |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| Playwright, before | 29 (+3 skipped) | 36.9s | ~4m30s in CI |
|
| Playwright, before (`1af51c9`) | 23 | 26 passed + 3 skipped | 36.2s / 36.3s | ~4m30s in CI |
|
||||||
| Playwright, after | 16 (+3 skipped) | 15.7s | ~4m30s in CI |
|
| Playwright, after | 14 | 17 passed + 3 skipped | 15.1s / 14.9s | ~4m30s in CI |
|
||||||
| `node --test web/tests` | 26 | 0.11s | seconds |
|
| `node --test web/tests` | — | 26 | 0.12s | seconds |
|
||||||
|
|
||||||
**Cutting Playwright tests barely moves the CI number**, because roughly four
|
**Two columns because one number was ambiguous three times.** Every spec but
|
||||||
minutes of that job is three `wasm-pack` builds, `npm ci` and a browser
|
`opfs.spec.js` is Chromium-only, so a count taken off a run is three larger than
|
||||||
download — not test execution. The feedback-time win is the `js` job, which
|
the count of tests in the files (Firefox's OPFS battery) with three more skipped
|
||||||
|
(WebKit's). The columns must also **agree**: chromium + 3 is the passed total,
|
||||||
|
and a row where they do not is a row built from two different trees.
|
||||||
|
`npx playwright test --list` prints the per-project truth; quote which one, and
|
||||||
|
this is the same "two numbers, not one" the Rust suite learned in
|
||||||
|
`design-principles.md` §7.
|
||||||
|
|
||||||
|
**The trap that made the first `before` figure wrong.** It was taken by
|
||||||
|
`git checkout 1af51c9 -- harness web` inside a working branch — which restores
|
||||||
|
*tracked* files and leaves untracked ones **where they are**, so the branch's own
|
||||||
|
`contract.spec.js` stayed and the "baseline" ran main's 23 tests **plus three of
|
||||||
|
the branch's**. A baseline taken by checking paths out over your own work
|
||||||
|
measures a tree that never existed. The full checkout above is the repair.
|
||||||
|
|
||||||
|
**What gave it away was the count, not the clock — and that is the lesson.** The
|
||||||
|
contaminated run read 29 passed where a clean `1af51c9` reads 26, because three
|
||||||
|
extra tests really were there; but it read 36.9s against the clean tree's 36.2s,
|
||||||
|
because those three cost under a second between them. **A wall clock absorbs a
|
||||||
|
contaminated tree without a trace; an arithmetic identity does not.** The same
|
||||||
|
error survived three separate corrections — it reached PR #61's body as "26
|
||||||
|
chromium" (23 + the 3), survived a fix that corrected the chromium column and
|
||||||
|
left the run count beside it, and was caught each time only by someone checking
|
||||||
|
that the columns add up.
|
||||||
|
|
||||||
|
**Where the saving actually came from, since the obvious answer is wrong.** The
|
||||||
|
eleven per-widget tests that were deleted cost **2.7 seconds of test time
|
||||||
|
between them** — restored verbatim and re-measured — against **1.55s** for the
|
||||||
|
three contract tests replacing them. Those are the per-test sums, not the two
|
||||||
|
wall clocks, which read 3.3s and 3.4s and **flattered the replacements**: they
|
||||||
|
were separate `npx playwright test` invocations, so each paid its own cold
|
||||||
|
start (~0.6s and ~1.9s) and a near-tie appeared between two things not measured
|
||||||
|
the same way. Warm inside a full suite those three cost under a second, which is
|
||||||
|
why the contaminated baseline's clock barely moved.
|
||||||
|
|
||||||
|
Eleven tests at 2.7s cannot buy a twenty-second saving, and they did not.
|
||||||
|
Essentially all of 36.2s → 15s was two
|
||||||
|
`shell.spec.js` tests that **waited out the 8s watchdog in real time**, and one
|
||||||
|
of those two was the routing guard this section explains should never have gone.
|
||||||
|
So the deletion of the eleven bought shape, not speed; the speed came from a
|
||||||
|
deletion that cost coverage. Both are back at ~75ms each, because `page.clock`
|
||||||
|
fakes the page's timers — the honest version of the trade: *the wait was the
|
||||||
|
cost, not the test.*
|
||||||
|
|
||||||
|
**Cutting Playwright tests barely moves the CI number** in any case, because
|
||||||
|
roughly four minutes of that job is three `wasm-pack` builds, `npm ci` and a
|
||||||
|
browser download — not test execution. The feedback-time win is the `js` job, which
|
||||||
needs **no Rust toolchain and no wasm bundle at all**, and that is a constraint
|
needs **no Rust toolchain and no wasm bundle at all**, and that is a constraint
|
||||||
rather than an accident: the moment a `web/tests/*.test.js` file needs
|
rather than an accident: the moment a `web/tests/*.test.js` file needs
|
||||||
`web/pkg/`, it inherits the four-minute build and the argument for it
|
`web/pkg/`, it inherits the four-minute build and the argument for it
|
||||||
|
|||||||
@@ -534,15 +534,33 @@ injected at the *peer*, not in the page** — a mocked socket that answers the
|
|||||||
handshake with a frame the client cannot fold — so this exercises the real
|
handshake with a frame the client cannot fold — so this exercises the real
|
||||||
`on_frame` and the real catch.
|
`on_frame` and the real catch.
|
||||||
|
|
||||||
#### "a peer that cannot be reached is named, and the watchdog stays out of it"
|
#### "a peer that cannot be reached is named, and not called a disconnect"
|
||||||
|
|
||||||
improvise-hwpi: without an `error` listener, a connection that never opened left
|
improvise-hwpi: without an `error` listener, a connection that never opened left
|
||||||
`#status` blank, and "the server is not running" read exactly like "still
|
`#status` blank, and "the server is not running" read exactly like "still
|
||||||
connecting". The watchdog half is asserted on the same page load rather than in
|
connecting". **This test no longer says anything about the watchdog, and the
|
||||||
its own test, because it needs the same nine-second wait. It holds for a reason
|
paragraph below is why it never could.** `shell.arm` runs only after
|
||||||
its own comment does not lead you to expect: `armWatchdog` runs only after
|
`await transport.ready`, which resolves on `open` — so on a socket that never
|
||||||
`transport.ready` resolves, which happens on `open` — so on a socket that never
|
opens the watchdog is **never armed at all**, and denying its message here
|
||||||
opened the watchdog is never armed at all.
|
denies an unreachable event.
|
||||||
|
|
||||||
|
**That sentence was already in this file, five lines above an assertion that
|
||||||
|
contradicted it.** PR #65 added the watchdog clause *and edited this very
|
||||||
|
paragraph*, inserting "`runFor(10s)` advances the watchdog" immediately before
|
||||||
|
the sentence saying it is never armed. Reverting PR #21's routing, or stripping
|
||||||
|
`fail()` of its `sawFrame()`, both left that clause **passing**. So a PR whose
|
||||||
|
subject was "a deleted guard went unnoticed" planted the same shape one level
|
||||||
|
up: three injections, three distinct test names, and two of them proving
|
||||||
|
nothing. What the test guards now is the **naming** — `close` on a socket that
|
||||||
|
never opened must not read as "disconnected" (improvise-taxx) — which is the
|
||||||
|
half injection 3 really does exercise.
|
||||||
|
|
||||||
|
**The mechanism, so it cannot come back.** `outlastTheWatchdog` now asks the
|
||||||
|
page `timesArmed() > 0` before jumping, and a watchdog denial re-added to this
|
||||||
|
path **fails by name**. It asks *ever armed* rather than *armed now*: by the
|
||||||
|
time the disconnect test jumps, a correct page has already disarmed — that is
|
||||||
|
the property — so `armed()` would have denied the passing case. Building that
|
||||||
|
guard is what surfaced the distinction, which no amount of reading had.
|
||||||
|
|
||||||
#### "a socket that opened and then dropped reads as disconnected, and stays that way"
|
#### "a socket that opened and then dropped reads as disconnected, and stays that way"
|
||||||
|
|
||||||
@@ -550,7 +568,8 @@ The other arm, which must keep working when the first is fixed: a socket that
|
|||||||
really did open and then dropped IS a disconnect, and saying so is the whole
|
really did open and then dropped IS a disconnect, and saying so is the whole
|
||||||
reason `close` cannot simply be suppressed.
|
reason `close` cannot simply be suppressed.
|
||||||
|
|
||||||
**It runs past `HANDSHAKE_TIMEOUT_MS` on purpose.** On this arm the watchdog IS
|
**It runs past `HANDSHAKE_TIMEOUT_MS` on purpose** — through `page.clock`, not
|
||||||
|
through a real wait. On this arm the watchdog IS
|
||||||
armed — `armWatchdog` runs after `transport.ready` resolves, i.e. on `open` — so
|
armed — `armWatchdog` runs after `transport.ready` resolves, i.e. on `open` — so
|
||||||
"disconnected" is written while a timer is pending, and the only reason it
|
"disconnected" is written while a timer is pending, and the only reason it
|
||||||
survives is that `close` goes through `setShellFailure`, which clears it. That
|
survives is that `close` goes through `setShellFailure`, which clears it. That
|
||||||
@@ -566,6 +585,45 @@ assertion rather than another paragraph — proved discriminating by making
|
|||||||
`setShellFailure` skip its `clearTimeout`, which makes it fail. Same rule as
|
`setShellFailure` skip its `clearTimeout`, which makes it fail. Same rule as
|
||||||
`doctrine(an_injected_fault_must_be_the_fault_you_named)`, one level up.
|
`doctrine(an_injected_fault_must_be_the_fault_you_named)`, one level up.
|
||||||
|
|
||||||
|
**And it was lost a third time, by the PR that was arguing about exactly this.**
|
||||||
|
PR #61 split the browser tests into boundary contracts (Playwright) and unit
|
||||||
|
tests (`node --test`), and merged these two tests into one that kept only the
|
||||||
|
last-value assertions. `shell.test.js`'s "a loud failure DISARMS the watchdog"
|
||||||
|
covers `createShell` in isolation and **cannot see whether `app.js` routes the
|
||||||
|
close event through `fail()` rather than `setShell()`** — which is the half
|
||||||
|
PR #21 fixed and the half that keeps getting lost. Reverting both socket arms
|
||||||
|
to `setShell` left the node suite 26/26 and Playwright 13/13 green. Restored
|
||||||
|
under their original names, and re-proved with that exact revert: the two-arm
|
||||||
|
revert and the close-arm-only revert each fail "…and stays that way", and
|
||||||
|
dropping the `opened` flag fails "…the watchdog stays out of it". **Three
|
||||||
|
injections, three different tests, none of them the unit test.**
|
||||||
|
|
||||||
|
**And the helper guards its own premise.** `page.clock.runFor` on a page where
|
||||||
|
`clock.install()` was never called does **nothing and throws nothing**, so a test
|
||||||
|
that forgot the install would assert "the watchdog never spoke" against a
|
||||||
|
watchdog never given the chance — vacuous, and green. `outlastTheWatchdog` reads
|
||||||
|
`Date.now()` inside the page either side of the jump and requires it to have
|
||||||
|
moved. Demonstrated both ways: with the install removed the suite passed 6/6,
|
||||||
|
and with the guard in place the same omission fails by name.
|
||||||
|
|
||||||
|
**The window is single-homed, and that is the second half of the same
|
||||||
|
finding.** `HANDSHAKE_TIMEOUT_MS` lived twice — once in `web/app.js`, once in
|
||||||
|
the spec — with a comment saying a silent disagreement "would pass for the
|
||||||
|
wrong reason" and nothing checking it. Setting the spec's copy to 1000 against
|
||||||
|
the page's 8000 made the whole suite green **against the reverted routing**:
|
||||||
|
the jump landed inside the window, the watchdog had not yet fired, and the
|
||||||
|
denial held vacuously. It is now exported from `web/shell.js` and imported by
|
||||||
|
both, so the two cannot disagree; shrinking it in its one home still catches
|
||||||
|
the routing revert, which is the demonstration that the guard did not go quiet.
|
||||||
|
|
||||||
|
**The faked clock is what makes the restoration cheap without weakening it**,
|
||||||
|
and the injections are the proof of that too: if `page.clock.runFor` did not
|
||||||
|
really advance the page's timers, a broken routing could not surface the
|
||||||
|
watchdog's message and those injections would have passed. They failed. The
|
||||||
|
test keeps the real page, the real socket, the real listeners and the real
|
||||||
|
`setTimeout` call — only the *passage of time* is simulated, and 9 seconds of
|
||||||
|
wall clock became 75 milliseconds.
|
||||||
|
|
||||||
**The measurement that could not become a CI guard.** Both of the above were once
|
**The measurement that could not become a CI guard.** Both of the above were once
|
||||||
asserted as `test.fail` expected-failures and **had to be removed**: neither
|
asserted as `test.fail` expected-failures and **had to be removed**: neither
|
||||||
reproduced on the GitHub runner, where `close` does not follow `error` within the
|
reproduced on the GitHub runner, where `close` does not follow `error` within the
|
||||||
@@ -1591,6 +1649,11 @@ Every test below was proved able to fail by patching the code it names,
|
|||||||
checking the patch was on disk, running the suite and reading the failure —
|
checking the patch was on disk, running the suite and reading the failure —
|
||||||
never by the absence of an error. Thirteen injections, thirteen failures:
|
never by the absence of an error. Thirteen injections, thirteen failures:
|
||||||
|
|
||||||
|
**What `shell.test.js` cannot reach, and the PR found out the hard way:** it
|
||||||
|
tests `createShell` in isolation, so it sees the watchdog RULE and never the
|
||||||
|
`app.js` wiring that calls it. That distinction is now written into
|
||||||
|
`context/code/clients.md` and guarded in `shell.spec.js`.
|
||||||
|
|
||||||
**`shell.test.js`** — `socketUrl` stops upgrading `ws://` on an https page;
|
**`shell.test.js`** — `socketUrl` stops upgrading `ws://` on an https page;
|
||||||
`composeStatus` returns the model fact alone (which fails five tests, because
|
`composeStatus` returns the model fact alone (which fails five tests, because
|
||||||
the watchdog's messages are shell facts and vanish with it); `fail()` stops
|
the watchdog's messages are shell facts and vanish with it); `fail()` stops
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
import { HANDSHAKE_TIMEOUT_MS } from "../../web/shell.js";
|
||||||
|
|
||||||
const status = (page) => page.locator("#status");
|
const status = (page) => page.locator("#status");
|
||||||
|
|
||||||
/// A peer that accepts the TCP connection and then refuses the WebSocket
|
/// A peer that accepts the TCP connection and then refuses the WebSocket
|
||||||
@@ -11,9 +13,45 @@ const status = (page) => page.locator("#status");
|
|||||||
/// the harness's OWN server, so it follows the port the config chose.
|
/// the harness's OWN server, so it follows the port the config chose.
|
||||||
const NO_WEBSOCKET_HERE = `127.0.0.1:${process.env.IMPROVISE_HARNESS_PORT ?? "8099"}`;
|
const NO_WEBSOCKET_HERE = `127.0.0.1:${process.env.IMPROVISE_HARNESS_PORT ?? "8099"}`;
|
||||||
|
|
||||||
/// `web/app.js`'s own HANDSHAKE_TIMEOUT_MS. One test has to outlast it, and a
|
/// The page's own constant, imported rather than copied: a spec that
|
||||||
/// number that silently disagreed with the page would pass for the wrong reason.
|
/// understated it would jump short of the window and every watchdog assertion
|
||||||
const HANDSHAKE_TIMEOUT_MS = 8_000;
|
/// below would pass for the wrong reason — demonstrated, PR #65 review.
|
||||||
|
|
||||||
|
/// Record every distinct #status the page shows, not just the last one: the
|
||||||
|
/// properties below are about a message being REPLACED, which a final read
|
||||||
|
/// cannot see. Installed before the page's own script runs.
|
||||||
|
function logStatus() {
|
||||||
|
window.__statusLog = [];
|
||||||
|
new MutationObserver(() => {
|
||||||
|
const text = document.getElementById("status")?.textContent ?? "";
|
||||||
|
if (text && window.__statusLog.at(-1) !== text) window.__statusLog.push(text);
|
||||||
|
}).observe(document, { childList: true, subtree: true, characterData: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = (v) => JSON.stringify(v);
|
||||||
|
|
||||||
|
/// Walk past the watchdog without waiting for it. `page.clock` fakes the
|
||||||
|
/// page's own timers, so the 8s is the PAGE's and the wall clock is not.
|
||||||
|
async function outlastTheWatchdog(page) {
|
||||||
|
// A negative assertion about the watchdog is vacuous where the watchdog was
|
||||||
|
// never armed, and `shell.arm` runs only after `await transport.ready`. It
|
||||||
|
// asks whether it was EVER armed: by here a correct page has disarmed it,
|
||||||
|
// which is the property, so `armed()` would deny the passing case.
|
||||||
|
expect(
|
||||||
|
await page.evaluate(() => window.__improviseShell?.timesArmed() ?? 0),
|
||||||
|
"the watchdog was never armed on this path: denying its message proves nothing",
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
const jump = HANDSHAKE_TIMEOUT_MS + 2_000;
|
||||||
|
const before = await page.evaluate(() => Date.now());
|
||||||
|
await page.clock.runFor(jump);
|
||||||
|
// `runFor` without `clock.install()` does NOTHING and throws nothing, so a
|
||||||
|
// test that forgot it would assert "the watchdog never spoke" vacuously.
|
||||||
|
const after = await page.evaluate(() => Date.now());
|
||||||
|
expect(
|
||||||
|
after - before,
|
||||||
|
"page.clock.install() was not called: the page's timers never advanced",
|
||||||
|
).toBeGreaterThanOrEqual(jump);
|
||||||
|
}
|
||||||
|
|
||||||
test.describe("the shell reports what its peer is doing", () => {
|
test.describe("the shell reports what its peer is doing", () => {
|
||||||
// improvise-hwpi. The fault is injected at the *peer*, not in the page, so
|
// improvise-hwpi. The fault is injected at the *peer*, not in the page, so
|
||||||
@@ -26,15 +64,30 @@ test.describe("the shell reports what its peer is doing", () => {
|
|||||||
await expect(status(page)).toContainText("bad frame", { timeout: 15_000 });
|
await expect(status(page)).toContainText("bad frame", { timeout: 15_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// A socket that never opened did not "disconnect" (improvise-taxx): nothing
|
// improvise-hwpi. This peer NEVER OPENS, so `await transport.ready` never
|
||||||
// on the close event can discriminate, so the page records `opened` itself.
|
// resolves and `shell.arm` never runs — the watchdog is not on this path.
|
||||||
// That it then STAYS is the watchdog's disarming, which is Node's now.
|
test("a peer that cannot be reached is named, and not called a disconnect", async ({
|
||||||
test("a peer that cannot be reached is named, and one that dropped is not confused with it", async ({
|
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
await page.addInitScript(logStatus);
|
||||||
await page.goto(`/app/index.html?ws=${NO_WEBSOCKET_HERE}`);
|
await page.goto(`/app/index.html?ws=${NO_WEBSOCKET_HERE}`);
|
||||||
await expect(status(page)).toContainText("cannot reach", { timeout: 15_000 });
|
await expect(status(page)).toContainText("cannot reach", { timeout: 15_000 });
|
||||||
|
|
||||||
|
// The naming is the whole property here: `close` on a socket that never
|
||||||
|
// opened must not read as "disconnected" (improvise-taxx).
|
||||||
|
const log = await page.evaluate(() => window.__statusLog);
|
||||||
|
expect(log.some((t) => t.includes("disconnected")), `#status said: ${json(log)}`).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The arm PR #21 fixed and that gotcha 48 says keeps getting lost: a socket
|
||||||
|
// that never opened did not "disconnect", and the one that did STAYS so.
|
||||||
|
test("a socket that opened and then dropped reads as disconnected, and stays that way", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.addInitScript(logStatus);
|
||||||
// Closes on the page's first frame, so "it opened first" is true by
|
// Closes on the page's first frame, so "it opened first" is true by
|
||||||
// construction rather than by timer.
|
// construction rather than by timer.
|
||||||
await page.routeWebSocket("ws://127.0.0.1:8790/", (ws) => {
|
await page.routeWebSocket("ws://127.0.0.1:8790/", (ws) => {
|
||||||
@@ -43,6 +96,16 @@ test.describe("the shell reports what its peer is doing", () => {
|
|||||||
await page.goto("/app/index.html?ws=127.0.0.1:8790");
|
await page.goto("/app/index.html?ws=127.0.0.1:8790");
|
||||||
await expect(status(page)).toContainText("connected", { timeout: 15_000 });
|
await expect(status(page)).toContainText("connected", { timeout: 15_000 });
|
||||||
await expect(status(page)).toContainText("disconnected", { timeout: 15_000 });
|
await expect(status(page)).toContainText("disconnected", { timeout: 15_000 });
|
||||||
|
|
||||||
|
// Past the watchdog it was armed against: the routing, not the rule, is
|
||||||
|
// what this can see and `shell.test.js` cannot.
|
||||||
|
await outlastTheWatchdog(page);
|
||||||
|
await expect(status(page)).toContainText("disconnected");
|
||||||
|
|
||||||
|
const log = await page.evaluate(() => window.__statusLog);
|
||||||
|
expect(log.some((t) => t.includes("sent no document")), `#status said: ${json(log)}`).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The one assertion that needs the real timer: that the watchdog is ARMED
|
// The one assertion that needs the real timer: that the watchdog is ARMED
|
||||||
|
|||||||
+4
-1
@@ -4,6 +4,7 @@
|
|||||||
import init, { Client } from "./pkg/improvise_wasm_client.js";
|
import init, { Client } from "./pkg/improvise_wasm_client.js";
|
||||||
import {
|
import {
|
||||||
COL_PX,
|
COL_PX,
|
||||||
|
HANDSHAKE_TIMEOUT_MS,
|
||||||
ROW_PX,
|
ROW_PX,
|
||||||
createShell,
|
createShell,
|
||||||
extentOf,
|
extentOf,
|
||||||
@@ -40,13 +41,15 @@ const client = new Client();
|
|||||||
|
|
||||||
// A peer that never answers the handshake, and the two facts `#status` shows.
|
// 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.
|
// Both are decided in shell.js; this is the wire to the element.
|
||||||
const HANDSHAKE_TIMEOUT_MS = 8000;
|
|
||||||
const shell = createShell({
|
const shell = createShell({
|
||||||
onStatus: (text) => {
|
onStatus: (text) => {
|
||||||
el.status.textContent = text;
|
el.status.textContent = text;
|
||||||
},
|
},
|
||||||
timeoutMs: HANDSHAKE_TIMEOUT_MS,
|
timeoutMs: HANDSHAKE_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
|
// The harness asks whether the watchdog is ARMED, because a negative assertion
|
||||||
|
// about it is vacuous where it is not (clients.md). Read by tests, nothing else.
|
||||||
|
window.__improviseShell = shell;
|
||||||
|
|
||||||
// What the grid's own frame costs, in whole cells, as of the last repaint —
|
// What the grid's own frame costs, in whole cells, as of the last repaint —
|
||||||
// measured off the painted model, never predicted. context/code/clients.md.
|
// measured off the painted model, never predicted. context/code/clients.md.
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export function indicator(collapsed) {
|
|||||||
export const ROW_PX = 26;
|
export const ROW_PX = 26;
|
||||||
export const COL_PX = 110;
|
export const COL_PX = 110;
|
||||||
|
|
||||||
|
/** How long a peer has to answer the handshake. Exported because the harness
|
||||||
|
* must jump *past* it: a spec with its own copy can understate it silently
|
||||||
|
* and every watchdog assertion then passes vacuously (clients.md). */
|
||||||
|
export const HANDSHAKE_TIMEOUT_MS = 8000;
|
||||||
|
|
||||||
/** The extent a box of this size can show, in whole DATA cells: the frame is
|
/** The extent a box of this size can show, in whole DATA cells: the frame is
|
||||||
* charged for first, and at least one cell is always asked for. */
|
* charged for first, and at least one cell is always asked for. */
|
||||||
export function extentOf(box, frameCost, metrics = { rowPx: ROW_PX, colPx: COL_PX }) {
|
export function extentOf(box, frameCost, metrics = { rowPx: ROW_PX, colPx: COL_PX }) {
|
||||||
@@ -107,6 +112,7 @@ export function createShell({ onStatus, timeoutMs, setTimer = setTimeout, clearT
|
|||||||
let shell = "";
|
let shell = "";
|
||||||
let model = "";
|
let model = "";
|
||||||
let watchdog = null;
|
let watchdog = null;
|
||||||
|
let arms = 0;
|
||||||
const paint = () => onStatus(composeStatus(shell, model));
|
const paint = () => onStatus(composeStatus(shell, model));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -121,6 +127,7 @@ export function createShell({ onStatus, timeoutMs, setTimer = setTimeout, clearT
|
|||||||
/** A peer that never answers the handshake. Deliberately about the *first
|
/** A peer that never answers the handshake. Deliberately about the *first
|
||||||
* frame*, not either peer's own failure modes — clients.md. */
|
* frame*, not either peer's own failure modes — clients.md. */
|
||||||
arm(message) {
|
arm(message) {
|
||||||
|
arms += 1;
|
||||||
clearTimer(watchdog);
|
clearTimer(watchdog);
|
||||||
watchdog = setTimer(() => {
|
watchdog = setTimer(() => {
|
||||||
watchdog = null;
|
watchdog = null;
|
||||||
@@ -140,5 +147,8 @@ export function createShell({ onStatus, timeoutMs, setTimer = setTimeout, clearT
|
|||||||
this.setShell(text);
|
this.setShell(text);
|
||||||
},
|
},
|
||||||
armed: () => watchdog !== null,
|
armed: () => watchdog !== null,
|
||||||
|
/** How many times the watchdog has been armed. A test denying its message
|
||||||
|
* needs this to be nonzero, or it is denying an unreachable event. */
|
||||||
|
timesArmed: () => arms,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
const read = (rel) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
|
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
|
/// 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.
|
/// belongs here only with an argument; the list is the debt, written down.
|
||||||
// improvise-z754 put the title bar's facts in one shared cache and taught
|
// improvise-z754 put the title bar's facts in one shared cache and taught
|
||||||
@@ -32,7 +39,7 @@ function renderModelFields() {
|
|||||||
|
|
||||||
test("every field the render model projects is painted, or named as debt", () => {
|
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.
|
// Both halves of the page: `app.js` paints, `shell.js` plans the grid.
|
||||||
const page = read("../app.js") + read("../shell.js");
|
const page = code("../app.js") + code("../shell.js");
|
||||||
const unpainted = renderModelFields().filter((field) => !page.includes(`model.${field}`));
|
const unpainted = renderModelFields().filter((field) => !page.includes(`model.${field}`));
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
unpainted.sort(),
|
unpainted.sort(),
|
||||||
|
|||||||
Reference in New Issue
Block a user