Files
improvise/harness/tests/pages.spec.js
T
Edward Langley 9156b48f12 fix: the page menu must not claim a modified key (ChatGPT Codex, PR #123)
The menu's early return sat ABOVE `app.js`'s `metaKey` guard, so with the menu
open `Cmd-R` reached `menuKey`, was answered `ignore`, and was then defaulted
away by `if (!e.ctrlKey && !e.altKey) e.preventDefault()` — true for Meta.
Reload was blocked. `Ctrl-j`/`Alt-Home` had the mirror fault and MOVED the menu,
two lines below a comment saying Ctrl and Alt stay the browser's.

Ordering plus one bail: the `metaKey` guard goes back above the menu branch,
`pageMenuKeydown` returns on `ctrlKey || altKey`, and what remains claims
unconditionally — every unmodified key, `ignore` included, which is the rule the
menu exists to keep.

The harness assertion installs its `defaultPrevented` probe AFTER load so it
runs second and sees what the page decided, and ends on an unmodified `j`
asserting `["j", true]` — without which "nothing was prevented" is satisfied by
a page that claims nothing. Proved by restoring the reviewed code: it fails on
`Control+j` moving the active row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:36:43 -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");
});
// 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\)/);
});
});