import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { render } from "@testing-library/preact"; import { Grid } from "./Grid.js"; describe("Grid", () => { // ── Rendering ── it("renders a div element", () => { const { container } = render(content); expect(container.firstElementChild?.tagName).toBe("DIV"); }); it("renders children in grid", () => { const { getByText } = render(
Item 1
Item 2
Item 3
, ); expect(getByText("Item 1")).toBeTruthy(); expect(getByText("Item 2")).toBeTruthy(); expect(getByText("Item 3")).toBeTruthy(); }); // ── Display ── // ── Columns ── it("defaults to 1 column in inline style", () => { const { container } = render(content); const el = container.firstElementChild as HTMLElement; expect(el.style.gridTemplateColumns).toBe("repeat(1, minmax(0, 1fr))"); }); it("applies custom column count in inline style", () => { const { container } = render(content); const el = container.firstElementChild as HTMLElement; expect(el.style.gridTemplateColumns).toBe("repeat(3, minmax(0, 1fr))"); }); it("applies 4-column layout", () => { const { container } = render(content); const el = container.firstElementChild as HTMLElement; expect(el.style.gridTemplateColumns).toBe("repeat(4, minmax(0, 1fr))"); }); // ── Auto-fit (minColWidth) ── it("renders a responsive auto-fit track when minColWidth is set", () => { const { container } = render(content); const el = container.firstElementChild as HTMLElement; expect(el.style.gridTemplateColumns).toBe( "repeat(auto-fit, minmax(220px, 1fr))", ); }); it("lets minColWidth win over columns (auto-fit, not fixed count)", () => { const { container } = render( content , ); const el = container.firstElementChild as HTMLElement; expect(el.style.gridTemplateColumns).toBe( "repeat(auto-fit, minmax(220px, 1fr))", ); }); it("keeps the gap alongside an auto-fit track", () => { const { container } = render( content , ); const el = container.firstElementChild as HTMLElement; expect(el.style.gridTemplateColumns).toBe( "repeat(auto-fit, minmax(220px, 1fr))", ); expect(el.style.gap).toBe("var(--strand-space-3)"); }); // ── Gap ── it("applies default gap as inline style", () => { const { container } = render(content); const el = container.firstElementChild as HTMLElement; expect(el.style.gap).toBe("var(--strand-space-4)"); }); it("applies custom gap as inline style", () => { const { container } = render(content); const el = container.firstElementChild as HTMLElement; expect(el.style.gap).toBe("var(--strand-space-8)"); }); // ── Custom className ── // ── Props forwarding ── it("forwards additional props", () => { const { container } = render( content , ); expect(container.firstElementChild).toHaveAttribute("id", "g1"); }); // ── Sidebar preset ── // // The library made this decision three times privately (.strand-ref-shell // at 256px, .strand-ref-example at 200px, .strand-ref-taxonomy__list at // 160px) and published none of them. Its GEOMETRY -- 264px beside a // flexible track at desktop, one column below md -- is asserted in the // layout tier, because jsdom neither lays out nor resolves media queries. it("renders the sidebar preset as a class, not an inline template", () => { // The column definition changes at a breakpoint and an inline style // cannot carry a media query, so emitting one here would produce a // declaration the stylesheet then has to fight at every width. const { container } = render(); const grid = container.querySelector(".strand-grid") as HTMLElement; expect(grid.classList.contains("strand-grid--sidebar")).toBe(true); expect(grid.style.gridTemplateColumns).toBe(""); }); it("keeps the gap when the sidebar preset owns the columns", () => { const { container } = render(); const grid = container.querySelector(".strand-grid") as HTMLElement; expect(grid.style.gap).toBe("var(--strand-space-6)"); }); it("lets the sidebar preset win over columns and minColWidth", () => { // A sidebar layout is a statement about the TRACKS rather than about // how many of them there are, so a stale `columns` must not leak an // inline template that overrides the preset. const { container } = render(); const grid = container.querySelector(".strand-grid") as HTMLElement; expect(grid.style.gridTemplateColumns).toBe(""); expect(grid.classList.contains("strand-grid--sidebar")).toBe(true); }); it("does not apply the preset unless asked", () => { const { container } = render(); const grid = container.querySelector(".strand-grid") as HTMLElement; expect(grid.classList.contains("strand-grid--sidebar")).toBe(false); expect(grid.style.gridTemplateColumns).toBe("repeat(2, minmax(0, 1fr))"); }); it("renders the split preset as a class, not an inline template", () => { const { container } = render(); const grid = container.querySelector(".strand-grid") as HTMLElement; expect(grid.classList.contains("strand-grid--split")).toBe(true); expect(grid.style.gridTemplateColumns).toBe(""); }); it("lets the split preset win over columns and minColWidth", () => { const { container } = render(); expect( (container.querySelector(".strand-grid") as HTMLElement).style.gridTemplateColumns, ).toBe(""); }); it("does not apply the split preset unless asked", () => { const { container } = render(); expect( container.querySelector(".strand-grid")?.classList.contains("strand-grid--split"), ).toBe(false); }); }); // ── Gap #114: the grid was clipping its children's hover affordance ── // // THESE READ THE STYLESHEET, and that is the only tier that can see this. // Clipping is a PAINT operation: it changes no box, so `getBoundingClientRect` // is identical either way and the browser layout tier is as blind to it as // jsdom. The regression this guards against is a one-word edit, so the source // is where it has to be caught. Same instrument, same reason, as gap #113's // clipped-not-hidden label guard. describe("gap ladder", () => { // ── The spacing ladder (gap #122) ── it("an off-ladder gap resolves to a real token instead of an undefined one", () => { // The defect: `gap={7}` wrote `gap: var(--strand-space-7)` inline, the // token does not exist, and an undefined custom property invalidates the // WHOLE declaration. The grid rendered with no gap. const { container } = render(x); const style = (container.firstElementChild as HTMLElement).getAttribute("style") || ""; expect(style).toContain("--strand-space-6"); expect(style).not.toContain("--strand-space-7"); }); it("an on-ladder gap is untouched, so no existing consumer moves", () => { const { container } = render(x); const style = (container.firstElementChild as HTMLElement).getAttribute("style") || ""; expect(style).toContain("--strand-space-6"); }); }); import { snapshotFixtures } from "../../test/snapshot.js"; import { snapshotStylesheet } from "../../test/stylesheet.js"; import { fixtures } from "./Grid.fixtures.js"; snapshotFixtures(Grid, fixtures); snapshotStylesheet(resolve(__dirname, "./Grid.css"));