955c30a864
Repair-forward for the accidental push at 959f249, which put the PRE-review
version of the browser-widget work onto main. The three defects review found
are all in the version main currently carries; this is the delta that
supersedes it.
1. `categoryList` did not call `indicator()`. It inlined U+25B8/U+25BE, the
SMALL triangles, where `indicator()` — the page's declared ONE spelling —
the page's own grid group headers, and src/ui/category_panel.rs all draw
U+25B6/U+25BC. The browser's category panel disagreed with the TUI's and
with its own grid, inside the file whose comment says "only this spelling
is the page's".
2. Two specs pinned the divergent pair, so the obvious cure would have failed
tests that shipped with the feature. widgets.spec.js AND pointer.spec.js —
the latter written on a different branch, which had grown the same wrong
assertions independently. Three files converging on a spelling nothing
compares is design-principles §5's predicted failure, observed. Proved
discriminating by running the corrected specs against the pre-fix bundle,
where they fail with `Received string: "▸ Product (4) R"`.
The pointer.spec.js line is in this PR and not a later one because main
already carries that file: without it this change leaves CI red. It is one
assertion corrected by the same rule as its sibling, not feature work.
3. `el.tiles.hidden` was a no-op, removed with its false comment. `#tiles`
declares `display: flex`, an author-origin rule that beats the UA sheet's
`[hidden] { display: none }`, so the guard never hid anything. Worth
knowing before restoring it: `#tiles` being ALWAYS laid out is why its
height never changes and it stays out of the ResizeObserver feedback path.
Also `playwright.config.js` takes IMPROVISE_HARNESS_PORT (default 8099, so CI
is unchanged). The hard-coded port plus `reuseExistingServer` let Playwright
silently ADOPT a server left by another worktree and test THAT tree. It fired
in both directions in one session: a false negative (a fix reported absent)
and — the dangerous half — a false POSITIVE, 5 passed against a tree verified
to contain no #tiles at all. A failure sends you looking; a false pass prompts
nobody.
The evidence prose records all of it, plus the ResizeObserver acyclicity proof
that retires "argued, not tested" there: frameCost.cols is row header LEVELS
(independent of projected rows) and frameCost.rows derives from the column
window, so cols -> rows and nothing depends on rows. No cycle to oscillate.
Gates: 1359 Rust tests, rustfmt, comment-run and evidence linters, 21/21
Chromium harness (on a private port, so not another checkout's tree).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
141 lines
5.8 KiB
JavaScript
141 lines
5.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();
|
|
}
|
|
|
|
/// Open the category panel the way a user does, and leave the keyboard in
|
|
/// Normal — so a later click has a focus change to make.
|
|
async function openPanelThenLeave(page) {
|
|
await page.keyboard.press("Shift+C");
|
|
await expect(page.locator("#categories")).toBeVisible();
|
|
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.
|
|
test("the sidebar is hidden until C, and C hides it again", async ({ page }) => {
|
|
await openApp(page);
|
|
await expect(page.locator("#categories")).toBeHidden();
|
|
|
|
await page.keyboard.press("Shift+C");
|
|
await expect(page.locator("#categories")).toBeVisible();
|
|
await expect(page.locator("#mode")).toHaveText("CATEGORIES");
|
|
await expect(page.locator("#categories li").first()).toBeVisible();
|
|
|
|
// The same key closes it, because `toggle-panel-and-focus` closes only
|
|
// when the panel is already both open AND focused.
|
|
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 tile bar is keyboard-reachable only through `T` plus arrows; a click
|
|
// is the whole interaction, so it selects AND cycles.
|
|
test("a click selects the tile and cycles its axis", async ({ page }) => {
|
|
await openApp(page);
|
|
const product = page.locator("#tiles .tile").filter({ hasText: "Product" });
|
|
await expect(product).toHaveText("Product Row");
|
|
|
|
await product.click();
|
|
|
|
await expect(page.locator("#mode")).toHaveText("TILES");
|
|
await expect(product).toHaveClass(/selected/);
|
|
// 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.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/);
|
|
});
|
|
});
|