litellm/ui/litellm-dashboard/src/components/shared/DataTable/DataTable.test.tsx
ryan-crabbe-berri c7cbebb84b fix(ui): right-align money and count columns across tables
Make numeric the one alignment token for the three shared table
wrappers (DataTable, MemberTable, SimpleTable) via NUMERIC_CELL_CLASS,
and flag every money, cost, and bare-count column that was still
left-aligned. Raw ui/table usages that render spend or budgets get the
same class on their header and cell.
2026-08-21 13:51:43 -07:00

685 lines
25 KiB
TypeScript

import type { ColumnDef, ExpandedState } from "@tanstack/react-table";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { DataTable } from "./DataTable";
import { DataTableMultiSortHeader, DataTableSortHeader } from "./DataTableSortHeader";
import { DataTableViewOptions } from "./DataTableViewOptions";
interface Person {
id: string;
name: string;
email: string;
flagged?: boolean;
}
function person(id: string, name: string, flagged = false): Person {
return { id, name, email: `${name.toLowerCase()}@x.io`, flagged };
}
const names = (): (string | null)[] => screen.getAllByTestId("name-cell").map((el) => el.textContent);
const nameCellColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
];
const filterableColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: "Name",
meta: { title: "Name" },
filterFn: (row, columnId, value) => row.getValue<string>(columnId) === value,
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
];
const headerCycleColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: ({ column }) => <DataTableSortHeader column={column} title="Name" variant="header-cycle" />,
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
];
const dropdownSortColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: ({ column }) => <DataTableSortHeader column={column} title="Name" variant="dropdown-tristate" />,
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
];
const multiSortColumns: ColumnDef<Person, unknown>[] = [
{
id: "spend",
accessorKey: "name",
header: ({ table }) => (
<DataTableMultiSortHeader
table={table}
fields={[
{ id: "spend", label: "Spend" },
{ id: "max_budget", label: "Budget" },
]}
/>
),
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
];
const nameEmailColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => <span>{row.original.email}</span>,
},
];
const pinnedColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
meta: { pinned: "left" },
},
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => <span>{row.original.email}</span>,
},
];
const rowClickColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
{
id: "actions",
header: "Actions",
cell: () => (
<div>
<button data-testid="row-button">Act</button>
<input data-testid="row-input" aria-label="row input" />
</div>
),
},
];
const expansionColumns: ColumnDef<Person, unknown>[] = [
{
id: "expander",
header: "",
cell: ({ row }) => (
<button data-testid={`expand-${row.id}`} onClick={() => row.toggleExpanded()}>
toggle
</button>
),
},
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
];
const numericColumns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
{
id: "spend",
header: ({ column }) => <DataTableSortHeader column={column} title="Spend" />,
meta: { numeric: true },
cell: () => <span>$1.50</span>,
},
];
const CHARLIE_ALICE_BOB: Person[] = [person("c", "Charlie"), person("a", "Alice"), person("b", "Bob")];
describe("DataTable numeric columns", () => {
it("right-aligns the header and cells of a numeric column only", () => {
render(<DataTable data={[person("a", "Alice")]} columns={numericColumns} sortingMode="client" />);
const spendHeader = screen.getByText("Spend").closest("th");
expect(spendHeader).toHaveClass("text-right", "tabular-nums");
expect(spendHeader?.querySelector("div")).toHaveClass("justify-end");
expect(screen.getByText("$1.50").closest("td")).toHaveClass("text-right", "tabular-nums");
expect(screen.getByText("Name").closest("th")).not.toHaveClass("text-right");
expect(screen.getByTestId("name-cell").closest("td")).not.toHaveClass("text-right");
});
});
describe("DataTable sorting", () => {
it("client mode reorders rows when the sort header is clicked", async () => {
const user = userEvent.setup();
render(<DataTable data={CHARLIE_ALICE_BOB} columns={headerCycleColumns} sortingMode="client" />);
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
await user.click(screen.getByTestId("sort-header-name"));
expect(names()).toEqual(["Alice", "Bob", "Charlie"]);
});
it("server mode fires the callback but never reorders locally", async () => {
const user = userEvent.setup();
const onSortingChange = vi.fn();
render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={headerCycleColumns}
sortingMode="server"
sorting={[{ id: "name", desc: false }]}
onSortingChange={onSortingChange}
/>,
);
// sorting state says ascending, but server mode must render data as given
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
await user.click(screen.getByTestId("sort-header-name"));
expect(onSortingChange).toHaveBeenCalledTimes(1);
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
});
it("dropdown-tristate variant sorts ascending, descending, then resets", async () => {
const user = userEvent.setup();
render(<DataTable data={CHARLIE_ALICE_BOB} columns={dropdownSortColumns} sortingMode="client" />);
await user.click(screen.getByTestId("sort-trigger-name"));
await user.click(await screen.findByText("Ascending"));
expect(names()).toEqual(["Alice", "Bob", "Charlie"]);
await user.click(screen.getByTestId("sort-trigger-name"));
await user.click(await screen.findByText("Descending"));
expect(names()).toEqual(["Charlie", "Bob", "Alice"]);
await user.click(screen.getByTestId("sort-trigger-name"));
await user.click(await screen.findByText("Reset"));
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
});
it("multi-sort header emits the chosen field id (not the column id) as the sort key", async () => {
const user = userEvent.setup();
const onSortingChange = vi.fn();
render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={multiSortColumns}
sortingMode="server"
sorting={[]}
onSortingChange={onSortingChange}
/>,
);
await user.click(screen.getByTestId("sort-trigger-spend"));
await user.click(await screen.findByText("Budget descending"));
expect(onSortingChange).toHaveBeenLastCalledWith([{ id: "max_budget", desc: true }]);
await user.click(screen.getByTestId("sort-trigger-spend"));
await user.click(await screen.findByText("Spend ascending"));
expect(onSortingChange).toHaveBeenLastCalledWith([{ id: "spend", desc: false }]);
});
it("multi-sort header reflects the active field and direction, and Reset clears it", async () => {
const user = userEvent.setup();
const onSortingChange = vi.fn();
render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={multiSortColumns}
sortingMode="server"
sorting={[{ id: "max_budget", desc: true }]}
onSortingChange={onSortingChange}
/>,
);
await user.click(screen.getByTestId("sort-trigger-spend"));
// The header trigger shows the active (descending) indicator while sorted by a field it owns.
expect(screen.getByTestId("sort-trigger-spend").querySelector("[data-sort-indicator='desc']")).not.toBeNull();
await user.click(await screen.findByText("Reset"));
expect(onSortingChange).toHaveBeenLastCalledWith([]);
});
});
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 />);
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%");
});
});
describe("DataTable pagination", () => {
const fivePeople: Person[] = Array.from({ length: 5 }, (_, i) => person(String(i), `P${i}`));
it("client mode slices rows and advances pages", async () => {
const user = userEvent.setup();
render(<DataTable data={fivePeople} columns={nameCellColumns} paginationMode="client" pageSizeOptions={[2]} />);
expect(names()).toEqual(["P0", "P1"]);
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 1-2 of 5");
await user.click(screen.getByTestId("pagination-next"));
expect(names()).toEqual(["P2", "P3"]);
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 3-4 of 5");
});
it("server mode shows X-Y of Z from rowCount and does NOT slice the given rows", async () => {
const user = userEvent.setup();
const onPaginationChange = vi.fn();
const pageSlice: Person[] = [person("10", "P10"), person("11", "P11"), person("12", "P12")];
render(
<DataTable
data={pageSlice}
columns={nameCellColumns}
paginationMode="server"
pagination={{ pageIndex: 1, pageSize: 10 }}
rowCount={25}
onPaginationChange={onPaginationChange}
/>,
);
expect(names()).toEqual(["P10", "P11", "P12"]);
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 11-20 of 25");
await user.click(screen.getByTestId("pagination-next"));
expect(onPaginationChange).toHaveBeenCalledTimes(1);
});
});
describe("DataTable filtering", () => {
it("client mode filters rows by columnFilters", () => {
const { rerender } = render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={filterableColumns}
filterMode="client"
columnFilters={[]}
onColumnFiltersChange={vi.fn()}
/>,
);
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
rerender(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={filterableColumns}
filterMode="client"
columnFilters={[{ id: "name", value: "Alice" }]}
onColumnFiltersChange={vi.fn()}
/>,
);
expect(names()).toEqual(["Alice"]);
});
it("client global filter matches substrings across columns", () => {
const { rerender } = render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={nameEmailColumns}
filterMode="client"
globalFilter=""
onGlobalFilterChange={vi.fn()}
/>,
);
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
rerender(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={nameEmailColumns}
filterMode="client"
globalFilter="ali"
onGlobalFilterChange={vi.fn()}
/>,
);
expect(names()).toEqual(["Alice"]);
});
it("server mode never filters locally even when columnFilters is set", () => {
render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={filterableColumns}
filterMode="server"
columnFilters={[{ id: "name", value: "Alice" }]}
onColumnFiltersChange={vi.fn()}
/>,
);
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
});
});
describe("DataTable loading", () => {
it("renders skeleton rows while loading and real rows once loaded", () => {
const { rerender } = render(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} isLoading />);
expect(screen.getAllByTestId("skeleton-row").length).toBeGreaterThan(0);
expect(screen.queryByTestId("name-cell")).not.toBeInTheDocument();
rerender(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} />);
expect(screen.queryAllByTestId("skeleton-row")).toHaveLength(0);
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
});
it("gives compact skeleton rows the same height as loaded rows so loading does not shrink the table", () => {
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);
rerender(<DataTable data={CHARLIE_ALICE_BOB} columns={nameCellColumns} size="compact" />);
expect(document.querySelector("[data-row-id]")?.className).toContain(loadedRowHeight);
});
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");
});
it("varies skeleton shape and width per column instead of one fixed bar", () => {
const columns: ColumnDef<Person, unknown>[] = [
{ accessorKey: "name", header: "Name", meta: { skeleton: "twoLine" }, cell: () => null },
{ accessorKey: "email", header: "Email", cell: () => null },
];
render(<DataTable data={CHARLIE_ALICE_BOB} columns={columns} isLoading />);
const firstRow = screen.getAllByTestId("skeleton-row").at(0);
expect(firstRow).toBeDefined();
const bars = Array.from(firstRow?.querySelectorAll('[data-slot="skeleton"]') ?? []);
// twoLine column contributes a main + sub bar (2); the text column contributes 1
expect(bars).toHaveLength(3);
// per-column widths differ instead of every cell sharing one fixed width
expect(new Set(bars.map((bar) => bar.className)).size).toBeGreaterThan(1);
});
it("renders shape-specific skeletons for badge, chips, and meter columns", () => {
const columns: ColumnDef<Person, unknown>[] = [
{ id: "badge", header: "Badge", meta: { skeleton: "badge" }, cell: () => null },
{ id: "chips", header: "Chips", meta: { skeleton: "chips" }, cell: () => null },
{ id: "meter", header: "Meter", meta: { skeleton: "meter" }, cell: () => null },
];
render(<DataTable data={CHARLIE_ALICE_BOB} columns={columns} isLoading />);
const firstRow = screen.getAllByTestId("skeleton-row").at(0);
const cells = Array.from(firstRow?.querySelectorAll("td") ?? []);
const barsIn = (cell: Element | undefined) => cell?.querySelectorAll('[data-slot="skeleton"]').length ?? 0;
// badge = a single pill, chips = three pills, meter = value bar + track bar
expect(barsIn(cells[0])).toBe(1);
expect(cells[0]?.querySelector('[data-slot="skeleton"]')?.className).toContain("rounded-full");
expect(barsIn(cells[1])).toBe(3);
expect(barsIn(cells[2])).toBe(2);
});
it("uses a column's renderSkeleton override when provided", () => {
const columns: ColumnDef<Person, unknown>[] = [
{
id: "custom",
header: "Custom",
meta: { renderSkeleton: () => <div data-testid="custom-skeleton">loading</div> },
cell: () => null,
},
];
render(<DataTable data={CHARLIE_ALICE_BOB} columns={columns} isLoading />);
expect(screen.getAllByTestId("custom-skeleton").length).toBeGreaterThan(0);
});
});
describe("DataTable column visibility", () => {
it("hides a column when toggled off in the view-options menu", async () => {
const user = userEvent.setup();
const { container } = render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={nameEmailColumns}
toolbar={(table) => <DataTableViewOptions table={table} />}
/>,
);
expect(container.querySelector('th[data-header-id="email"]')).not.toBeNull();
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 user.click(screen.getByTestId("view-option-email"));
await waitFor(() => expect(container.querySelector('th[data-header-id="email"]')).not.toBeNull());
});
it("omits columns that opt out of hiding from the menu", async () => {
const user = userEvent.setup();
const columns: ColumnDef<Person, unknown>[] = [
{
accessorKey: "name",
header: "Name",
enableHiding: false,
cell: ({ row }) => <span data-testid="name-cell">{row.original.name}</span>,
},
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => <span>{row.original.email}</span>,
},
];
render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={columns}
toolbar={(table) => <DataTableViewOptions table={table} />}
/>,
);
await user.click(screen.getByTestId("view-options-trigger"));
expect(await screen.findByTestId("view-option-email")).toBeInTheDocument();
expect(screen.queryByTestId("view-option-name")).not.toBeInTheDocument();
});
});
describe("DataTable pinned columns", () => {
it("applies sticky positioning to a pinned column only", () => {
const { container } = 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("");
});
});
describe("DataTable row click guard", () => {
it("fires onRowClick from a plain cell but not from interactive elements", async () => {
const user = userEvent.setup();
const onRowClick = vi.fn();
render(<DataTable data={[person("a", "Alice")]} columns={rowClickColumns} onRowClick={onRowClick} />);
await user.click(screen.getByTestId("name-cell"));
expect(onRowClick).toHaveBeenCalledTimes(1);
expect(onRowClick).toHaveBeenCalledWith(expect.objectContaining({ id: "a" }));
await user.click(screen.getByTestId("row-button"));
expect(onRowClick).toHaveBeenCalledTimes(1);
await user.click(screen.getByTestId("row-input"));
expect(onRowClick).toHaveBeenCalledTimes(1);
});
});
describe("DataTable expansion", () => {
const subComponent = ({ row }: { row: { original: Person } }) => (
<div data-testid="sub-row">details for {row.original.name}</div>
);
it("toggles the sub-row in uncontrolled mode", async () => {
const user = userEvent.setup();
render(
<DataTable
data={[person("a", "Alice")]}
columns={expansionColumns}
getRowId={(row) => row.id}
getRowCanExpand={() => true}
renderSubComponent={subComponent}
/>,
);
expect(screen.queryByTestId("sub-row")).not.toBeInTheDocument();
await user.click(screen.getByTestId("expand-a"));
expect(screen.getByTestId("sub-row")).toBeInTheDocument();
await user.click(screen.getByTestId("expand-a"));
expect(screen.queryByTestId("sub-row")).not.toBeInTheDocument();
});
it("toggles the sub-row in controlled mode driven by parent state", async () => {
const user = userEvent.setup();
const Harness = () => {
const [expanded, setExpanded] = useState<ExpandedState>({});
return (
<DataTable
data={[person("a", "Alice")]}
columns={expansionColumns}
getRowId={(row) => row.id}
expanded={expanded}
onExpandedChange={setExpanded}
getRowCanExpand={() => true}
renderSubComponent={subComponent}
/>
);
};
render(<Harness />);
expect(screen.queryByTestId("sub-row")).not.toBeInTheDocument();
await user.click(screen.getByTestId("expand-a"));
expect(screen.getByTestId("sub-row")).toBeInTheDocument();
await user.click(screen.getByTestId("expand-a"));
expect(screen.queryByTestId("sub-row")).not.toBeInTheDocument();
});
it("stays collapsed in controlled mode when the parent ignores the change", async () => {
const user = userEvent.setup();
const onExpandedChange = vi.fn();
render(
<DataTable
data={[person("a", "Alice")]}
columns={expansionColumns}
getRowId={(row) => row.id}
expanded={{}}
onExpandedChange={onExpandedChange}
getRowCanExpand={() => true}
renderSubComponent={subComponent}
/>,
);
await user.click(screen.getByTestId("expand-a"));
expect(onExpandedChange).toHaveBeenCalledTimes(1);
expect(screen.queryByTestId("sub-row")).not.toBeInTheDocument();
});
});
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(
<DataTable
data={data}
columns={nameCellColumns}
getRowId={(row) => row.id}
rowClassName={(row) => (row.original.flagged ? "flagged-row" : "")}
/>,
);
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");
});
it("renders the footer slot inside a tfoot element", () => {
render(
<DataTable
data={CHARLIE_ALICE_BOB}
columns={nameCellColumns}
footer={() => (
<tr data-testid="footer-row">
<td>Total: 3</td>
</tr>
)}
/>,
);
expect(screen.getByTestId("footer-row").closest("tfoot")).not.toBeNull();
});
});
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);
rerender(<DataTable data={CHARLIE_ALICE_BOB} columns={nameEmailColumns} />);
expect(container.querySelectorAll("[data-resizer]").length).toBe(0);
});
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" });
});
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;
// 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.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).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");
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");
});
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;
expect(scroller.className).toContain("overflow-x-auto");
expect(scroller.className).not.toContain("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");
});
});