50e4a37214
Four user rulings and three review findings, all in the pointer's one resolver. improvise-hkr8 — a click is all of its intents or none of them. Each `click_*` now returns `Option<Vec<Command>>` and `click` does the single `unwrap_or_default`. The `None` that matters is `BasisCache::claim` answering that no basis has been published: the click used to keep its navigation prefix, drop its activation and report success, while the keyboard handled the identical `None` correctly as `KeyOutcome::Upstream`. USER RULING — a tile click SELECTS; a second click on the already-selected tile CYCLES. Every click in the UI is now non-mutating by default. The argument that earns the two keystrokes is not tidiness: a cycle is `LayoutGeometry`, so it bumps `cursor_epoch` and resets EVERY connected client's cursor (gotcha 24) — the easiest input to trigger by accident was wired to the one class of change that reaches other people. The divergence from `Enter`, which still cycles in one keystroke, is chosen and recorded. "Already selected" is mode AND index, because `tile_cat_idx` is 0 before anything selects anything and tile 0 would otherwise cycle on first click. USER RULING — the category panel is open at launch, as the shared `ViewState::default()` rather than a browser special case. A browser-only default is "the page overrides the flag", which is the same defect as the "the page ignores the flag" bug improvise-wsaw fixed. This changes the TUI too: `src/draw.rs` draws the panel off the same field. Three review findings, all "a guard that would not catch the drift it is named for": - The formula-row `assert!` could never fail — the `assert_eq!` above it pins the same vector and aborts first. Replaced by an assertion aimed at the keymap itself, which is where the fact lives; the reviewer had shown that adding `Enter` to FormulaPanel left 1381 tests green while the click and the keyboard disagreed. - `click_view_row` re-spelled `SwitchView + ChangeMode` inline instead of sharing a helper, so a change forced into the keyboard arm by lockstep left the click behind silently. Both now call `switch_view_at`. - `context/test-evidence/clients.md` documented the behaviour PR #59 ships as one of the injected faults that disproved it, and had no entry for #59's new test. Both fixed; the checker's one-directional blind spot that hid it is filed as improvise-lyyc. Every change is proved by injecting the fault: six injections, six distinct failure signatures, none passing for another's reason. Gates: 1389 workspace tests (was 1381), clippy -D warnings, fmt, harness fmt, all eight wasm crates, 27/27 Chromium harness, 189 cited ticket IDs resolve, comment runs and evidence headings clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
234 lines
9.8 KiB
JavaScript
234 lines
9.8 KiB
JavaScript
// The pointer: improvise-7ybz (a click resolves in Rust) and improvise-wsaw
|
|
// (the sidebar obeys its flag). History: context/test-evidence/clients.md.
|
|
|
|
import { test, expect } from "@playwright/test";
|
|
|
|
async function openApp(page) {
|
|
await page.goto("/app/index.html");
|
|
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
|
|
await page.locator("#grid").focus();
|
|
}
|
|
|
|
/// Leave the panel open and the keyboard in Normal — so a later click has a
|
|
/// focus change to make. It opens at launch, so `C` here only focuses it.
|
|
async function openPanelThenLeave(page) {
|
|
await expect(page.locator("#categories")).toBeVisible();
|
|
await page.keyboard.press("Shift+C");
|
|
await expect(page.locator("#mode")).toHaveText("CATEGORIES");
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.locator("#mode")).toHaveText("NORMAL");
|
|
await expect(page.locator("#categories")).toBeVisible();
|
|
}
|
|
|
|
test.describe("the category sidebar obeys category_panel_open", () => {
|
|
// improvise-wsaw. The flag existed from the start and app.js never read it,
|
|
// so the sidebar was drawn always and `C` changed only the mode label.
|
|
// The launch state is the user's later ruling; the toggle is wsaw's. Both
|
|
// are asserted, because either alone passes against the bug wsaw fixed.
|
|
test("the sidebar is open at launch, C focuses it, and C again hides it", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
await expect(page.locator("#categories")).toBeVisible();
|
|
await expect(page.locator("#categories li").first()).toBeVisible();
|
|
await expect(page.locator("#mode")).toHaveText("NORMAL");
|
|
|
|
// Open but not focused, so the first press focuses rather than closes.
|
|
await page.keyboard.press("Shift+C");
|
|
await expect(page.locator("#categories")).toBeVisible();
|
|
await expect(page.locator("#mode")).toHaveText("CATEGORIES");
|
|
|
|
// And now it closes — which is the half that proves the page reads the
|
|
// flag rather than drawing the sidebar unconditionally.
|
|
await page.keyboard.press("Shift+C");
|
|
await expect(page.locator("#categories")).toBeHidden();
|
|
await expect(page.locator("#mode")).toHaveText("NORMAL");
|
|
});
|
|
});
|
|
|
|
test.describe("clicking the category panel", () => {
|
|
// improvise-7ybz. One click means three things — focus the panel, move its
|
|
// cursor, activate the row — and all three are resolved in Rust.
|
|
test("a click focuses the panel, moves the cursor and expands the row", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
await openPanelThenLeave(page);
|
|
|
|
const product = page.locator("#categories li").filter({ hasText: "Product" });
|
|
await expect(product).toHaveText(/▶ Product \(4\)/);
|
|
await expect(page.locator("#categories li.selected")).toHaveText(/_Index/);
|
|
|
|
await product.click();
|
|
|
|
// All three, and the third is what the user reported as impossible.
|
|
await expect(page.locator("#mode")).toHaveText("CATEGORIES");
|
|
await expect(page.locator("#categories li.selected")).toHaveText(/Product/);
|
|
await expect(product).toHaveText(/▼ Product \(4\)/);
|
|
await expect(page.locator("#categories li.item")).toHaveText([
|
|
"Espresso",
|
|
"Latte",
|
|
"Cold Brew",
|
|
"Pastry",
|
|
]);
|
|
|
|
// Clicking it again collapses: the click mirrors Enter, which is a toggle.
|
|
await product.click();
|
|
await expect(page.locator("#categories li.item")).toHaveCount(0);
|
|
});
|
|
|
|
// The other arm of `filter-to-item`: on an ITEM row the same click filters,
|
|
// because the rule is "do what Enter does here" rather than "expand".
|
|
test("a click on an item row filters, as Enter does", async ({ page }) => {
|
|
await openApp(page);
|
|
await openPanelThenLeave(page);
|
|
|
|
await page.locator("#categories li").filter({ hasText: "Product" }).click();
|
|
await page.locator("#categories li.item").filter({ hasText: "Latte" }).click();
|
|
|
|
await expect(page.locator("#status")).toContainText("Filter: Product = Latte");
|
|
await expect(page.locator("#categories li.selected")).toHaveText("Latte");
|
|
});
|
|
});
|
|
|
|
test.describe("clicking a tile", () => {
|
|
// The user's ruling: a click SELECTS, a second click CYCLES. Every click in
|
|
// the UI is non-mutating by default; the cost is two for the common action.
|
|
test("the first click selects the tile and changes nothing", async ({ page }) => {
|
|
await openApp(page);
|
|
const product = page.locator("#tiles .tile").filter({ hasText: "Product" });
|
|
await expect(product).toHaveText("Product Row");
|
|
|
|
await product.click();
|
|
|
|
// The positive half: something did happen, so a green test is not the
|
|
// dead-page shape. `Product` is selected and the mode moved.
|
|
await expect(page.locator("#mode")).toHaveText("TILES");
|
|
await expect(product).toHaveClass(/selected/);
|
|
|
|
// And the negative half: the document did not move. Waited out rather
|
|
// than asserted instantly, since a cycle would arrive asynchronously.
|
|
await page.waitForTimeout(500);
|
|
await expect(product).toHaveText("Product Row");
|
|
await expect(
|
|
page.locator("#grid thead th").filter({ hasText: "Espresso" }),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("a second click on the selected tile cycles its axis", async ({ page }) => {
|
|
await openApp(page);
|
|
const product = page.locator("#tiles .tile").filter({ hasText: "Product" });
|
|
await product.click();
|
|
await expect(product).toHaveClass(/selected/);
|
|
|
|
await product.click();
|
|
|
|
// Row → Column is `Axis::next`; the page does not spell the rotation.
|
|
await expect(product).toHaveText("Product Col", { timeout: 10_000 });
|
|
// And the grid moved with it, not just the label.
|
|
await expect(
|
|
page.locator("#grid thead th").filter({ hasText: "Espresso" }).first(),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("clicking a different tile selects it rather than cycling it", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
const product = page.locator("#tiles .tile").filter({ hasText: "Product" });
|
|
const quarter = page.locator("#tiles .tile").filter({ hasText: "Quarter" });
|
|
await product.click();
|
|
await expect(product).toHaveClass(/selected/);
|
|
|
|
await quarter.click();
|
|
|
|
// The guarantee has to hold for every click, not only a session's first.
|
|
await expect(quarter).toHaveClass(/selected/);
|
|
await page.waitForTimeout(500);
|
|
await expect(quarter).toHaveText("Quarter Pag");
|
|
await expect(product).toHaveText("Product Row");
|
|
});
|
|
});
|
|
|
|
test.describe("clicking a grid cell", () => {
|
|
// Before improvise-7ybz a click reached only `edit()`, so the selection —
|
|
// which every keyboard command reads — stayed wherever it had been.
|
|
test("a click moves the selection, not only the editor", async ({ page }) => {
|
|
await openApp(page);
|
|
const cells = page.locator("#grid tbody td");
|
|
// The cursor starts at the origin, so any other cell is a real move.
|
|
await expect(cells.nth(0)).toHaveClass(/selected/);
|
|
|
|
await cells.nth(4).click();
|
|
// demo.improv is all Records, so the click also opened an editor; close it
|
|
// without committing and the selection must have followed the pointer.
|
|
await page.keyboard.press("Escape");
|
|
|
|
await expect(cells.nth(4)).toHaveClass(/selected/);
|
|
await expect(page.locator("#grid tbody td.selected")).toHaveCount(1);
|
|
});
|
|
|
|
// The selection a click leaves is the one the keyboard then moves from —
|
|
// the property that makes the two input paths one cursor rather than two.
|
|
test("the keyboard continues from where the pointer left the cursor", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
const cells = page.locator("#grid tbody td");
|
|
await cells.nth(4).click();
|
|
await page.keyboard.press("Escape");
|
|
await expect(cells.nth(4)).toHaveClass(/selected/);
|
|
|
|
await page.keyboard.press("ArrowLeft");
|
|
await expect(cells.nth(3)).toHaveClass(/selected/);
|
|
});
|
|
});
|
|
|
|
test.describe("clicking the other two panels", () => {
|
|
// improvise-iioi. The category panel got a listener and its two siblings did
|
|
// not, so a user who had just learned rows are clickable clicked a view row
|
|
// and read the silence as broken — which IS the report behind improvise-7ybz.
|
|
test("a click on a view row focuses, moves the cursor and switches", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
await page.keyboard.press("Shift+V");
|
|
await expect(page.locator("#views")).toBeVisible();
|
|
await expect(page.locator("#view-name")).toHaveText("Plan");
|
|
await expect(page.locator("#views li.active")).toContainText("Plan");
|
|
// Leave the panel open but unfocused, so the focus third is observable.
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.locator("#mode")).toHaveText("NORMAL");
|
|
|
|
await page.locator("#views li").filter({ hasText: "Default" }).click();
|
|
|
|
// Switching ends in Normal — `switch-view-at-cursor` says so — but the
|
|
// cursor and the active marker must both have moved to the clicked row.
|
|
await expect(page.locator("#view-name")).toHaveText("Default", {
|
|
timeout: 10_000,
|
|
});
|
|
await expect(page.locator("#views li.active")).toContainText("Default");
|
|
await expect(page.locator("#views li.selected")).toContainText("Default");
|
|
});
|
|
|
|
// The formula panel binds NO Enter, so the rule yields navigation and
|
|
// nothing else. That is the rule applying, not the fix stopping half way.
|
|
test("a click on a formula row moves the cursor and does not edit", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
await page.keyboard.press("Shift+F");
|
|
await expect(page.locator("#formulas")).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.locator("#mode")).toHaveText("NORMAL");
|
|
|
|
await page.locator("#formulas li").first().click();
|
|
|
|
// Focused and selected — and still FORMULAS, because there is nothing to
|
|
// activate. A mode change here would mean an activation was invented.
|
|
await expect(page.locator("#mode")).toHaveText("FORMULAS");
|
|
await expect(page.locator("#formulas li.selected")).toHaveCount(1);
|
|
await expect(page.locator("#grid input")).toHaveCount(0);
|
|
});
|
|
});
|