2026-03-29 21:17:56 +07:00

175 lines
5.9 KiB
TypeScript

import { fireEvent, render, screen } from "@solidjs/testing-library";
import { describe, expect, it, vi } from "vitest";
import { DataTable, DataTableColumnSchema, DataTableMeta, DataTableRootPropsSchema } from "../../../src/components/data-table/index";
const data = [
{ id: 1, name: "Alice", age: 30 },
{ id: 2, name: "Bob", age: 25 },
{ id: 3, name: "Charlie", age: 35 },
];
const columns = [
{ id: "name", header: "Name", cell: (row: typeof data[0]) => row.name, sortable: true },
{ id: "age", header: "Age", cell: (row: typeof data[0]) => String(row.age), sortable: true },
];
function TestTable(props: { pageSize?: number; enableRowSelection?: boolean }) {
return (
<DataTable data={data} columns={columns} pageSize={props.pageSize ?? 10} enableRowSelection={props.enableRowSelection}>
<table>
<DataTable.Header />
<DataTable.Body />
</table>
<DataTable.Pagination />
</DataTable>
);
}
describe("DataTable — rendering", () => {
it("renders all rows by default", () => {
render(() => <TestTable />);
expect(screen.getByText("Alice")).toBeTruthy();
expect(screen.getByText("Bob")).toBeTruthy();
expect(screen.getByText("Charlie")).toBeTruthy();
});
it("renders column headers", () => {
render(() => <TestTable />);
expect(screen.getByText("Name")).toBeTruthy();
expect(screen.getByText("Age")).toBeTruthy();
});
it("renders pagination controls", () => {
render(() => <TestTable />);
expect(screen.getByText("Previous")).toBeTruthy();
expect(screen.getByText("Next")).toBeTruthy();
expect(screen.getByText(/Page 1 of/)).toBeTruthy();
});
});
describe("DataTable — sorting", () => {
it("sorts ascending on first header click", () => {
render(() => <TestTable />);
fireEvent.click(screen.getByText("Name"));
const cells = screen.getAllByRole("cell");
const names = cells.filter((_, i) => i % 2 === 0).map((c) => c.textContent);
expect(names[0]).toBe("Alice");
expect(names[1]).toBe("Bob");
expect(names[2]).toBe("Charlie");
});
it("sorts descending on second header click", () => {
render(() => <TestTable />);
const nameHeader = screen.getByRole("columnheader", { name: /Name/ });
fireEvent.click(nameHeader);
fireEvent.click(nameHeader);
const cells = screen.getAllByRole("cell");
const names = cells.filter((_, i) => i % 2 === 0).map((c) => c.textContent);
expect(names[0]).toBe("Charlie");
expect(names[1]).toBe("Bob");
expect(names[2]).toBe("Alice");
});
it("resets sort on third header click", () => {
render(() => <TestTable />);
const nameHeader = screen.getByRole("columnheader", { name: /Name/ });
fireEvent.click(nameHeader);
fireEvent.click(nameHeader);
fireEvent.click(nameHeader);
expect(nameHeader.getAttribute("aria-sort")).toBe("none");
});
it("header has aria-sort=ascending when sorted asc", () => {
render(() => <TestTable />);
const nameHeader = screen.getByRole("columnheader", { name: /Name/ });
fireEvent.click(nameHeader);
expect(nameHeader.getAttribute("aria-sort")).toBe("ascending");
});
});
describe("DataTable — pagination", () => {
it("shows only pageSize rows per page", () => {
render(() => <TestTable pageSize={2} />);
const cells = screen.getAllByRole("cell");
expect(cells.length).toBe(4);
});
it("next page shows remaining rows", () => {
render(() => <TestTable pageSize={2} />);
fireEvent.click(screen.getByText("Next"));
expect(screen.getByText("Charlie")).toBeTruthy();
});
it("previous button is disabled on first page", () => {
render(() => <TestTable pageSize={2} />);
const prev = screen.getByText("Previous") as HTMLButtonElement;
expect(prev.disabled).toBe(true);
});
it("next button is disabled on last page", () => {
render(() => <TestTable pageSize={2} />);
fireEvent.click(screen.getByText("Next"));
const next = screen.getByText("Next") as HTMLButtonElement;
expect(next.disabled).toBe(true);
});
it("shows correct page label", () => {
render(() => <TestTable pageSize={2} />);
expect(screen.getByText("Page 1 of 2")).toBeTruthy();
});
});
describe("DataTable — row selection", () => {
it("clicking a row marks it selected", () => {
render(() => <TestTable enableRowSelection />);
const rows = screen.getAllByRole("row");
fireEvent.click(rows[1]);
expect(rows[1].getAttribute("aria-selected")).toBe("true");
});
it("clicking selected row deselects it", () => {
render(() => <TestTable enableRowSelection />);
const rows = screen.getAllByRole("row");
fireEvent.click(rows[1]);
fireEvent.click(rows[1]);
expect(rows[1].getAttribute("aria-selected")).toBe("false");
});
it("onSelectionChange is called", () => {
const onChange = vi.fn();
render(() => (
<DataTable data={data} columns={columns} enableRowSelection onSelectionChange={onChange}>
<table>
<DataTable.Header />
<DataTable.Body />
</table>
<DataTable.Pagination />
</DataTable>
));
const rows = screen.getAllByRole("row");
fireEvent.click(rows[1]);
expect(onChange).toHaveBeenCalledWith(expect.any(Set));
});
});
describe("DataTable — schema and meta", () => {
it("DataTableColumnSchema validates a valid column", () => {
const result = DataTableColumnSchema.safeParse({ id: "name", header: "Name", sortable: true });
expect(result.success).toBe(true);
});
it("DataTableRootPropsSchema validates pageSize", () => {
const result = DataTableRootPropsSchema.safeParse({ pageSize: 20 });
expect(result.success).toBe(true);
});
it("DataTableMeta has correct name", () => {
expect(DataTableMeta.name).toBe("DataTable");
});
it("DataTableMeta lists required parts", () => {
expect(DataTableMeta.requiredParts).toContain("Root");
expect(DataTableMeta.requiredParts).toContain("Body");
});
});