Files
improvise/harness/tests/keys.spec.js
T
Edward Langley d1ce2ecbc3 fix(web): an advertised key is never silent, a formula is visible, and +cat commits
improvise-rmvx, improvise-fdt6, improvise-gcvb — three faces of the same
thing: a browser offering something and then doing nothing.

rmvx. `the_command_names_that_still_need_the_host_are_these` was built with
`in_mode(AppMode::Normal)`, so it only ever asked about Normal-mode bindings:
`add-record-row` (RecordsNormal) and `delete-formula-at-cursor` (FormulaPanel)
were outside its reach entirely and could never have failed it. A named list
that silently scopes itself to one mode is a guard that looks broader than it
is. `every_advertised_command_answers_in_its_own_mode` crosses
`help::advertised` — command NAMES, so no hint string is tokenised — against
`resolve` in every mode, over a fixture furnished enough that an `Upstream` can
only mean an unhandled name. It found a seventh instance the ticket had not
predicted: `view-back` is advertised in RecordsNormal.

Six names now decline out loud, each with its own true subject, out of one
`declined` table; `stranding` returns the subject rather than a bool, so `:`
says the command line is unavailable instead of doing nothing. A declining step
takes its whole `Binding::Sequence` with it, which is what keeps `o` ("add a
record row, then edit it") from opening an editor on a row nobody created.

fdt6. `commit-buffer formula` already resolved; what did not exist was any way
to SEE the typing. `RenderModel::editing` is scoped to the two cell-editing
modes and reads the "edit" buffer by name, so FormulaEdit, CategoryAdd, ItemAdd
and the three prompts opened and swallowed every character. `RenderModel::
minibuffer` is `AppMode::minibuffer()` plus the buffer that config names, read
through `improvise_protocol::read_buffer` so gotcha 11 keeps its one home, and
`web/app.js` paints it.

gcvb. The records-mode early return was broader than its reason. A
`CellId::Create` commits the typed NAME as one existing `ModelCommand`, reads
no `record_col` and writes no cell, so the return narrows rather than
disappears. `+cat` must NOT advance: an inserted column renumbers every index
to its right and the advance was computed from the pre-commit layout, so a
client that advanced anyway would land one column off, silently. Both halves
proved by injection against `CommitAndAdvance` in `tests/lockstep.rs`.

Every new guard was proved by injecting the fault it names. The one worth
repeating: `web/tests/render_surface.test.js` went green over a page that
painted nothing, because the string `model.minibuffer` was inside a function
nothing called. The static check is a completeness check, never an execution
check — `harness/tests/keys.spec.js` caught it in Chromium on the first run.

1394 → 1397 Rust tests; harness 17 → 19 passed, 3 skipped; node 26.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 16:36:03 -07:00

69 lines
3.0 KiB
JavaScript

// Keys the page ADVERTISES, executed. Two properties: a key this client cannot
// compose declines out loud (improvise-rmvx), and the one text-entry room the
// browser could enter but never showed you typing in (improvise-fdt6).
import { test, expect } from "@playwright/test";
/// The shipped demo, focused, with a grid on screen. `#grid` must have focus
/// or the page's keydown listener never sees a key and every assertion below
/// would pass or fail for a reason that has nothing to do with resolution.
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();
await expect(page.locator("#grid td.editable").first()).toBeVisible();
}
test.describe("an advertised key is never silent", () => {
// Each of these is named in the mode's own hint line, so silence read as
// broken rather than as unavailable (gotcha 47). The subjects must differ:
// one message reached by three keys would tell the user nothing.
test("the three Normal-mode keys this client cannot compose each say their own subject", async ({
page,
}) => {
await theDemo(page);
for (const [key, said] of [
["P", "hiding empty rows and columns is not available in the browser yet"],
["R", "records mode is not available in the browser yet"],
[":", "the command line is not available in the browser yet"],
]) {
// A stale status would let the NEXT key pass on the previous key's
// message, so each assertion is preceded by a state it is not.
await expect(page.locator("#status")).not.toContainText(said);
await page.keyboard.press(key);
await expect(page.locator("#status")).toContainText(said);
// A refusal that changed the mode would be a door opened anyway.
await expect(page.locator("#mode")).toHaveText("NORMAL");
}
});
});
test.describe("a formula can be composed in the browser", () => {
// improvise-fdt6. `commit-buffer formula` resolved here already; what did
// not exist was any way to SEE the typing, so the room swallowed it.
test("the formula minibuffer shows what is typed and the commit reaches the panel", async ({
page,
}) => {
await theDemo(page);
await expect(page.locator("#minibuffer")).toBeHidden();
await page.keyboard.press("F");
await expect(page.locator("#mode")).toHaveText("FORMULAS");
await page.keyboard.press("n");
await expect(page.locator("#mode")).toHaveText("FORMULA");
// The property: every character is visible while it is being typed.
await expect(page.locator("#minibuffer")).toBeVisible();
await page.keyboard.type("Margin = Profit");
await expect(page.locator("#minibuffer")).toContainText("Margin = Profit");
await page.keyboard.press("Enter");
await expect(page.locator("#minibuffer")).toBeHidden();
// And it reached the DOCUMENT, not just the buffer: the panel is a
// projection the host sends back.
await expect(page.locator("#formulas")).toContainText("Margin = Profit");
});
});