Files
improvise/harness/tests/pages.spec.js
T
Edward Langley 260f6e0b48 test: the page-menu spec reads the demo under the decimal default
`harness/tests/pages.spec.js` arrived on `main` with #123 while this branch was
open, and asserts `toHaveText("20,300")` against `web/demo.improv`. The merge
was clean and the combination was wrong — design-principles §7's third case
exactly: `git merge-tree` answers "can git combine these texts", not "does the
combination do what both sides meant". CI caught it on the merge ref, which is
the only place the two sides had ever been run together.

The assertion is unchanged in what it discriminates: it exists so that a menu
moving only the label fails, and `"20,300.00"` separates Q4 from Q1's
`"18,400.00"` exactly as the old literal did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:59:35 -07:00

164 lines
6.9 KiB
JavaScript

// The page bar as a control: the arrow, the menu, and the keys that reach it.
// The RULES behind each decision are `node --test web/tests` — clients.md.
import { test, expect } from "@playwright/test";
/// The shipped demo, focused. `#grid` must have focus or the page's keydown
/// listener never sees a key and every key assertion would pass vacuously.
async function theDemo(page) {
await page.goto("/app/index.html");
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
await page.locator("#grid").focus();
}
const chip = (page) => page.locator("#pages .page-axis");
const menu = (page) => page.locator("#page-menu");
test.describe("a page axis can be picked from a menu", () => {
// improvise-rw6l. The demo has Quarter on Page, so the chip is navigable and
// the arrow is the affordance that says a menu is behind it.
test("the chip carries a down-arrow and opens a menu of every item", async ({
page,
}) => {
await theDemo(page);
await expect(chip(page)).toHaveText(/Quarter = Q1 \(1\/4\)/);
// A <button> and not a <span>: the affordance is the element as much as
// the glyph, and a Filter axis gets neither.
await expect(chip(page)).toHaveAttribute("aria-haspopup", "listbox");
await expect(chip(page).locator(".page-arrow")).toHaveText("▾");
await expect(menu(page)).toBeHidden();
await chip(page).click();
await expect(menu(page)).toBeVisible();
await expect(menu(page).locator(".page-item")).toHaveText([
"✓ Q1",
" Q2",
" Q3",
" Q4",
]);
// Which item the document is fixed to is MARKED, so the menu says where
// you are as well as where you may go.
await expect(menu(page).locator(".page-item.selected")).toHaveText("✓ Q1");
});
// The whole point: the picked item reaches the host and the grid it filters
// changes. Nothing on the page moves the bar itself.
test("picking an item moves the page axis and the numbers under it", async ({
page,
}) => {
await theDemo(page);
const firstCell = page.locator("#grid td").first();
const q1 = await firstCell.textContent();
await chip(page).click();
await menu(page).getByText("Q4", { exact: false }).click();
await expect(chip(page)).toHaveText(/Quarter = Q4 \(4\/4\)/);
await expect(menu(page)).toBeHidden();
// Espresso Revenue is 18,400 in Q1 and 20,300 in Q4, so a menu that moved
// only the label would leave this unchanged.
await expect(firstCell).not.toHaveText(q1 ?? "");
await expect(firstCell).toHaveText("20,300.00");
});
// The menu is operable without a mouse, and while it is open it owns the
// keyboard: a grid key must not reach the grid behind it.
test("the menu is driven by the keyboard and does not leak keys to the grid", async ({
page,
}) => {
await theDemo(page);
await chip(page).click();
await expect(menu(page)).toBeVisible();
// `j` is the grid's own "move down" and here it moves the MENU. If it
// leaked, the selection under the menu would move as well.
const selected = page.locator("#grid td.selected");
const before = await selected.textContent();
await page.keyboard.press("j");
await expect(menu(page).locator(".page-item.active")).toHaveText(" Q2");
await expect(selected).toHaveText(before ?? "");
await page.keyboard.press("Enter");
await expect(menu(page)).toBeHidden();
await expect(chip(page)).toHaveText(/Quarter = Q2 \(2\/4\)/);
});
// Found by ChatGPT Codex on PR #123: the menu branch sat ABOVE the page's
// `metaKey` guard, so Cmd-R was answered `ignore` and then defaulted away.
test("a modified key is the browser's, and the menu neither acts on it nor claims it", async ({
page,
}) => {
await theDemo(page);
// Registered AFTER `app.js`'s own window listener, so it runs second and
// reads what the page did — a listener added first would see `false` always.
await page.evaluate(() => {
window.__claimed = [];
window.addEventListener("keydown", (e) =>
window.__claimed.push([e.key, e.defaultPrevented]),
);
});
await chip(page).click();
await expect(menu(page)).toBeVisible();
await expect(menu(page).locator(".page-item.active")).toHaveText("✓ Q1");
// `Ctrl-j` is not `j`: it must not move the menu the way the bare key does.
await page.keyboard.press("Control+j");
await expect(menu(page).locator(".page-item.active")).toHaveText("✓ Q1");
await page.keyboard.press("Meta+r");
await expect(menu(page)).toBeVisible();
const claimed = await page.evaluate(() => window.__claimed);
expect(claimed, `nothing reached the handler: ${JSON.stringify(claimed)}`).not.toEqual([]);
for (const [key, prevented] of claimed) {
expect(prevented, `${key} was defaulted away; it is the browser's`).toBe(false);
}
// The precondition the rows above lean on: an UNMODIFIED key still is
// claimed, so "nothing was prevented" is a fact about the modifier.
await page.keyboard.press("j");
await expect(menu(page).locator(".page-item.active")).toHaveText(" Q2");
const after = await page.evaluate(() => window.__claimed.at(-1));
expect(after).toEqual(["j", true]);
});
test("Escape closes the menu and changes nothing", async ({ page }) => {
await theDemo(page);
await chip(page).click();
await expect(menu(page)).toBeVisible();
await page.keyboard.press("Escape");
await expect(menu(page)).toBeHidden();
await expect(chip(page)).toHaveText(/Quarter = Q1 \(1\/4\)/);
// And the keyboard is the grid's again: `]` is the page key, so the bar
// moving here proves the menu gave it back.
await page.keyboard.press("]");
await expect(chip(page)).toHaveText(/Quarter = Q2 \(2\/4\)/);
});
});
test.describe("the page keys follow the document, not the grid", () => {
// improvise-wiew, end to end: `[`/`]` are on the layer between the global
// and the modes whose subject IS the document, so a panel pages too.
test("a panel is still somewhere the page axis can be moved", async ({ page }) => {
await theDemo(page);
await expect(chip(page)).toHaveText(/Quarter = Q1/);
await page.keyboard.press("C");
await expect(page.locator("#mode")).toHaveText("CATEGORIES");
await page.keyboard.press("]");
await expect(chip(page)).toHaveText(/Quarter = Q2 \(2\/4\)/);
// The key did its own job and no other: the panel is still open.
await expect(page.locator("#mode")).toHaveText("CATEGORIES");
});
// …and the help overlay is not, because it covers the grid it would be
// changing. That is the one case the layer exists to exclude.
test("the help overlay pages nothing behind itself", async ({ page }) => {
await theDemo(page);
await page.keyboard.press("?");
await expect(page.locator("#help")).toBeVisible();
await page.keyboard.press("]");
await expect(page.locator("#help")).toBeVisible();
await expect(chip(page)).toHaveText(/Quarter = Q1 \(1\/4\)/);
});
});