import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { Button } from "./button"; import { DataTable, type DataTableColumn } from "./data-table"; type ReleaseRow = { id: string; lane: string; note: string; owner: string; risk: number; status: string; }; const rows: ReleaseRow[] = [ { id: "legal", lane: "Legal", note: "Footnote needs one more pass before the customer note goes out.", owner: "Ava", risk: 3, status: "Pending" }, { id: "support", lane: "Support", note: "Macro pack is staged and aligned with the migration narrative.", owner: "Nia", risk: 1, status: "Ready" }, { id: "engineering", lane: "Engineering", note: "Canary checks are green and ready for the 10% wave.", owner: "Mika", risk: 2, status: "Watching" }, { id: "editorial", lane: "Editorial", note: "Copy is locked and the public note stays staged.", owner: "Jun", risk: 4, status: "Staged" } ]; const columns: DataTableColumn[] = [ { accessor: "lane", header: "Lane", id: "lane", sortable: true }, { accessor: "owner", header: "Owner", id: "owner", sortable: true }, { accessor: "status", header: "Status", id: "status" }, { accessor: "risk", align: "end", header: "Risk", id: "risk", sortable: true }, { cell: (row) => row.note, header: "Narrative", id: "note", searchValue: (row) => row.note } ]; function getBodyRows() { return screen .getAllByRole("row") .filter((row) => row.closest("tbody") !== null); } function getRenderedLanes() { return getBodyRows().map((row) => within(row).getAllByRole("cell")[0].textContent); } describe("DataTable", () => { it("renders semantic table slots, toolbar search, and row actions", () => { render( } rows={rows.slice(0, 2)} toolbarActions={} /> ); expect(screen.getByRole("table").closest('[data-slot="root"]')).toBeInTheDocument(); expect(screen.getByRole("searchbox", { name: "Search rows" })).toHaveAttribute( "data-slot", "input" ); expect( screen.getByRole("button", { name: "Create lane" }).closest('[data-slot="actions"]') ).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: /lane/i })).toHaveAttribute( "data-slot", "header" ); expect( screen.getByRole("button", { name: "Open Legal" }).closest('[data-slot="actions"]') ).toBeInTheDocument(); }); it("sorts rows when a sortable header is activated", async () => { const user = userEvent.setup(); render(); expect(getRenderedLanes()).toEqual(["Legal", "Support", "Engineering"]); await user.click(within(screen.getByRole("columnheader", { name: /risk/i })).getByRole("button")); expect(getRenderedLanes()).toEqual(["Support", "Engineering", "Legal"]); await user.click(within(screen.getByRole("columnheader", { name: /risk/i })).getByRole("button")); expect(getRenderedLanes()).toEqual(["Legal", "Engineering", "Support"]); }); it("supports row selection and shows a bulk selection surface", async () => { const user = userEvent.setup(); render( } /> ); await user.click(screen.getByRole("checkbox", { name: "Select row 1" })); expect(getBodyRows()[0]).toHaveAttribute("data-selected", ""); expect(screen.getByText("1 selected")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Assign owner" })).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Clear selection" })); expect(screen.queryByText("1 selected")).not.toBeInTheDocument(); }); it("filters rows from the built-in search and falls back to the empty state", async () => { const user = userEvent.setup(); render(); await user.type(screen.getByRole("searchbox", { name: "Search rows" }), "migration"); expect( screen.getByText("Macro pack is staged and aligned with the migration narrative.") ).toBeInTheDocument(); expect( screen.queryByText("Canary checks are green and ready for the 10% wave.") ).not.toBeInTheDocument(); await user.clear(screen.getByRole("searchbox", { name: "Search rows" })); await user.type(screen.getByRole("searchbox", { name: "Search rows" }), "missing"); expect(screen.getByText("No matching rows")).toBeInTheDocument(); expect( screen.getByText( "Try another search, clear filters, or create a new record to repopulate this view." ) ).toBeInTheDocument(); }); it("paginates rows and updates the visible range", async () => { const user = userEvent.setup(); render( ); expect(screen.getByText("1-2 of 4")).toBeInTheDocument(); expect(getRenderedLanes()).toEqual(["Legal", "Support"]); expect(screen.getByRole("button", { name: "Previous" })).toBeDisabled(); await user.click(screen.getByRole("button", { name: "Next" })); expect(screen.getByText("3-4 of 4")).toBeInTheDocument(); expect(getRenderedLanes()).toEqual(["Engineering", "Editorial"]); expect(screen.getByRole("button", { name: "Next" })).toBeDisabled(); }); it("renders loading status without dropping the table chrome", () => { render(); expect(screen.getByRole("status")).toHaveTextContent("Loading rows"); expect(screen.getByRole("columnheader", { name: /lane/i })).toBeInTheDocument(); expect(screen.getByRole("table").closest('[data-slot="root"]')).toHaveAttribute( "data-loading", "" ); }); it("supports controlled sorting and selection callbacks", async () => { const user = userEvent.setup(); const onSortingChange = vi.fn(); const onSelectionChange = vi.fn(); render( ); await user.click(within(screen.getByRole("columnheader", { name: /lane/i })).getByRole("button")); await user.click(screen.getByRole("checkbox", { name: "Select row 2" })); expect(onSortingChange).toHaveBeenCalledWith([{ desc: false, id: "lane" }]); expect(onSelectionChange).toHaveBeenCalledWith({ support: true }); }); it("toggles hideable columns from the built-in view menu", async () => { const user = userEvent.setup(); render( ); expect(screen.getByRole("columnheader", { name: /owner/i })).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "View" })); await user.click(screen.getByRole("menuitemcheckbox", { name: "Owner" })); expect(screen.queryByRole("columnheader", { name: /owner/i })).not.toBeInTheDocument(); expect(screen.queryByText("Ava")).not.toBeInTheDocument(); }); it("switches density from the built-in view menu", async () => { const user = userEvent.setup(); render(); const root = screen.getByRole("table").closest('[data-slot="root"]'); expect(root).toHaveAttribute("data-density", "comfortable"); await user.click(screen.getByRole("button", { name: "View" })); await user.click(screen.getByRole("menuitemradio", { name: "Compact" })); expect(root).toHaveAttribute("data-density", "compact"); expect(screen.getByRole("columnheader", { name: /lane/i })).toHaveAttribute( "data-density", "compact" ); }); it("opens row details inside a sheet", async () => { const user = userEvent.setup(); render(
{row.note}
} rowDetailsDescription={(row) => `${row.lane} handoff`} rowDetailsTitle={(row) => `${row.lane} detail`} rows={rows.slice(0, 2)} /> ); await user.click(screen.getAllByRole("button", { name: "Open details" })[0]); const detailHeading = await screen.findByText("Legal detail"); const sheet = detailHeading.closest('[data-slot="content"]'); expect(detailHeading).toBeInTheDocument(); expect(screen.getByText("Legal handoff")).toBeInTheDocument(); expect( within(sheet as HTMLElement).getByText( "Footnote needs one more pass before the customer note goes out." ) ).toBeInTheDocument(); }); });