// The help overlay: the rows are `improvise_protocol::help`'s, and the keys in // them came off the keymap (improvise-ktv1). PR #61 deleted the spec that said // so and nothing replaced it (improvise-78v6); what replaced it said only that // a row EXISTED, which a row drawn blank satisfies (improvise-igxp). import { test, expect } from "@playwright/test"; async function openHelp(page) { await page.goto("/app/index.html"); await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 }); await page.locator("#grid").focus(); await page.keyboard.press("?"); await expect(page.locator("#help")).toBeVisible(); await expect(page.locator(".help-tab.active")).toHaveText("Welcome"); } /// The `
` read as the pairs it is: a `dt` and the `dd` that follows it, /// which is the association the page encodes in document order and nowhere /// else. Reading `dt` and `dd` as two lists would zip prose rows (which have /// no `dd`) against the wrong descriptions and still look tidy. const pairs = (page) => page.locator(".help-body").evaluate((dl) => [...dl.children] .filter((el) => el.tagName === "DT") .map((dt) => ({ kind: dt.className, keys: dt.textContent, description: dt.nextElementSibling?.tagName === "DD" ? dt.nextElementSibling.textContent : null, })), ); test.describe("the help overlay draws the shared content", () => { // improvise-igxp. Every assertion here is about TEXT: `toBeVisible` on a // `dt` passes against an empty one, which is the whole failure this exists // to catch — a page drawing the right NUMBER of blank rows. test("? opens help, and its rows carry the keys and the words, not empty frames", async ({ page, }) => { await openHelp(page); // The five pages are `help::pages()`; the page spells no title. await expect(page.locator(".help-tab")).toHaveText([ "Welcome", "Navigation", "Editing", "Panels & Views", "Commands", ]); // Welcome is prose end to end: every row is a `dt` with words in it and // NO `dd` beside it — the rule `paintHelp` encodes as "a prose row has no // description and spans both columns". An empty `dd` per prose row would // leave a blank grid cell, and nothing but a count can see that. const welcome = await pairs(page); expect(welcome.length).toBeGreaterThan(20); expect(welcome.every((row) => row.description === null)).toBe(true); expect(welcome.filter((row) => row.kind === "help-banner")).toEqual([ { kind: "help-banner", keys: " improvise", description: null }, ]); expect(welcome.filter((row) => row.kind === "help-heading").map((r) => r.keys)).toEqual([ "Quick start", "Core concepts", ]); // A blank row is the ONE row that is legitimately empty, so the // "nothing is blank" rule below has to be stated over the others. expect( welcome.some((row) => row.kind === "help-blank" && row.keys === ""), ).toBe(true); expect( welcome.filter((row) => row.kind !== "help-blank").every((row) => row.keys.trim() !== ""), ).toBe(true); await page.keyboard.press("l"); await expect(page.locator(".help-tab.active")).toHaveText("Navigation"); const navigation = await pairs(page); // The keys rows, which are the ones with a `dd`. `keys_for` drops a row // whose command is unbound, so a row that is HERE claims a real binding. const keyed = navigation.filter((row) => row.kind === "help-keys"); expect(keyed.length).toBeGreaterThan(10); // The injection improvise-igxp names: blank `keys.textContent` in // `paintHelp`. Every one of these is a string off the keymap. expect(keyed.every((row) => row.keys.trim() !== "")).toBe(true); expect(keyed.every((row) => (row.description ?? "").trim() !== "")).toBe(true); // And the pairing itself, for three rows whose keys are the keymap's: // `h j k l` and the arrows are four bindings compacted into one row, and // a page that drew keys of its own would not know to. const described = (text) => keyed.find((row) => row.description === text)?.keys; expect(described("Move one cell")).toBe("h j k l ← ↑ → ↓"); expect(described("Next page-axis item")).toBe("]"); expect(described("Drill into selected cell")).toBe(">"); await page.keyboard.press("Escape"); await expect(page.locator("#help")).toBeHidden(); await expect(page.locator("#mode")).toHaveText("NORMAL"); }); // The assertions above are still about a string on a screen. This one asks // whether the string is TRUE: the key the overlay advertises is read out of // the DOM and then pressed, so a page drawing a plausible key it does not // have — the exact failure a hand-spelled table decays into — is caught // without this spec holding a second copy of the keymap. test("a key the overlay advertises does what the overlay says it does", async ({ page, }) => { await openHelp(page); await page.keyboard.press("l"); await expect(page.locator(".help-tab.active")).toHaveText("Navigation"); const rows = await pairs(page); const advertised = rows.find((row) => row.description === "Jump to last row")?.keys; // Not `toBeTruthy`: an empty string is falsy and would read as "the row // is missing", which is a different fault with a different repair. expect(advertised, "no row advertises jump-to-last-row").not.toBe(undefined); expect(advertised.trim(), "the row is drawn, and drawn blank").not.toBe(""); await page.keyboard.press("Escape"); await expect(page.locator("#help")).toBeHidden(); const rows_ = page.locator("#grid tbody tr"); await expect(rows_.first().locator("td.selected")).toHaveCount(1); await expect(rows_.last().locator("td.selected")).toHaveCount(0); // One binding per row is the compaction (`h j k l` is four), so press // the first key the row names rather than the whole string. await page.keyboard.press(advertised.trim().split(/\s+/)[0]); await expect(rows_.last().locator("td.selected")).toHaveCount(1); await expect(page.locator("#grid tbody td.selected")).toHaveCount(1); }); // improvise-1c5m. Below the width at which the app bar wraps, `#help` covers // `#hint`, and Welcome itself says nothing about paging. The contract: the // mode's own hint is UNCOVERED somewhere while help is open. test("a narrow window still shows how to page through help", async ({ page }) => { await page.setViewportSize({ width: 600, height: 800 }); await openHelp(page); // The string is the page's own `#hint`, i.e. `hint_line(Help)`; this spec // spells no keys. Asserting it names paging keeps an empty hint out. const hint = (await page.locator("#hint").textContent()).trim(); expect(hint).toContain("next-page"); // An element showing exactly that string, topmost at its own centre: the // overlay covering `#hint` is precisely what `elementFromPoint` sees. const uncovered = await page.evaluate((text) => [...document.querySelectorAll("body *")].some((node) => { if (node.children.length > 0 || node.textContent.trim() !== text) return false; const r = node.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const top = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2); return top !== null && (top === node || node.contains(top)); }), hint); expect(uncovered, "the paging hint is under the help overlay").toBe(true); }); });