feat: add data table and release checks
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user