// The page↔peer boundary: the handshake, a frame answered whole, and what the // page says when a peer fails. What the arm/disarm RULE is, and every other // decision between these seams, is `node --test web/tests` — clients.md. import { test, expect } from "@playwright/test"; const status = (page) => page.locator("#status"); /// A peer that accepts the TCP connection and then refuses the WebSocket /// upgrade — deterministic where a closed port is not (evidence file). It is /// 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"}`; /// `web/app.js`'s own HANDSHAKE_TIMEOUT_MS. One test has to outlast it, and a /// number that silently disagreed with the page would pass for the wrong reason. const HANDSHAKE_TIMEOUT_MS = 8_000; test.describe("the shell reports what its peer is doing", () => { // improvise-hwpi. The fault is injected at the *peer*, not in the page, 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("/app/index.html?ws=127.0.0.1:8787"); await expect(status(page)).toContainText("bad frame", { timeout: 15_000 }); }); // A socket that never opened did not "disconnect" (improvise-taxx): nothing // on the close event can discriminate, so the page records `opened` itself. // That it then STAYS is the watchdog's disarming, which is Node's now. test("a peer that cannot be reached is named, and one that dropped is not confused with it", async ({ page, }) => { await page.goto(`/app/index.html?ws=${NO_WEBSOCKET_HERE}`); await expect(status(page)).toContainText("cannot reach", { timeout: 15_000 }); // Closes on the page's first frame, so "it opened first" is true by // construction rather than by timer. await page.routeWebSocket("ws://127.0.0.1:8790/", (ws) => { ws.onMessage(() => ws.close()); }); 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("disconnected", { timeout: 15_000 }); }); // The one assertion that needs the real timer: that the watchdog is ARMED // after the handshake goes out. What it then says, and that a loud failure // disarms it, are unit tests. 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("/app/index.html?ws=127.0.0.1:8788"); await expect(status(page)).toContainText("sent no document", { timeout: HANDSHAKE_TIMEOUT_MS + 12_000, }); }); }); test.describe("the standalone deployment", () => { // No `?ws=`, so the page spawns the in-page worker over OPFS. If this draws // a grid, worker.js, bindings.rs, OpfsFs and the seed fetch all ran. test("the page draws the demo document out of the worker", async ({ page }) => { await page.goto("/app/index.html"); await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 }); await expect(status(page)).toContainText("in-page worker"); await expect(page.locator("#grid td.editable").first()).toBeVisible(); }); // What lands on screen is the HOST's value, not the string typed — the // round trip through a real worker, which no unit test can stand in for. test("committing an edit sends one frame and shows the host's value", async ({ page, }) => { await page.goto("/app/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"); // The editor is gone and did not come back: the half of the re-entry guard // that IS observable from the DOM. await expect(page.locator("#grid input")).toHaveCount(0); }); });