import { resolve } from "node:path";
import { describe, expect, it, vi } from "vitest";
import { render, fireEvent } from "@testing-library/preact";
import { Table } from "./Table.js";
const columns = [
{ key: "name", header: "Name", sortable: true },
{ key: "role", header: "Role" },
{ key: "status", header: "Status", sortable: true },
];
const data = [
{ name: "Alice", role: "Engineer", status: "Active" },
{ name: "Bob", role: "Designer", status: "Away" },
];
describe("Table", () => {
// ── Rendering ──
it("renders a table element", () => {
const { container } = render(
);
expect(container.querySelector("table")).toBeTruthy();
});
it("renders column headers", () => {
const { getByText } = render();
expect(getByText("Name")).toBeTruthy();
expect(getByText("Role")).toBeTruthy();
expect(getByText("Status")).toBeTruthy();
});
it("renders data rows", () => {
const { getByText } = render();
expect(getByText("Alice")).toBeTruthy();
expect(getByText("Bob")).toBeTruthy();
});
it("renders correct number of cells", () => {
const { container } = render();
const cells = container.querySelectorAll(".strand-table__td");
// 2 rows x 3 columns = 6 cells
expect(cells.length).toBe(6);
});
it("renders correct number of header cells", () => {
const { container } = render();
const headers = container.querySelectorAll(".strand-table__th");
expect(headers.length).toBe(3);
});
// ── Sorting ──
it("renders sort button for sortable columns", () => {
const { container } = render();
const sortButtons = container.querySelectorAll(".strand-table__sort-btn");
// "Name" and "Status" are sortable
expect(sortButtons.length).toBe(2);
});
it("does not render sort button for non-sortable columns", () => {
const nonSortable = [{ key: "role", header: "Role" }];
const { container } = render(
,
);
const sortButtons = container.querySelectorAll(".strand-table__sort-btn");
expect(sortButtons.length).toBe(0);
});
it("calls onSort with key and asc direction on first click", () => {
const onSort = vi.fn();
const { container } = render(
,
);
const sortButtons = container.querySelectorAll(".strand-table__sort-btn");
fireEvent.click(sortButtons[0]);
expect(onSort).toHaveBeenCalledWith("name", "asc");
});
it("toggles sort direction on second click of same column", () => {
const onSort = vi.fn();
const { container } = render(
,
);
const sortButtons = container.querySelectorAll(".strand-table__sort-btn");
fireEvent.click(sortButtons[0]); // asc
fireEvent.click(sortButtons[0]); // desc
expect(onSort).toHaveBeenLastCalledWith("name", "desc");
});
// ── Responsive ──
it("wraps table in overflow-x scroll container", () => {
const { container } = render();
const wrapper = container.querySelector(".strand-table-wrapper");
expect(wrapper).toBeTruthy();
});
// ── Custom className ──
// ── Empty state ──
it("says so, in one full-width cell, when there are no rows", () => {
const { container, getByText } = render();
const cell = getByText("Nothing yet");
expect(cell.getAttribute("colspan")).toBe(String(columns.length));
expect(container.querySelectorAll(".strand-table__row").length).toBe(1);
});
it("names itself for assistive technology through a hidden caption, or shows a visible one", () => {
const { container, rerender } = render();
const caption = container.querySelector("caption")!;
expect(caption.textContent).toBe("People");
expect(caption.className).toContain("strand-sr-only");
rerender(Visible caption} />);
expect(container.querySelector("caption")!.className).not.toContain("strand-sr-only");
});
it("announces the sorted column and direction on the header cell", () => {
const { container, getByLabelText } = render();
const th = container.querySelector("th")!;
expect(th.getAttribute("aria-sort")).toBe("none");
fireEvent.click(getByLabelText("Sort by Name"));
expect(th.getAttribute("aria-sort")).toBe("ascending");
fireEvent.click(getByLabelText("Sort by Name"));
expect(th.getAttribute("aria-sort")).toBe("descending");
expect(container.querySelectorAll("th[aria-sort]").length).toBe(columns.filter((c) => c.sortable).length);
});
it("shows the sort its owner holds when the sort is controlled", () => {
const onSort = vi.fn();
const { container, getByLabelText, rerender } = render();
expect(container.querySelector("th")!.getAttribute("aria-sort")).toBe("descending");
fireEvent.click(getByLabelText("Sort by Name"));
expect(onSort).toHaveBeenCalledWith("name", "asc");
expect(container.querySelector("th")!.getAttribute("aria-sort")).toBe("descending");
rerender();
expect(container.querySelector("th")!.getAttribute("aria-sort")).toBe("none");
});
it("renders a cell from the whole row when the column asks", () => {
const cols = [{ key: "name", header: "Name", render: (row: Record) => {String(row.name).toUpperCase()} }];
const { container } = render();
expect(container.querySelector("td strong")!.textContent).toBe("ADA");
});
it("keys rows by the field or function it is given", () => {
const rows = [{ id: "b", name: "Bee" }, { id: "a", name: "Ay" }];
const { container, rerender } = render();
const first = container.querySelector("tbody tr")!;
rerender();
// The node that was first is now second: keyed rows move rather than being rewritten in place.
expect(container.querySelectorAll("tbody tr")[1]).toBe(first);
});
// ── Column width ──
it("applies width style to column headers", () => {
const cols = [{ key: "name", header: "Name", width: "200px" }];
const { container } = render();
const th = container.querySelector(".strand-table__th") as HTMLElement;
expect(th?.style.width).toBe("200px");
});
// ── Sort indicator ──
it("shows sort indicator on sortable columns", () => {
const { container } = render();
const indicators = container.querySelectorAll(
".strand-table__sort-indicator",
);
expect(indicators.length).toBe(2);
});
});
import { snapshotFixtures } from "../../test/snapshot.js";
import { snapshotStylesheet } from "../../test/stylesheet.js";
import { fixtures } from "./Table.fixtures.js";
snapshotFixtures(Table, fixtures);
snapshotStylesheet(resolve(__dirname, "./Table.css"));