import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { Metric, MetricStrip } from "./metric-strip"; describe("MetricStrip", () => { it("pairs each label with its value as a described term", () => { render( , ); // The label is wrapped for truncation, so assert the term it belongs to. expect(screen.getByText("Balance").closest("dt")).not.toBeNull(); expect(screen.getByText("$248.55").tagName).toBe("DD"); }); // axe's `definition-list` rule allows only dt, dd, script, template and div // as direct children of a dl. The hint is a second dd for this reason: a

// between the terms is a violation, and it was one. it("puts only permitted elements directly inside the dl", () => { const { container } = render( , ); const dl = container.querySelector("dl") as HTMLElement; const permitted = new Set(["DT", "DD", "SCRIPT", "TEMPLATE", "DIV"]); for (const child of Array.from(dl.children)) { expect(permitted).toContain(child.tagName); } }); it("renders the hint as a dd rather than a paragraph", () => { render( , ); const hint = screen.getByText("Personal wallet"); expect(hint.tagName).toBe("DD"); }); it("omits the hint entirely when not given", () => { const { container } = render( , ); expect(container.querySelectorAll("dd")).toHaveLength(1); }); // `attention` means a person has to do something, so it shows a pill next to // the label. A zero on its own is not attention. it("shows a pill beside the label only when attention is set", () => { const { rerender, container } = render( , ); expect(container.querySelector("svg")).toBeNull(); rerender( , ); expect(screen.getByText("Empty")).toBeInTheDocument(); expect(container.querySelector("svg")).not.toBeNull(); }); it("raises the value's tone only for a danger attention", () => { const { container } = render( , ); const value = screen.getByText("$0.00"); expect(value.className).toContain("--surface-danger-text"); expect(container.querySelectorAll("dd")).toHaveLength(1); }); // The divider is drawn only where it belongs, never drawn and then // suppressed: `border-l` alongside `border-l-0` variants puts both in one // conflict group whose winner is decided by emitted-rule order, and that // resolved the wrong way at every breakpoint — every item kept a left border, // including the first in each row. it("draws the divider without a suppressing counterpart", () => { const { container } = render( , ); const item = (container.querySelector("dl") as HTMLElement) .children[0] as HTMLElement; expect(item.className).not.toContain("border-l-0"); }); // The row-start test has to name the same column count the grid does, at each // breakpoint. Nothing in the type system ties them together, so changing the // grid to three columns without moving the divider puts a rule mid-row. Read // both out of the rendered classes and require them to agree, so the coupling // is enforced rather than only described. it("keeps the divider's row-start test matching the grid's column count", () => { const { container } = render( , ); const dl = container.querySelector("dl") as HTMLElement; const item = dl.children[0] as HTMLElement; // `grid-cols-N` unprefixed is the base breakpoint; `:grid-cols-N` is that // breakpoint. The divider spells the same breakpoints as `max-:` / `:`. const columnsAt = (prefix: string) => { const re = prefix ? new RegExp(`(?:^| )${prefix}:grid-cols-(\\d+)(?: |$)`) : /(?:^| )grid-cols-(\d+)(?: |$)/; const m = dl.className.match(re); return m ? Number(m[1]) : null; }; const modulusAt = (prefix: string) => { const m = item.className.match( new RegExp(`(?:^| )${prefix}:\\[&:not\\(:nth-child\\((\\d+)n\\+1\\)\\)\\]:border-l(?: |$)`), ); return m ? Number(m[1]) : null; }; // base grid (below sm) is guarded by the `max-sm:` divider rule expect(modulusAt("max-sm")).toBe(columnsAt("")); // the `sm:` grid is guarded by the `sm:` divider rule expect(modulusAt("sm")).toBe(columnsAt("sm")); // Every grid-cols breakpoint the strip declares must have a divider rule. const declared = [...dl.className.matchAll(/(?:^| )(?:([a-z]+):)?grid-cols-\d+(?= |$)/g)] .map((m) => m[1] ?? ""); for (const bp of declared) { const guard = bp === "" ? "max-sm" : bp; expect( modulusAt(guard), `grid-cols at "${bp || "base"}" has no matching ${guard}: divider rule`, ).not.toBeNull(); } }); it("titles the hint too, so a truncated qualifier stays recoverable", () => { render( , ); expect(screen.getByText("Personal wallet (0x1234abcd5678efgh)")).toHaveAttribute( "title", "Personal wallet (0x1234abcd5678efgh)", ); }); it("titles a string value so a truncated figure stays readable", () => { render( , ); expect(screen.getByText("$1,284,003.10")).toHaveAttribute( "title", "$1,284,003.10", ); }); });