d1ce2ecbc3
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
69 lines
3.0 KiB
JavaScript
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");
|
|
});
|
|
});
|