// 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, and it must have REMOVED rows: Upland's // two regions are absent while Inland's two are here. Without this, a // page that drew the indicator and ignored the frame would pass. const labels = await page.locator("#grid tbody tr th[scope=row]").allTextContents(); expect(labels).toContain("East"); expect(labels).toContain("West"); expect(labels).not.toContain("Ridge"); expect(labels).not.toContain("Valley"); // 4 regions visible x 2 stores. Stated as a count as well, because a row // drawn with empty labels would satisfy the four assertions above. await expect(page.locator("#grid tbody tr").filter({ hasNot: page.locator("th.group") })).toHaveCount( 8, ); }); });