mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
test(ui): assert DataTable behavior instead of DOM structure
The shared DataTable test reached for elements by CSS selector and by walking parentElement chains, then asserted on Tailwind class strings. It had no role queries at all, so a wrapper div anywhere in the render tree broke it while changing nothing a user sees. Columns, rows and headers are now found the way a user finds them: by role and by the text on screen. The compact skeleton row is compared against the loaded row's height rather than a hard-coded h-8, so renaming the class no longer breaks the test but shrinking the row still does. The fillHeight and maxBodyHeight cases stay class assertions. jsdom has no layout engine, so there is nothing behavioural to assert there. What they no longer do is derive their elements from incidental nesting: the three layout wrappers and the header now publish a stable test id, which is also why the resizer's write-only data-resizer attribute became one. Budgets drop with the counts: no-container 150 to 133, no-node-access 760 to 723.
This commit is contained in:
parent
847d737b8e
commit
2fbea77afa
3 changed files with 66 additions and 65 deletions
|
|
@ -5,7 +5,7 @@
|
|||
"max-depth": { "max": 70, "target": 30 },
|
||||
"local/no-large-inline-object-arg": { "max": 559, "target": 300 },
|
||||
"local/no-long-condition-chain": { "max": 265, "target": 120 },
|
||||
"testing-library/no-container": { "max": 150, "target": 50 },
|
||||
"testing-library/no-node-access": { "max": 760, "target": 500 },
|
||||
"testing-library/no-container": { "max": 133, "target": 50 },
|
||||
"testing-library/no-node-access": { "max": 723, "target": 500 },
|
||||
"testing-library/prefer-screen-queries": { "max": 221, "target": 0 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ColumnDef, ExpandedState } from "@tanstack/react-table";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useState } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
|
@ -21,6 +21,12 @@ function person(id: string, name: string, flagged = false): Person {
|
|||
|
||||
const names = (): (string | null)[] => screen.getAllByTestId("name-cell").map((el) => el.textContent);
|
||||
|
||||
const heightClassesOf = (el: HTMLElement | undefined): string[] =>
|
||||
(el?.className ?? "")
|
||||
.split(/\s+/)
|
||||
.filter((cls) => cls.startsWith("h-"))
|
||||
.sort();
|
||||
|
||||
const nameCellColumns: ColumnDef<Person, unknown>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
|
@ -229,12 +235,10 @@ describe("DataTable sorting", () => {
|
|||
|
||||
describe("DataTable layout", () => {
|
||||
it("stretches the table to fill the container when resizing is on, so hidden columns leave no right-side gap", () => {
|
||||
const { container } = render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} enableColumnResizing />);
|
||||
render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} enableColumnResizing />);
|
||||
|
||||
const table = container.querySelector("table");
|
||||
expect(table).not.toBeNull();
|
||||
// width pins the natural column total (horizontal scroll on overflow); minWidth:100% fills the gap on underflow.
|
||||
expect(table?.style.minWidth).toBe("100%");
|
||||
expect(screen.getByRole("table")).toHaveStyle({ minWidth: "100%" });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -354,17 +358,21 @@ describe("DataTable loading", () => {
|
|||
const { rerender } = render(
|
||||
<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} size="compact" isLoading />,
|
||||
);
|
||||
const skeletonRow = screen.getAllByTestId("skeleton-row").at(0);
|
||||
const loadedRowHeight = "h-8";
|
||||
expect(skeletonRow?.className).toContain(loadedRowHeight);
|
||||
const skeletonHeight = heightClassesOf(screen.getAllByRole("row").at(-1));
|
||||
|
||||
rerender(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} size="compact" />);
|
||||
expect(document.querySelector("[data-row-id]")?.className).toContain(loadedRowHeight);
|
||||
const loadedHeight = heightClassesOf(screen.getByRole("row", { name: /Charlie/ }));
|
||||
|
||||
expect(loadedHeight).not.toEqual([]);
|
||||
expect(skeletonHeight).toEqual(loadedHeight);
|
||||
});
|
||||
|
||||
it("does not force the compact height on default-size skeleton rows", () => {
|
||||
render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} isLoading />);
|
||||
expect(screen.getAllByTestId("skeleton-row").at(0)?.className).not.toContain("h-8");
|
||||
const { rerender } = render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} isLoading />);
|
||||
const skeletonHeight = heightClassesOf(screen.getAllByRole("row").at(-1));
|
||||
|
||||
rerender(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} size="compact" isLoading />);
|
||||
expect(heightClassesOf(screen.getAllByRole("row").at(-1))).not.toEqual(skeletonHeight);
|
||||
});
|
||||
|
||||
it("varies skeleton shape and width per column instead of one fixed bar", () => {
|
||||
|
|
@ -420,7 +428,7 @@ describe("DataTable loading", () => {
|
|||
describe("DataTable column visibility", () => {
|
||||
it("hides a column when toggled off in the view-options menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
render(
|
||||
<DataTable
|
||||
data={CHARLIE_ALICE_BOB}
|
||||
columns={nameEmailColumns}
|
||||
|
|
@ -428,13 +436,13 @@ describe("DataTable column visibility", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('th[data-header-id="email"]')).not.toBeNull();
|
||||
expect(screen.getByRole("columnheader", { name: "Email" })).toBeInTheDocument();
|
||||
await user.click(screen.getByTestId("view-options-trigger"));
|
||||
await user.click(await screen.findByTestId("view-option-email"));
|
||||
await waitFor(() => expect(container.querySelector('th[data-header-id="email"]')).toBeNull());
|
||||
await waitFor(() => expect(screen.queryByRole("columnheader", { name: "Email" })).not.toBeInTheDocument());
|
||||
|
||||
await user.click(screen.getByTestId("view-option-email"));
|
||||
await waitFor(() => expect(container.querySelector('th[data-header-id="email"]')).not.toBeNull());
|
||||
expect(await screen.findByRole("columnheader", { name: "Email" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("omits columns that opt out of hiding from the menu", async () => {
|
||||
|
|
@ -468,14 +476,10 @@ describe("DataTable column visibility", () => {
|
|||
|
||||
describe("DataTable pinned columns", () => {
|
||||
it("applies sticky positioning to a pinned column only", () => {
|
||||
const { container } = render(<DataTable data={CHARLIE_ALICE_BOB} columns={pinnedColumns} />);
|
||||
render(<DataTable data={CHARLIE_ALICE_BOB} columns={pinnedColumns} />);
|
||||
|
||||
const pinnedHead = container.querySelector<HTMLElement>('th[data-header-id="name"]');
|
||||
const normalHead = container.querySelector<HTMLElement>('th[data-header-id="email"]');
|
||||
|
||||
expect(pinnedHead?.style.position).toBe("sticky");
|
||||
expect(pinnedHead?.style.left).toBe("0px");
|
||||
expect(normalHead?.style.position).toBe("");
|
||||
expect(screen.getByRole("columnheader", { name: "Name" })).toHaveStyle({ position: "sticky", left: "0px" });
|
||||
expect(screen.getByRole("columnheader", { name: "Email" })).not.toHaveStyle({ position: "sticky" });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -570,7 +574,7 @@ describe("DataTable expansion", () => {
|
|||
describe("DataTable row styling and footer", () => {
|
||||
it("applies rowClassName to the matching row only", () => {
|
||||
const data = [person("a", "Alice", true), person("b", "Bob", false)];
|
||||
const { container } = render(
|
||||
render(
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={nameCellColumns}
|
||||
|
|
@ -579,8 +583,8 @@ describe("DataTable row styling and footer", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('tr[data-row-id="a"]')?.className).toContain("flagged-row");
|
||||
expect(container.querySelector('tr[data-row-id="b"]')?.className).not.toContain("flagged-row");
|
||||
expect(screen.getByRole("row", { name: /Alice/ })).toHaveClass("flagged-row");
|
||||
expect(screen.getByRole("row", { name: /Bob/ })).not.toHaveClass("flagged-row");
|
||||
});
|
||||
|
||||
it("renders the footer slot inside a tfoot element", () => {
|
||||
|
|
@ -596,63 +600,57 @@ describe("DataTable row styling and footer", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("footer-row").closest("tfoot")).not.toBeNull();
|
||||
const rowGroups = screen.getAllByRole("rowgroup");
|
||||
expect(within(rowGroups.at(-1) as HTMLElement).getByText("Total: 3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DataTable layout", () => {
|
||||
it("exposes resize handles with stable selectors only when resizing is enabled", () => {
|
||||
const { container, rerender } = render(
|
||||
<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} enableColumnResizing />,
|
||||
);
|
||||
expect(container.querySelectorAll("[data-resizer][data-header-id]").length).toBe(2);
|
||||
const { rerender } = render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} enableColumnResizing />);
|
||||
expect(screen.getByTestId("column-resizer-name")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("column-resizer-email")).toBeInTheDocument();
|
||||
|
||||
rerender(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} />);
|
||||
expect(container.querySelectorAll("[data-resizer]").length).toBe(0);
|
||||
expect(screen.queryByTestId("column-resizer-name")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("makes the header sticky and constrains body height when maxBodyHeight is set", () => {
|
||||
const { container } = render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} maxBodyHeight={240} />);
|
||||
expect(container.querySelector("thead")?.className).toContain("sticky");
|
||||
const scroller = container.querySelector('[data-slot="table-container"]')?.parentElement as HTMLElement;
|
||||
expect(scroller).toHaveStyle({ maxHeight: "240px" });
|
||||
render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} maxBodyHeight={240} />);
|
||||
expect(screen.getByTestId("data-table-head")).toHaveClass("sticky");
|
||||
expect(screen.getByTestId("data-table-scroller")).toHaveStyle({ maxHeight: "240px" });
|
||||
});
|
||||
|
||||
it("caps fillHeight at the parent's height instead of stretching to it, so a short table stays short", () => {
|
||||
const { container } = render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} fillHeight />);
|
||||
const scroller = container.querySelector('[data-slot="table-container"]')?.parentElement as HTMLElement;
|
||||
const frame = scroller.parentElement as HTMLElement;
|
||||
const outer = frame.parentElement as HTMLElement;
|
||||
render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} fillHeight />);
|
||||
const outer = screen.getByTestId("data-table-root");
|
||||
const frame = screen.getByTestId("data-table-frame");
|
||||
const scroller = screen.getByTestId("data-table-scroller");
|
||||
|
||||
// A ceiling, not a stretch: flex-1 here would hold the footer at the bottom on a two-row table.
|
||||
expect(outer.className).toContain("max-h-full");
|
||||
expect(outer.className).not.toContain("flex-1");
|
||||
expect(frame.className).not.toContain("flex-1");
|
||||
expect(scroller.className).not.toContain("flex-1");
|
||||
expect(outer).toHaveClass("max-h-full", "flex-col");
|
||||
expect(outer).not.toHaveClass("flex-1");
|
||||
expect(frame).toHaveClass("flex-col");
|
||||
expect(frame).not.toHaveClass("flex-1");
|
||||
expect(scroller).not.toHaveClass("flex-1");
|
||||
|
||||
expect(outer.className).toContain("flex-col");
|
||||
expect(frame.className).toContain("flex-col");
|
||||
expect(scroller.className).toContain("min-h-0");
|
||||
expect(scroller.className).toContain("overflow-auto");
|
||||
expect(scroller).toHaveClass("min-h-0", "overflow-auto");
|
||||
expect(scroller).toHaveStyle({ maxHeight: "" });
|
||||
// Without this the Table primitive's own overflow container captures the sticky header.
|
||||
expect(scroller.className).toContain("[&_[data-slot=table-container]]:overflow-visible");
|
||||
expect(scroller).toHaveClass("[&_[data-slot=table-container]]:overflow-visible");
|
||||
|
||||
const thead = container.querySelector("thead") as HTMLElement;
|
||||
expect(thead.className).toContain("sticky");
|
||||
// Rows pass under the header, so the semi-transparent row tint alone would let them show through.
|
||||
expect(thead.className).toContain("bg-background");
|
||||
expect(screen.getByTestId("data-table-head")).toHaveClass("sticky", "bg-background");
|
||||
});
|
||||
|
||||
it("leaves the default layout untouched when neither height mode is set", () => {
|
||||
const { container } = render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} />);
|
||||
const scroller = container.querySelector('[data-slot="table-container"]')?.parentElement as HTMLElement;
|
||||
render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} />);
|
||||
const scroller = screen.getByTestId("data-table-scroller");
|
||||
|
||||
expect(scroller.className).toContain("overflow-x-auto");
|
||||
expect(scroller.className).not.toContain("min-h-0");
|
||||
expect(scroller).toHaveClass("overflow-x-auto");
|
||||
expect(scroller).not.toHaveClass("min-h-0");
|
||||
expect(scroller).toHaveStyle({ maxHeight: "" });
|
||||
expect((scroller.parentElement as HTMLElement).className).not.toContain("flex-col");
|
||||
expect(container.querySelector("thead")?.className).not.toContain("sticky");
|
||||
expect(container.querySelector("thead")?.className).not.toContain("bg-background");
|
||||
expect(screen.getByTestId("data-table-frame")).not.toHaveClass("flex-col");
|
||||
expect(screen.getByTestId("data-table-head")).not.toHaveClass("sticky", "bg-background");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -195,8 +195,7 @@ function DataTableHeadCell<TData>({ header, size, stickyHeader, enableColumnResi
|
|||
)}
|
||||
{canResize && (
|
||||
<div
|
||||
data-resizer
|
||||
data-header-id={header.id}
|
||||
data-testid={`column-resizer-${header.id}`}
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
onDoubleClick={() => column.resetSize()}
|
||||
|
|
@ -589,15 +588,19 @@ export function DataTable<TData extends RowData, TValue>(props: DataTableProps<T
|
|||
const paginationNode = renderPagination();
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", fill.outer)}>
|
||||
<div className={cn("overflow-hidden rounded-lg border border-border", fill.frame)}>
|
||||
<div data-testid="data-table-root" className={cn("w-full", fill.outer)}>
|
||||
<div data-testid="data-table-frame" className={cn("overflow-hidden rounded-lg border border-border", fill.frame)}>
|
||||
{toolbar !== undefined && <div className="shrink-0 border-b border-border px-4 py-3">{toolbar(table)}</div>}
|
||||
<div
|
||||
data-testid="data-table-scroller"
|
||||
className={cn(stickyHeader ? "overflow-auto" : "overflow-x-auto", fill.body)}
|
||||
style={maxBodyHeight !== undefined ? { maxHeight: maxBodyHeight } : undefined}
|
||||
>
|
||||
<TableRoot className={enableColumnResizing ? "table-fixed" : ""} style={tableStyle}>
|
||||
<TableHeader className={cn(stickyHeader ? "sticky top-0 z-sticky" : "", fill.header)}>
|
||||
<TableHeader
|
||||
data-testid="data-table-head"
|
||||
className={cn(stickyHeader ? "sticky top-0 z-sticky" : "", fill.header)}
|
||||
>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-muted/50">
|
||||
{headerGroup.headers.map((header) => (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue