// The RenderModel→DOM boundary and the pointer's half of the JS→wasm one: the // two contracts only a browser can establish. Why these and not more, and what // moved to `node --test web/tests`: context/code/clients.md. import { test, expect } from "@playwright/test"; async function openApp(page) { await page.goto("/app/index.html"); await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 }); await page.locator("#grid").focus(); } test.describe("every field the render model projects reaches the screen", () => { // The four widgets a user reported as "not working" — the tile bar, the view // list, the formula panel and the category sidebar ignoring its flag — were // ONE failure at this boundary: a projected field with no element painting // it. This is that contract, and it is what catches the fifth. test("the widgets the demo document fills are drawn, in the state that fills them", async ({ page, }) => { await openApp(page); // `rows`, `col_levels`, `row_levels`: the grid itself. web/demo.improv is // Product on row, _Measure on column (gotcha 46). await expect(page.locator("#grid thead th", { hasText: "Revenue" })).toBeVisible(); await expect(page.locator("#grid tbody th").first()).toHaveText("Espresso"); await expect(page.locator("#grid tbody td").first()).not.toHaveText(""); // `mode`, `hint`, `view_name`, `status`: the header's four facts. await expect(page.locator("#mode")).toHaveText("NORMAL"); await expect(page.locator("#hint")).not.toHaveText(""); await expect(page.locator("#view-name")).toHaveText("Plan"); await expect(page.locator("#status")).toContainText("in-page worker"); // `tiles`: drawn on every frame, as the TUI draws them. The axis label is // `Axis::short_label` — "Pag", and "·" for None, an answer not a stub. await expect(page.locator("#tiles .tile")).toHaveCount(5); await expect(page.locator("#tiles .tile").filter({ hasText: "Product" })).toHaveText( "Product Row", ); // `categories` + `category_panel_open`: the flag is the client's answer, // and the sidebar was drawn always until it read it (improvise-wsaw). await expect(page.locator("#categories")).toBeHidden(); await page.keyboard.press("Shift+C"); await expect(page.locator("#categories")).toBeVisible(); await expect(page.locator("#categories li").first()).toBeVisible(); await page.keyboard.press("Shift+C"); await expect(page.locator("#categories")).toBeHidden(); // `views`: empty unless open, so the page asks no question about the mode. await expect(page.locator("#views")).toBeHidden(); await page.keyboard.press("Shift+V"); await expect(page.locator("#views li")).toHaveCount(2); await expect(page.locator("#views li.active")).toContainText("Plan"); // The axis line is the shared `view_axis_summary`; its absence would mean // a second derivation had crept in. await expect(page.locator("#views li").nth(0).locator(".axes")).toHaveText( "R:Product C:Quarter", ); await page.keyboard.press("Escape"); // `formulas`: the third of `Panel::ALL`, on the same terms. await page.keyboard.press("Shift+F"); await expect(page.locator("#formulas li")).toHaveText(["Profit = Revenue - Cost"]); await page.keyboard.press("Escape"); // `help`, `help_page`, `help_titles`: the rows and their keys are // `improvise_protocol::help`'s, the same ones the TUI draws. await expect(page.locator("#help")).toBeHidden(); await page.keyboard.press("?"); await expect(page.locator(".help-tab.active")).toHaveText("Welcome"); await expect(page.locator(".help-body dt").first()).toBeVisible(); await page.keyboard.press("l"); await expect(page.locator(".help-tab.active")).toHaveText("Navigation"); await page.keyboard.press("Escape"); await expect(page.locator("#help")).toBeHidden(); // `editing`: what has been typed, drawn into the selected cell with a // caret. The decision that an editor is open is made in Rust. await page.keyboard.press("i"); await expect(page.locator("#grid td.typing")).toHaveCount(1); // The class alone would pass against a cell drawn blank: what is on the // screen is the buffer AND the caret. await expect(page.locator("#grid td.typing")).toContainText("▏"); await page.keyboard.press("Escape"); await expect(page.locator("#grid td.typing")).toHaveCount(0); }); // The other half of the same contract: a change the client resolved must // reach the pixels, not merely the cache. One instance is enough — what // each key MEANS is `improvise_protocol::resolve`'s, and tested there. test("a resolved change repaints the widget it moved and the grid with it", async ({ page, }) => { await openApp(page); const product = page.locator("#tiles .tile").filter({ hasText: "Product" }); const rowHeaders = page.locator("#grid tbody th"); await expect(rowHeaders).toHaveText(["Espresso", "Latte", "Cold Brew", "Pastry"]); await page.keyboard.press("Shift+T"); for (let i = 0; i < 3; i++) await page.keyboard.press("ArrowRight"); await expect(product).toHaveClass(/selected/); await page.keyboard.press("c"); await expect(product).toHaveText("Product Col", { timeout: 10_000 }); // The label alone would be the bar reporting an axis the document has not // got: the grid must have moved with it. await expect( page.locator("#grid thead th").filter({ hasText: "Espresso" }).first(), ).toBeVisible(); await expect(rowHeaders).not.toHaveText(["Espresso", "Latte", "Cold Brew", "Pastry"]); }); }); test.describe("a pointer event reaches the client and its answer is painted", () => { // improvise-7ybz. What a click MEANS is resolved in Rust and tested in // `improvise_protocol::resolve`; what this pins is the wiring — the handler // is on the element, it passes THAT element's index, and the result repaints. test("each clickable surface is wired to its own index", async ({ page }) => { await openApp(page); // A grid cell: the coordinate is the data row, which group headers do not // consume — `gridPlan` counts it and `web/tests/grid_plan.test.js` pins it. const cells = page.locator("#grid tbody td"); await expect(cells.nth(0)).toHaveClass(/selected/); await cells.nth(4).click(); await page.keyboard.press("Escape"); await expect(cells.nth(4)).toHaveClass(/selected/); await expect(page.locator("#grid tbody td.selected")).toHaveCount(1); // The cursor a click leaves is the one the keyboard moves from: one // cursor, two input paths. await page.keyboard.press("ArrowLeft"); await expect(cells.nth(3)).toHaveClass(/selected/); // A tile: selects and cycles, because a click is the whole interaction. const product = page.locator("#tiles .tile").filter({ hasText: "Product" }); await product.click(); await expect(page.locator("#mode")).toHaveText("TILES"); await expect(product).toHaveClass(/selected/); await expect(product).toHaveText("Product Col", { timeout: 10_000 }); await page.keyboard.press("Escape"); // A category row: focuses the panel, moves its cursor and activates the // row — three things from one click, and the index must be the row's own. await page.keyboard.press("Shift+C"); await expect(page.locator("#categories")).toBeVisible(); await page.keyboard.press("Escape"); const productRow = page.locator("#categories li").filter({ hasText: "Product" }); await expect(page.locator("#categories li.selected")).toHaveText(/_Index/); await productRow.click(); await expect(page.locator("#mode")).toHaveText("CATEGORIES"); await expect(page.locator("#categories li.selected")).toHaveText(/Product/); await expect(page.locator("#categories li.item")).toHaveText([ "Espresso", "Latte", "Cold Brew", "Pastry", ]); }); });