Files
improvise/harness/tests/grouped.spec.js
T
Edward Langley 216b14860d improvise-6qet: the grouped harness spec expects the collapsed group's row
grouped.spec.js asserted a collapsed group contributes no rows (8 data rows).
It now contributes one per Store (10), labelled Upland, and its cells are
not editable. The editable assertion was checked against cell_id without
folds_a_group: it fails with 6 editable cells where 0 are expected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:31:06 -07:00

167 lines
7.6 KiB
JavaScript

// The grid paths no shipped document reaches: two categories on an axis, a
// group spanning its members, a collapsed one, and the inline group row that
// carries no cells. This is the first test anywhere that runs improvise-anr9's
// header-level code in a browser (improvise-plcx).
import { test, expect } from "@playwright/test";
/// `harness/fixtures/grouped.improv`, served beside the shipped demo by
/// `harness/build.sh`. A seed that 404s is NOT an error -- `web/worker.js`
/// answers a missing seed with an EMPTY model -- so everything below asserts
/// labels the fixture really carries. "A grid is drawn" would pass on the
/// empty document a typo in the filename produces.
/// The first row that carries DATA. Not `tbody tr` first: since improvise-0o6x
/// a group header first on its axis is drawn, and `Coastal` is exactly that in
/// `grouped.improv` -- so the first row is a header alone, with no `td` at all.
const firstDataRow = (page) =>
page
.locator("#grid tbody tr")
.filter({ hasNot: page.locator("th.group") })
.first();
async function openGrouped(page, doc) {
await page.goto(`/app/index.html?doc=${doc}&seed=grouped.improv`);
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
await expect(page.locator("#view-name")).toHaveText("Grouped");
// The head, waited for with a RETRYING assertion before anything below
// reads the DOM in one shot: a grid painted from the first projection and
// reframed by the viewport it then announces would otherwise be read
// half-drawn, and a one-shot read cannot wait.
await expect(page.locator("#grid thead tr")).toHaveCount(3);
await expect(page.locator("#grid thead th.group").first()).toHaveText("▼ Q1");
await expect(firstDataRow(page).locator("th").last()).toHaveText("Main");
await page.locator("#grid").focus();
}
/// The head as lines of text, group line included: `thead > tr` in order, each
/// as the labels it draws with the corner dropped. Read in one pass, because
/// what these tests are about is the RELATION between the lines.
const headLines = (page) =>
page.locator("#grid thead").evaluate((head) =>
[...head.rows].map((tr) => ({
group: tr.cells[0].className === "group" || tr.cells[1]?.className === "group",
cells: [...tr.cells].slice(1).map((th) => ({
text: th.textContent,
span: th.colSpan,
})),
cornerSpan: tr.cells[0].colSpan,
})),
);
test.describe("a grouped, multi-level document", () => {
// The defect this is aimed at is the one improvise-anr9 fixed: a label tuple
// slash-joined into one string. That draws the RIGHT number of columns with
// the right words in them, so only the SHAPE of the head can see it.
test("two categories on an axis are two header lines, not one slash-joined one", async ({
page,
}) => {
await openGrouped(page, "grouped-levels.improv");
const lines = await headLines(page);
// One group line and one line per column level: Month over Channel.
expect(lines.length).toBe(3);
expect(lines.map((l) => l.group)).toEqual([true, false, false]);
// Two row categories, so the corner spans two — and the row headers below
// must agree with it, or the table is ragged.
expect(lines.every((l) => l.cornerSpan === 2)).toBe(true);
await expect(firstDataRow(page).locator("th")).toHaveCount(2);
// The level lines. The blanks are repeat suppression, decided in Rust by
// `AxisFrame::header_labels` — NOT an absent label, and not the page's
// idea. A slash-join would put "Jan/Web" on ONE line and none of this
// would hold; so would a page that drew only the innermost level.
expect(lines[1].cells.map((c) => c.text)).toEqual(["Jan", "", "Feb", "", "Mar", ""]);
expect(lines[2].cells.map((c) => c.text)).toEqual([
"Web",
"Shop",
"Web",
"Shop",
"Web",
"Shop",
]);
// Stated as well as implied: a joined label would satisfy a length check.
expect(lines.flatMap((l) => l.cells).every((c) => !c.text.includes("/"))).toBe(true);
// The row labels are the same rule on the other axis: North is drawn
// once and its second store's line is blank.
const rowHeads = await page
.locator("#grid tbody tr")
.evaluateAll((rows) =>
rows
.filter((tr) => tr.cells[0].className !== "group")
.map((tr) => [...tr.cells].filter((c) => c.tagName === "TH").map((c) => c.textContent)),
);
expect(rowHeads.slice(0, 4)).toEqual([
["North", "Main"],
["", "Annex"],
["South", "Main"],
["", "Annex"],
]);
});
// A column group header spans exactly the columns under it. The span is
// `col_groups`' run length, and a wrong one is not a wrong word: it is a
// table whose header sits over the wrong data.
test("a column group spans its members, and the spans tile the columns", async ({
page,
}) => {
await openGrouped(page, "grouped-spans.improv");
const lines = await headLines(page);
const groups = lines[0].cells;
// Q1 holds Jan and Feb, each split by Channel: 2 x 2. Q2 holds Mar: 1 x 2.
expect(groups.map((c) => c.text)).toEqual(["▼ Q1", "▼ Q2"]);
expect(groups.map((c) => c.span)).toEqual([4, 2]);
// And they tile: every column is under exactly one group header. A span
// that were merely "big enough" passes the pair above and fails here.
const columns = lines[2].cells.length;
expect(groups.reduce((n, c) => n + c.span, 0)).toBe(columns);
await expect(firstDataRow(page).locator("td")).toHaveCount(columns);
});
// `RenderRow::Group` carries no cells, so the page CANNOT draw one with
// cells — which is worth asserting from outside, because the DOM is where
// that guarantee is cashed and the table goes ragged if it is lost.
test("an inline group row is a header alone, and its indicator reports the collapse", async ({
page,
}) => {
await openGrouped(page, "grouped-collapse.improv");
const groupRows = page.locator("#grid tbody tr", {
has: page.locator("th.group"),
});
// Three, not two: this WAS two, because `Coastal` is the frame's FIRST
// entry and a leading group header was dropped on the way to the page.
// improvise-0o6x fixed it, and this line is the record of when.
await expect(groupRows).toHaveCount(3);
await expect(groupRows).toHaveText([
"▼ Coastal",
"▼ Inland",
"▶ Upland",
]);
// The glyph is `indicator(collapsed)`: a page that hard-coded either one
// passes on one of these rows and fails on the other.
expect(await groupRows.locator("td").count()).toBe(0);
await expect(groupRows.first().locator("th")).toHaveCount(1);
// The collapse is the document's: Upland's two regions are gone and the
// group is ONE row in their place (improvise-6qet, core.md §14).
const labels = await page.locator("#grid tbody tr th[scope=row]").allTextContents();
expect(labels).toContain("East");
expect(labels).toContain("West");
expect(labels).toContain("Upland");
expect(labels).not.toContain("Ridge");
expect(labels).not.toContain("Valley");
// (4 regions + the Upland fold) x 2 stores. A count too, because a row
// drawn with empty labels would satisfy the assertions above.
const dataRows = page.locator("#grid tbody tr").filter({ hasNot: page.locator("th.group") });
await expect(dataRows).toHaveCount(10);
// The fold is an aggregate: no record carries Region=Upland, so nothing
// in its rows may open an editor, where a stored row beside it does.
await expect(dataRows.first().locator("td.editable")).not.toHaveCount(0);
await expect(dataRows.last().locator("td.editable")).toHaveCount(0);
});
});