mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-10-16 22:19:57 +00:00
169 lines
6.2 KiB
TypeScript
169 lines
6.2 KiB
TypeScript
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<Row>[] => [{ id: "identity", header, cell: (r) => r.id, sortValue: (r) => r.id }];
|
|
const view = (header: string) => <DataTable columns={translatedColumns(header)} rows={data} rowKey={(r) => 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(<DataTable columns={[{ header: "ID", cell: (r: Row) => 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<Row>[] = [{ 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(
|
|
<DataTable
|
|
columns={columns}
|
|
rows={rows}
|
|
rowKey={(r) => 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(
|
|
<DataTable
|
|
columns={columns}
|
|
rows={rows}
|
|
rowKey={(r) => r.id}
|
|
selectedKey={null}
|
|
onSelect={() => {}}
|
|
emptyLabel="none"
|
|
renderCard={(r) => <span>card-{r.id}</span>}
|
|
/>,
|
|
);
|
|
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(
|
|
<DataTable
|
|
columns={columns}
|
|
rows={rows}
|
|
rowKey={(r) => r.id}
|
|
selectedKey={null}
|
|
onSelect={() => {}}
|
|
emptyLabel="none"
|
|
renderCard={(r) => <span>card-{r.id}</span>}
|
|
/>,
|
|
);
|
|
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(
|
|
<DataTable
|
|
columns={columns}
|
|
rows={rows}
|
|
rowKey={(r) => r.id}
|
|
selectedKey={null}
|
|
onSelect={onSelect}
|
|
emptyLabel="none"
|
|
renderCard={(r) => <span>card-{r.id}</span>}
|
|
/>,
|
|
);
|
|
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(
|
|
<DataTable
|
|
columns={columns}
|
|
rows={rows}
|
|
rowKey={(r) => r.id}
|
|
selectedKey={null}
|
|
onSelect={() => {}}
|
|
emptyLabel="none"
|
|
onEndReached={onEndReached}
|
|
renderCard={(r) => <span>card-{r.id}</span>}
|
|
/>,
|
|
);
|
|
const scroller = container.querySelector(".overflow-y-auto") as HTMLElement;
|
|
setScroll(scroller, { scrollTop: 400, clientHeight: 500, scrollHeight: 1000 });
|
|
fireEvent.scroll(scroller);
|
|
expect(onEndReached).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|