import { describe, expect, it, vi, afterEach } from "vitest"; import { render, fireEvent, screen } from "@testing-library/react"; import { DataTable, type Column } from "../../src/components/DataTable"; it("keeps a stable column's default sort and focused button when its label changes", () => { const data = [{ id: "a" }, { id: "b" }]; const translatedColumns = (header: string): Column[] => [{ id: "identity", header, cell: (r) => r.id, sortValue: (r) => r.id }]; const view = (header: string) => r.id} selectedKey={null} onSelect={() => {}} emptyLabel="none" defaultSort={{ id: "identity", direction: "desc" }} />; const { container, rerender } = render(view("ID")); const order = () => [...container.querySelectorAll("tbody tr")].map((r) => r.textContent); expect(order()).toEqual(["b", "a"]); const button = screen.getByRole("button", { name: /^ID\s*▼$/ }); button.focus(); rerender(view("Identifiant")); expect(order()).toEqual(["b", "a"]); expect(screen.getByRole("button", { name: /^Identifiant\s*▼$/ })).toBe(button); expect(button).toHaveFocus(); fireEvent.click(button); expect(order()).toEqual(["a", "b"]); }); it("retains header-based default and user sorting for callers without column IDs", () => { const { container } = render( r.id, sortValue: (r) => r.id }]} rows={rows} rowKey={(r) => r.id} selectedKey={null} onSelect={() => {}} emptyLabel="none" defaultSort={{ header: "ID", direction: "desc" }} />); const order = () => [...container.querySelectorAll("tbody tr")].map((r) => r.textContent); expect(order()).toEqual(["b", "a"]); fireEvent.click(screen.getByRole("button", { name: /^ID\s*▼$/ })); expect(order()).toEqual(["a", "b"]); }); interface Row { id: string; } const rows: Row[] = [{ id: "a" }, { id: "b" }]; const columns: Column[] = [{ header: "ID", cell: (r) => r.id }]; // Force the mobile media query to match so DataTable enters card mode. function setMobile(matches: boolean) { window.matchMedia = vi.fn().mockImplementation((query: string) => ({ matches, media: query, onchange: null, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), dispatchEvent: vi.fn(), })) as unknown as typeof window.matchMedia; } afterEach(() => { vi.restoreAllMocks(); }); // jsdom does no layout, so the scroll metrics are stubbed onto the scroll container directly. function setScroll(el: HTMLElement, { scrollTop, clientHeight, scrollHeight }: { scrollTop: number; clientHeight: number; scrollHeight: number }) { Object.defineProperty(el, "scrollTop", { value: scrollTop, configurable: true }); Object.defineProperty(el, "clientHeight", { value: clientHeight, configurable: true }); Object.defineProperty(el, "scrollHeight", { value: scrollHeight, configurable: true }); } function renderTable(onEndReached?: () => void) { const { container } = render( r.id} selectedKey={null} onSelect={() => {}} emptyLabel="none" onEndReached={onEndReached} />, ); return container.querySelector(".overflow-y-auto") as HTMLElement; } describe("DataTable onEndReached", () => { it("fires onEndReached when scrolled near the bottom", () => { const onEndReached = vi.fn(); const scroller = renderTable(onEndReached); setScroll(scroller, { scrollTop: 400, clientHeight: 500, scrollHeight: 1000 }); // 100px from bottom fireEvent.scroll(scroller); expect(onEndReached).toHaveBeenCalledTimes(1); }); it("does not fire onEndReached when far from the bottom", () => { const onEndReached = vi.fn(); const scroller = renderTable(onEndReached); setScroll(scroller, { scrollTop: 0, clientHeight: 500, scrollHeight: 1000 }); // 500px from bottom fireEvent.scroll(scroller); expect(onEndReached).not.toHaveBeenCalled(); }); }); describe("DataTable card mode", () => { it("renders cards instead of a table when mobile and renderCard is provided", () => { setMobile(true); const { container } = render( r.id} selectedKey={null} onSelect={() => {}} emptyLabel="none" renderCard={(r) => card-{r.id}} />, ); expect(container.querySelector("table")).toBeNull(); expect(screen.getByText("card-a")).toBeInTheDocument(); expect(screen.getByText("card-b")).toBeInTheDocument(); }); it("still renders a table on desktop even with renderCard provided", () => { setMobile(false); const { container } = render( r.id} selectedKey={null} onSelect={() => {}} emptyLabel="none" renderCard={(r) => card-{r.id}} />, ); expect(container.querySelector("table")).not.toBeNull(); expect(screen.queryByText("card-a")).toBeNull(); }); it("selects a card on click in card mode", () => { setMobile(true); const onSelect = vi.fn(); render( r.id} selectedKey={null} onSelect={onSelect} emptyLabel="none" renderCard={(r) => card-{r.id}} />, ); fireEvent.click(screen.getByText("card-a")); expect(onSelect).toHaveBeenCalledWith("a"); }); it("still fires onEndReached in card mode", () => { setMobile(true); const onEndReached = vi.fn(); const { container } = render( r.id} selectedKey={null} onSelect={() => {}} emptyLabel="none" onEndReached={onEndReached} renderCard={(r) => card-{r.id}} />, ); const scroller = container.querySelector(".overflow-y-auto") as HTMLElement; setScroll(scroller, { scrollTop: 400, clientHeight: 500, scrollHeight: 1000 }); fireEvent.scroll(scroller); expect(onEndReached).toHaveBeenCalledTimes(1); }); });