Files
improvise/harness/tests/drill.spec.js
T
Edward Langley a913390322 feat: the page bar becomes a control, and [/] reach the modes about the document
Three tickets on one subject: paging.

improvise-rw6l — clicking a page chip opens a menu of that axis's items, with
a down-arrow saying so. Almost nothing had to be derived: `PageAxis::items` was
already projected and `ModelCommand::SetPageSelection` was already on the wire.
The one gap was that `RenderPageAxis` dropped the names, keeping the position
and count improvise-p1ym needed for `[`/`]`. `items` is relayed INSIDE `cycle`
rather than beside it, so there is one predicate and not two: a Filter axis has
no cycle, therefore no arrow, no menu, no items, and composes nothing if a click
reaches it — all four off `PageAxis::cycle`, the one home of gotcha 27's split.

improvise-771e — the browser tile bar said "Quarter Pag". `short_label` is three
characters so the TUI aligns in a fixed-width terminal (gotcha 26); a browser has
no column to fit. `render.rs` asks `plain_label` instead; `short_label` is
untouched. Note what pinned the truncation was `harness/tests/drill.spec.js`,
not `tests/grid_frame_agreement.rs`, which does not mention the tile bar.

improvise-wiew — `[`/`]` move from Normal onto a new `document_keymap` between
the global and the modes whose subject IS the document (Normal, the three
panels, TileSelect; RecordsNormal through Normal). Help and the import wizard
take the global directly. Per the user's ruling: an overlay has a subject of its
own, and these are the first shared keys that MUTATE the document — under the
help overlay that is an invisible change to the grid it is covering.

Boundary crossings to flag, both outside the brief's stated write scope and
neither a wire change:
  - `resolve.rs` gains `Click::PageItem` and `lib.rs` a `click_page_item`
    binding. Composing the command anywhere else would put a derivation in the
    page, which design-principles §5 forbids; `Command`/`ModelCommand` are
    untouched.
  - `improvise-command/src/command/keymap.rs` gains three tests. The two
    existing guards were the mechanism that forced the wiew argument, so they
    had to be answered where they live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:24:38 -07:00

79 lines
3.6 KiB
JavaScript

// Drill in the browser: what it does today, which is refuse. improvise-gd28
// says why, and context/test-evidence/clients.md records the layer it stops at.
import { test, expect } from "@playwright/test";
/// `web/demo.improv` has NO aggregate cell by construction — gotcha 46 puts
/// exactly one category on each visible axis so every cell is a Record, and
/// `tests/web_demo_model.rs` asserts it. So a drill test must MAKE one first.
async function anAggregateGrid(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();
// Quarter is on Page; one cycle takes it to None, so every cell aggregates
// over the four quarters. The tile bar is how a user would do it.
await page.keyboard.press("Shift+T");
const tiles = page.locator("#tiles .tile");
const quarter = tiles.filter({ hasText: "Quarter" });
await expect(quarter).toHaveText("Quarter Page");
const names = await tiles.allTextContents();
for (let i = 0; i < names.findIndex((t) => t.startsWith("Quarter")); i++) {
await page.keyboard.press("ArrowRight");
}
await expect(quarter).toHaveClass(/selected/);
await page.keyboard.press("Enter");
await expect(quarter).toHaveText("Quarter None", { timeout: 10_000 });
await page.keyboard.press("Escape");
// The precondition, asserted rather than assumed: nothing is editable now,
// which is `CellId::Aggregate` reaching `render.rs::editable` (ADR 002).
await expect(page.locator("#grid td.editable")).toHaveCount(0);
await expect(page.locator("#grid tbody td").first()).toBeVisible();
}
test.describe("drill is refused in the browser, not broken", () => {
// The honest half: `i`/`a` on an aggregate declines OUT LOUD, because
// `edit_or_drill` answers the `CellId::Aggregate` arm with a status.
test("editing an aggregate says drill needs the host", async ({ page }) => {
await anAggregateGrid(page);
await page.keyboard.press("i");
await expect(page.locator("#status")).toContainText(
"drilling into an aggregate needs the host",
);
// And it really is a refusal: no editor opened, no mode change.
await expect(page.locator("#mode")).toHaveText("NORMAL");
await expect(page.locator("#grid input")).toHaveCount(0);
});
// `>` is the key the hint line advertises as ":drill", and it used to be a
// name `resolve::step` did not handle — so `KeyOutcome::Upstream` ->
// `ClientFrame::Key` -> `Host::handle`'s empty vec, a black hole rather than
// a slow path (gotcha 47). It now answers. Interim until improvise-gd28.
test("the drill key says drill is unavailable, not something about aggregates", async ({
page,
}) => {
await anAggregateGrid(page);
await page.keyboard.press(">");
await expect(page.locator("#status")).toContainText(
"drill is not available in the browser yet",
);
await expect(page.locator("#mode")).toHaveText("NORMAL");
// The two refusals have two SUBJECTS, and conflating them would be a
// true-sounding sentence about the wrong thing — worse than silence,
// because it stops the reader looking. `>` must not talk about aggregates.
await expect(page.locator("#status")).not.toContainText("aggregate");
// And `i` still gives the OTHER refusal, which is what proves the two are
// distinct rather than one message reached by two keys.
await page.keyboard.press("i");
await expect(page.locator("#status")).toContainText(
"drilling into an aggregate needs the host",
);
});
});