7e991360df
improvise-vvl3, absorbing improvise-xf1. The condition the user set when
deferring browser testing — "WE HAVE TO HAVE SOMETHING TO TEST" — has been
met: after improvise-9x6, djm, hwpi and the demo model there is a complete
self-contained application whose entire untested surface is what a harness
covers.
`python3 -m http.server` plus one Playwright project, in `harness/`:
harness/opfs-contract/ a wasm crate that runs
`improvise_io::storage::contract::storage_contract`
against `OpfsFs` — the shared function, not a
transcription of it
harness/www/opfs/ the page and the dedicated Web Worker it runs in;
`createSyncAccessHandle` exists nowhere else
harness/tests/ two specs
harness/build.sh site + harness bundle -> harness/serve
It lives at the repo root rather than under `web/`, because `web/` is the
*source of what ships* and every file in it is copied to the site. The
harness serves the **assembled** tree (`web/assemble.sh`, extracted from
the CI workflow in this commit so there is one answer to "how is the site
arranged" rather than two) plus its own `/opfs/` — a harness that runs
against a different arrangement of files is testing something nobody
deploys. `harness/opfs-contract` is deliberately not a workspace member: it
only builds for wasm32, so a member would break `cargo test --workspace`.
What this runs that nothing has ever run:
- `storage_contract` against `OpfsFs`, at `""` **and** at `"docs"`. Two
bugs fell straight out — improvise-vhcz's `list` path shape, whose fix
the `"docs"` run guards (reverting it fails with `lists the one
document`), and the zero-byte entry fix in the previous commit, which
this found by failing on `starts empty`.
- The `keys()` async-iterator walk in `refresh`, hand-rolled through
`js_sys::Reflect` and named by xf1 as the least certain code in the file.
Three entries written out of order, so one passing by accident cannot
carry it.
- `FileSystemReadWriteOptions`' `at` on read and write: a long write then a
short one, which is where an append or a non-zero start shows up.
- `commit_deletes`, both halves — queued but still in OPFS before, gone
after — and **across a reload**, which is the only way to ask whether
OPFS persisted. That reload is also improvise-bck's persistence criterion
at the storage layer.
- improvise-hwpi's three one-assertion shell tests, which that ticket said
it could not write: status after a bad frame, status with no peer, one
repaint on commit. Plus the first-frame watchdog, which was found by
reading and only because someone went looking.
Check the checker, throughout, because a harness that passes against a stub
— or that silently skips because the worker never started — is exactly this
repo's characteristic defect wearing a green tick:
- `run_canary` deliberately fails, through the *same* reporting path every
real check uses, and the spec asserts it is reported as a failure with its
message intact.
- The spec asserts the report contains exactly the expected check names, in
order, so a battery that quietly shrinks is a failure.
- Every shell assertion was injected and watched to fail: removing the
bad-frame catch, the watchdog's `setTimeout`, the socket `error` listener
and the `transport.send(frame)` on commit each fails its own test and
only its own.
- The commit test types `20000` and asserts `20,000`, so a page that echoed
what was typed instead of drawing what the host returned would fail.
One live bug is pinned rather than fixed. `a peer that cannot be reached`
asserts the *history* of `#status`, because improvise-m18f is real and
reproduced here: the socket's `close` listener overwrites the precise
"cannot reach" with "disconnected". That is filed and deliberately not the
same fix as the watchdog's (gotcha 48), so the test asserts both halves and
will speak up whichever one moves.
CI gets a `browser` job that can fail the build, and `pages-deploy` now
needs it. Playwright's Chromium download is ~80 MB and well under a minute
on ubuntu-latest, next to the two wasm-pack builds the job already needs —
so "impractical in this CI" does not arise. A harness nobody runs is worse
than none, because it reads as coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
116 lines
5.7 KiB
JavaScript
116 lines
5.7 KiB
JavaScript
// The browser shell: the three assertions improvise-hwpi could not write.
|
|
//
|
|
// That ticket fixed four things and said of itself that the fixes were "as
|
|
// unverified as the bugs", because three of them are one-assertion tests that
|
|
// needed a browser. These are those three, plus the watchdog — which was found
|
|
// by reading, and only because someone went looking.
|
|
//
|
|
// `web/app.js` and `web/worker.js` have never been rendered or run (gotcha 48).
|
|
// Everything below is the first execution of either.
|
|
|
|
import { test, expect } from "@playwright/test";
|
|
|
|
const status = (page) => page.locator("#status");
|
|
|
|
test.describe("the shell reports what its peer is doing", () => {
|
|
// improvise-hwpi: `on_frame` returns a Result, which wasm-bindgen throws.
|
|
// Thrown in a bare listener it reached the devtools console and nothing
|
|
// else: the grid simply stopped updating with nothing on screen saying why.
|
|
//
|
|
// The fault is 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 `on_frame` and the real catch.
|
|
test("a frame the client cannot fold says so on screen", async ({ page }) => {
|
|
await page.routeWebSocket("ws://127.0.0.1:8787/", (ws) => {
|
|
ws.onMessage(() => ws.send("{\"NotAFrame\":{}}"));
|
|
});
|
|
await page.goto("/index.html?ws=127.0.0.1:8787");
|
|
await expect(status(page)).toContainText("bad frame", { timeout: 15_000 });
|
|
});
|
|
|
|
// improvise-hwpi: without an `error` listener, a connection that never
|
|
// opened left #status blank, and "the server is not running" read exactly
|
|
// like "still connecting". Port 9 (discard) refuses immediately.
|
|
//
|
|
// It is asserted over the *history* of #status rather than its final value,
|
|
// because of improvise-m18f, which this reproduces: the socket's `close`
|
|
// listener fires after `error` and overwrites the precise message with
|
|
// "disconnected". That one is filed and deliberately not fixed here —
|
|
// suppressing `close` could hide a genuine mid-session disconnect, so it is
|
|
// not the same fix as the watchdog's (gotcha 48). Asserting both halves
|
|
// pins hwpi's fix AND m18f's bug, so whichever moves, this speaks up.
|
|
test("a peer that cannot be reached is named, not left blank", async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
window.__statusLog = [];
|
|
new MutationObserver(() => {
|
|
const el = document.getElementById("status");
|
|
const text = el?.textContent ?? "";
|
|
if (text && window.__statusLog.at(-1) !== text) window.__statusLog.push(text);
|
|
}).observe(document, { childList: true, subtree: true, characterData: true });
|
|
});
|
|
await page.goto("/index.html?ws=127.0.0.1:9");
|
|
await expect(status(page)).toContainText("disconnected", { timeout: 15_000 });
|
|
|
|
const log = await page.evaluate(() => window.__statusLog);
|
|
expect(log.some((t) => t.includes("cannot reach")), `#status said: ${JSON.stringify(log)}`)
|
|
.toBe(true);
|
|
// improvise-m18f, as it stands today. Delete this line when it is fixed —
|
|
// it is here so the fix cannot land silently.
|
|
expect(log.at(-1)).toContain("disconnected");
|
|
});
|
|
|
|
// The first-frame watchdog (gotcha 46), found by reading. The condition is
|
|
// one no per-transport check would catch: the peer accepted the connection
|
|
// and then said nothing. The mock accepts and never answers.
|
|
test("a peer that accepts and then says nothing trips the watchdog", async ({
|
|
page,
|
|
}) => {
|
|
await page.routeWebSocket("ws://127.0.0.1:8788/", () => {});
|
|
await page.goto("/index.html?ws=127.0.0.1:8788");
|
|
await expect(status(page)).toContainText("sent no document", { timeout: 20_000 });
|
|
});
|
|
});
|
|
|
|
test.describe("the standalone deployment", () => {
|
|
// No `?ws=`, so `web/app.js` spawns the in-page worker over OPFS: the whole
|
|
// application, with no server process. If this draws a grid, then
|
|
// worker.js, bindings.rs, OpfsFs, vfs::open and the seed fetch all ran.
|
|
test("the page draws the demo document out of the worker", async ({ page }) => {
|
|
await page.goto("/index.html");
|
|
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
|
|
await expect(status(page)).toContainText("in-page worker");
|
|
// web/demo.improv: Product on row, _Measure on column (gotcha 46).
|
|
await expect(page.locator("#grid th", { hasText: "Revenue" })).toBeVisible();
|
|
await expect(page.locator("#grid td.editable").first()).toBeVisible();
|
|
});
|
|
|
|
// improvise-hwpi's third fix: Enter removes the focused <input>, which
|
|
// fires `blur`, which called `close` a second time — harmless before, but
|
|
// only by accident. The observable is that the commit takes effect exactly
|
|
// once and the value on screen is the SERVER's, not the string typed.
|
|
//
|
|
// `20000` is typed and `20,000` is asserted: the number format is applied
|
|
// by the host and relayed back, so a page that simply echoed what was typed
|
|
// would show `20000` and fail.
|
|
test("committing an edit repaints once, with the server's value", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/index.html?doc=commit-test.improv");
|
|
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
|
|
|
|
const cell = page.locator("#grid td.editable").first();
|
|
const before = await cell.textContent();
|
|
await cell.click();
|
|
const input = cell.locator("input");
|
|
await expect(input).toBeFocused();
|
|
await input.fill("20000");
|
|
await input.press("Enter");
|
|
|
|
await expect(cell).toHaveText("20,000", { timeout: 10_000 });
|
|
expect(before).not.toBe("20,000");
|
|
// One editor, closed once: a second `close` would reopen nothing, but a
|
|
// stray input left in the DOM is what the guard exists to prevent.
|
|
await expect(page.locator("#grid input")).toHaveCount(0);
|
|
});
|
|
});
|