9156b48f12
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
164 lines
6.9 KiB
JavaScript
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\)/);
|
|
});
|
|
});
|