Files
cadence-ui/packages/ui/src/components/data-table.test.tsx
T

320 lines
9.3 KiB
TypeScript

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<ReleaseRow>[] = [
{
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(
<DataTable
columns={columns}
renderRowActions={(row) => <Button size="sm">Open {row.lane}</Button>}
rows={rows.slice(0, 2)}
toolbarActions={<Button size="sm" variant="secondary">Create lane</Button>}
/>
);
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(<DataTable columns={columns} rows={rows.slice(0, 3)} />);
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(
<DataTable
columns={columns}
enableSelection
rows={rows.slice(0, 2)}
selectionActions={() => <Button size="sm">Assign owner</Button>}
/>
);
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(<DataTable columns={columns} rows={rows.slice(0, 3)} />);
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(
<DataTable
columns={columns}
defaultPageSize={2}
pageSizeOptions={[2]}
rows={rows}
/>
);
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(<DataTable columns={columns} loading rows={rows} />);
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(
<DataTable
columns={columns}
enableSelection
onSelectionChange={onSelectionChange}
onSortingChange={onSortingChange}
rows={rows.slice(0, 2)}
/>
);
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(
<DataTable
columns={[
{
accessor: "lane",
header: "Lane",
hideable: false,
id: "lane"
},
{
accessor: "owner",
header: "Owner",
hideable: true,
id: "owner"
}
]}
rows={rows.slice(0, 2)}
/>
);
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(<DataTable columns={columns} rows={rows.slice(0, 2)} />);
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(
<DataTable
columns={columns}
renderRowDetails={(row) => <div>{row.note}</div>}
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();
});
});