a8737f3fa5
improvise-5xbq: resolve::page_rows is now the one home of the page-scroll rule; the TUI's Move calls it instead of repeating visible_rows * 3 / 4. A lockstep case pages (PgDn/PgUp, Ctrl-d/Ctrl-u) on a non-square window, 8 rows by 3 columns, and pins the TUI's landing rows as well as comparing host and client, since one home moves both ends together. Both faults the ticket injected (page_rows = window_rows / 2; paging by window_cols) now fail at the first PgDn, before and after the move to one home. improvise-1c5m: the help overlay draws the mode's own hint (hint_line, the string #hint carries) in a sticky footer, so a narrow window whose app bar wraps under #help still says how to page. Playwright case at 600px, red at base, and red again with the footer made non-sticky. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
162 lines
7.5 KiB
JavaScript
162 lines
7.5 KiB
JavaScript
// 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 `<dl>` 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);
|
|
});
|
|
});
|