diff --git a/harness/tests/contract.spec.js b/harness/tests/contract.spec.js index 906a0455..cb8d816b 100644 --- a/harness/tests/contract.spec.js +++ b/harness/tests/contract.spec.js @@ -9,6 +9,19 @@ import { test, expect } from "@playwright/test"; // the same with the handshake window). import { NOTICE_KEY } from "../../web/shell.js"; +/** Every string the page posts to its worker, kept on `window.__posted`. */ +function recordPosts() { + window.__posted = []; + const post = Worker.prototype.postMessage; + Worker.prototype.postMessage = function (message, ...rest) { + window.__posted.push(String(message)); + return post.call(this, message, ...rest); + }; +} + +const setCells = (page) => + page.evaluate(() => window.__posted.filter((m) => m.includes("SetCell")).length); + async function openApp(page) { await page.goto("/app/index.html"); await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 }); @@ -320,6 +333,76 @@ test.describe("a pointer event reaches the client and its answer is painted", () await expect.poll(writes).toBe(1); }); + // improvise-jibp. The page reports its first edit, so retyping the seed over + // the selection is a write, not an untouched editor. + test("a mouse editor typed back to its own seed still writes", async ({ page }) => { + await page.addInitScript(recordPosts); + await openApp(page); + const cells = page.locator("#grid tbody td"); + await cells.nth(4).click(); + const input = cells.nth(4).locator("input"); + await expect(input).toHaveCount(1); + const seed = await input.inputValue(); + expect(seed, "the fixture cell has no seed to retype").not.toBe(""); + await page.keyboard.type(seed); + await expect(input).toHaveValue(seed); + await page.keyboard.press("Enter"); + await expect.poll(() => setCells(page)).toBe(1); + }); + + // improvise-ugj4. A frame that lands while an editor is open changes the + // cache but not the paint; the next editor must open on what it captured. + test("an editor opened after an unpainted write shows and commits what it captured", async ({ + page, + }) => { + await page.addInitScript(recordPosts); + await page.addInitScript(() => { + window.__frames = []; + const add = Worker.prototype.addEventListener; + Worker.prototype.addEventListener = function (type, listener, ...rest) { + if (type !== "message") return add.call(this, type, listener, ...rest); + window.__deliver = (data) => listener.call(this, new MessageEvent("message", { data })); + const recorded = (event) => { + if (typeof event.data === "string") window.__frames.push(event.data); + return listener.call(this, event); + }; + return add.call(this, type, recorded, ...rest); + }; + }); + await openApp(page); + const cells = page.locator("#grid tbody td"); + const target = cells.nth(4); + const where = await target.evaluate((td) => [Number(td.dataset.row), Number(td.dataset.col)]); + + await cells.nth(0).click(); + await expect(cells.nth(0).locator("input")).toHaveCount(1); + // Another client's write to cell 4, delivered while cell 0's editor is open. + await page.evaluate(([row, col]) => { + let latest = null; + const walk = (node) => { + if (Array.isArray(node)) node.forEach(walk); + else if (node && typeof node === "object") { + for (const [key, value] of Object.entries(node)) { + if (key === "Cells" && Array.isArray(value)) { + for (const [[r, c], cell] of value) if (r === row && c === col) latest = cell; + } else walk(value); + } + } + }; + window.__frames.forEach((f) => walk(JSON.parse(f))); + const moved = { ...latest, text: "777", edit_text: "777", value: { Number: 777 } }; + window.__deliver(JSON.stringify({ Commands: [{ Project: { Grid: { Cells: [[[row, col], moved]] } } }] })); + }, where); + + await target.click(); + const input = target.locator("input"); + await expect(input).toHaveCount(1); + await expect(input, "the editor opened on the stale paint").toHaveValue("777"); + await page.keyboard.press("Enter"); + await page.waitForTimeout(300); + expect(await setCells(page), "an untouched editor wrote the stale text").toBe(0); + }); + // improvise-b8ot moved these five surfaces onto one handler per stable root. // The three tests below are the properties a naive delegation loses, each of // which is silent: the page still paints, and only the pointer stops working.