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>
143 lines
6.5 KiB
JavaScript
143 lines
6.5 KiB
JavaScript
// The three widgets a user reported as "not working": the category panel's
|
|
// expand, the tile bar and the view list. History: context/test-evidence/clients.md.
|
|
|
|
import { test, expect } from "@playwright/test";
|
|
|
|
/// Open the demo document and put the keyboard where the page listens.
|
|
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();
|
|
}
|
|
|
|
test.describe("the category panel", () => {
|
|
// Expanding is resolved client-side and has always worked by KEYBOARD; the
|
|
// panel is nevertheless the one of the three that has DOM, so this pins it.
|
|
test("Enter on a category header reveals its items", async ({ page }) => {
|
|
await openApp(page);
|
|
const rows = page.locator("#categories li");
|
|
|
|
await page.keyboard.press("Shift+C");
|
|
await expect(page.locator("#mode")).toHaveText("CATEGORIES");
|
|
// web/demo.improv: _Index, _Dim, _Measure, Product, Quarter. Product is
|
|
// the first with items a user would recognise. The glyphs are
|
|
// `indicator()`'s, which is the page's ONE spelling and the TUI's too.
|
|
for (let i = 0; i < 3; i++) await page.keyboard.press("j");
|
|
await expect(rows.filter({ hasText: "Product" })).toHaveText(/▶ Product \(4\)/);
|
|
|
|
await page.keyboard.press("Enter");
|
|
await expect(rows.filter({ hasText: "Product" })).toHaveText(/▼ Product \(4\)/);
|
|
await expect(page.locator("#categories li.item")).toHaveText([
|
|
"Espresso",
|
|
"Latte",
|
|
"Cold Brew",
|
|
"Pastry",
|
|
]);
|
|
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.locator("#categories li.item")).toHaveCount(0);
|
|
});
|
|
});
|
|
|
|
test.describe("the tile bar", () => {
|
|
// Nothing was DRAWN: TileBarCache was folded, `T` entered the mode and the
|
|
// axis keys reached the host, with no tile bar on screen to aim them at.
|
|
test("the tiles are drawn, and tile-select moves a visible cursor", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
const tiles = page.locator("#tiles .tile");
|
|
await expect(tiles).toHaveCount(5);
|
|
// Axis labels are `Axis::short_label`: three columns, so Page is "Pag",
|
|
// and None is "·" — an answer rather than a truncation (gotcha 26).
|
|
await expect(tiles.filter({ hasText: "Product" })).toHaveText("Product Row");
|
|
await expect(tiles.filter({ hasText: "Quarter" })).toHaveText("Quarter Pag");
|
|
await expect(tiles.filter({ hasText: "_Index" })).toHaveText("_Index ·");
|
|
|
|
// Out of tile select nothing is highlighted, as the TUI's widget decides.
|
|
await expect(page.locator("#tiles .tile.selected")).toHaveCount(0);
|
|
|
|
await page.keyboard.press("Shift+T");
|
|
await expect(page.locator("#mode")).toHaveText("TILES");
|
|
await expect(tiles.nth(0)).toHaveClass(/selected/);
|
|
await page.keyboard.press("ArrowRight");
|
|
await expect(tiles.nth(1)).toHaveClass(/selected/);
|
|
await expect(page.locator("#tiles .tile.selected")).toHaveCount(1);
|
|
});
|
|
|
|
// The axis change was reaching the host all along; what it could not do was
|
|
// show the result. Setting Product to Column must redraw the tile AND the grid.
|
|
test("setting a tile's axis is visible in the bar it was aimed at", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
const product = page.locator("#tiles .tile").filter({ hasText: "Product" });
|
|
await expect(product).toHaveText("Product Row");
|
|
|
|
await page.keyboard.press("Shift+T");
|
|
for (let i = 0; i < 3; i++) await page.keyboard.press("ArrowRight");
|
|
await expect(product).toHaveClass(/selected/);
|
|
// Product's four items are the grid's ROW headers to begin with.
|
|
const rowHeaders = page.locator("#grid tbody th");
|
|
await expect(rowHeaders).toHaveText(["Espresso", "Latte", "Cold Brew", "Pastry"]);
|
|
|
|
await page.keyboard.press("c");
|
|
await expect(product).toHaveText("Product Col", { timeout: 10_000 });
|
|
// The tile label is not the point on its own: the grid must have moved
|
|
// too, or the bar would be reporting an axis the document does not have.
|
|
// `.first()`: Product now shares the column axis with _Measure, so the
|
|
// label appears once per header level. That it appears at all is the point.
|
|
await expect(
|
|
page.locator("#grid thead th").filter({ hasText: "Espresso" }).first(),
|
|
).toBeVisible();
|
|
await expect(rowHeaders).not.toHaveText(["Espresso", "Latte", "Cold Brew", "Pastry"]);
|
|
});
|
|
});
|
|
|
|
test.describe("the view panel", () => {
|
|
// Nothing was DRAWN: ViewListCache held both views and `V` entered ViewPanel
|
|
// mode, so Enter switched views off a cursor the user could not see.
|
|
test("V lists the views, marks the active one, and Enter switches", async ({
|
|
page,
|
|
}) => {
|
|
await openApp(page);
|
|
await expect(page.locator("#views")).toBeHidden();
|
|
await expect(page.locator("#view-name")).toHaveText("Plan");
|
|
|
|
await page.keyboard.press("Shift+V");
|
|
await expect(page.locator("#mode")).toHaveText("VIEWS");
|
|
await expect(page.locator("#views")).toBeVisible();
|
|
// web/demo.improv carries two views; the axis line is the shared
|
|
// `view_axis_summary`, so its absence would be a second derivation.
|
|
await expect(page.locator("#views li")).toHaveCount(2);
|
|
await expect(page.locator("#views li").nth(0)).toContainText("Default");
|
|
await expect(page.locator("#views li").nth(0).locator(".axes")).toHaveText(
|
|
"R:Product C:Quarter",
|
|
);
|
|
await expect(page.locator("#views li.active")).toContainText("Plan");
|
|
await expect(page.locator("#views li.selected")).toContainText("Default");
|
|
|
|
await page.keyboard.press("Enter");
|
|
await expect(page.locator("#view-name")).toHaveText("Default", { timeout: 10_000 });
|
|
// Switching returns to Normal but leaves the panel OPEN, exactly as
|
|
// `src/draw.rs` keeps drawing it off `view_panel_open` alone. The active
|
|
// marker is what must move, and it is the fact the list exists to carry.
|
|
await expect(page.locator("#mode")).toHaveText("NORMAL");
|
|
await expect(page.locator("#views")).toBeVisible();
|
|
await expect(page.locator("#views li.active")).toContainText("Default");
|
|
});
|
|
});
|
|
|
|
test.describe("the formula panel", () => {
|
|
// The third of `Panel::ALL`, equally undrawn and fixed by the same mechanism.
|
|
test("F lists the document's formulas", async ({ page }) => {
|
|
await openApp(page);
|
|
await expect(page.locator("#formulas")).toBeHidden();
|
|
|
|
await page.keyboard.press("Shift+F");
|
|
await expect(page.locator("#mode")).toHaveText("FORMULAS");
|
|
await expect(page.locator("#formulas li")).toHaveText(["Profit = Revenue - Cost"]);
|
|
await expect(page.locator("#formulas li.selected")).toHaveCount(1);
|
|
});
|
|
});
|