Files
prowler/ui/components/shadcn/table/data-table.test.tsx
T

244 lines
8.1 KiB
TypeScript

import type { ColumnDef } from "@tanstack/react-table";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { MetaDataProps } from "@/types";
import { DataTable } from "./data-table";
vi.mock("next/navigation", () => ({
usePathname: () => "/findings",
useRouter: () => ({
push: vi.fn(),
}),
useSearchParams: () => new URLSearchParams(),
}));
vi.mock("@/contexts", () => ({
useFilterTransitionOptional: () => null,
}));
vi.mock("@/lib", () => ({
cn: (...classes: Array<string | false | null | undefined>) =>
classes.filter(Boolean).join(" "),
getPaginationInfo: (metadata: MetaDataProps) => ({
currentPage: metadata.pagination.page,
totalPages: metadata.pagination.pages,
totalEntries: metadata.pagination.count,
itemsPerPageOptions: metadata.pagination.itemsPerPage ?? [10, 20, 50],
}),
}));
interface TestRow {
name: string;
}
const columns: ColumnDef<TestRow>[] = [
{
accessorKey: "name",
header: "Name",
},
];
const columnsWithActions: ColumnDef<TestRow>[] = [
{
accessorKey: "name",
header: "Name",
},
{
id: "actions",
header: "",
cell: () => <button type="button">Actions</button>,
enableSorting: false,
},
];
const metadata: MetaDataProps = {
pagination: {
page: 1,
pages: 1,
count: 7,
},
version: "v1",
};
describe("DataTable", () => {
describe("when toolbar right content is provided", () => {
it("should render it before the total entries count", () => {
// Given
render(
<DataTable
columns={columns}
data={[{ name: "Finding A" }]}
metadata={metadata}
toolbarRightContent={<span>Include muted findings</span>}
/>,
);
// When
const toolbarContent = screen.getByText("Include muted findings");
const totalEntries = screen.getByText("7 Total Entries");
const tableContainerText =
toolbarContent.parentElement?.textContent ?? "";
// Then
expect(toolbarContent).toBeInTheDocument();
expect(totalEntries).toBeInTheDocument();
expect(tableContainerText.indexOf("Include muted findings")).toBeLessThan(
tableContainerText.indexOf("7 Total Entries"),
);
});
it("should stack the right content below search when the container is narrow", () => {
// Given
render(
<DataTable
columns={columns}
data={[{ name: "Finding A" }]}
metadata={metadata}
showSearch
toolbarRightContent={<span>Include muted findings</span>}
/>,
);
// When
const toolbar = screen.getByTestId("data-table-toolbar");
const rightContent = screen.getByTestId("data-table-toolbar-right");
// Then: md is a container query app-wide, so this also reacts when the
// side panel shrinks the page; the right group stays on one row.
expect(toolbar).toHaveClass("flex-col");
expect(toolbar).toHaveClass("md:flex-row");
expect(rightContent).toHaveClass("w-full");
expect(rightContent).toHaveClass("md:w-auto");
expect(rightContent).toHaveClass("flex-wrap", "items-center");
});
});
describe("when an actions column is present", () => {
it("should keep the actions header and cells sticky on the right edge", () => {
// Given
render(
<DataTable
columns={columnsWithActions}
data={[{ name: "Finding A" }]}
/>,
);
// When
const columnHeaders = screen.getAllByRole("columnheader");
const nameHeader = columnHeaders[0];
const actionsHeader = columnHeaders.at(-1);
expect(actionsHeader).toBeDefined();
const actionsHeaderElement = actionsHeader as HTMLElement;
const nameCell = screen.getByText("Finding A").closest("td");
const actionsCell = screen
.getByRole("button", {
name: "Actions",
})
.closest("td");
// Then
expect(nameHeader).not.toHaveClass("sticky");
expect(nameCell).not.toHaveClass("sticky");
expect(nameHeader).toHaveClass("pr-6");
expect(nameCell).toHaveClass("pr-6");
expect(actionsHeaderElement).not.toHaveClass("sticky");
expect(actionsHeaderElement).not.toHaveClass("pr-6");
expect(actionsHeaderElement).not.toHaveClass("right-0");
expect(actionsHeaderElement).not.toHaveClass("z-20");
expect(actionsHeaderElement).toHaveClass("bg-bg-neutral-tertiary");
expect(actionsHeaderElement).toHaveClass("border-y");
expect(actionsHeaderElement).toHaveClass("last:border-r");
expect(actionsHeaderElement).toHaveClass("last:rounded-r-full");
expect(actionsHeaderElement).not.toHaveClass("bg-transparent");
expect(actionsHeaderElement).not.toHaveClass("border-y-0");
expect(actionsHeaderElement).not.toHaveClass("border-l-0");
expect(actionsHeaderElement).not.toHaveClass("border-r-0");
expect(actionsHeaderElement).not.toHaveClass("last:border-r-0");
expect(actionsHeaderElement).not.toHaveClass("backdrop-blur-none");
expect(actionsHeaderElement).not.toHaveClass("last:rounded-r-none");
expect(actionsHeaderElement).not.toHaveClass("before:bg-gradient-to-r");
expect(actionsHeaderElement).not.toHaveClass("before:content-['']");
expect(actionsHeaderElement).not.toHaveClass("after:content-['']");
expect(actionsHeaderElement).not.toHaveClass("after:rounded-r-full");
expect(actionsHeaderElement.querySelector("div")).not.toBeInTheDocument();
expect(actionsCell).toHaveClass("sticky");
expect(actionsCell).not.toHaveClass("pr-6");
expect(actionsCell).toHaveClass("right-0");
expect(actionsCell).toHaveClass("z-20");
expect(actionsCell).toHaveClass("bg-bg-neutral-secondary");
expect(actionsCell).toHaveClass("last:rounded-r-none!");
expect(actionsCell).not.toHaveClass("border-l");
expect(actionsCell).toHaveClass("before:bg-gradient-to-r");
expect(actionsCell).toHaveClass("before:from-transparent");
expect(actionsCell).toHaveClass("before:to-bg-neutral-secondary");
});
});
describe("when a preview lock limits the visible rows", () => {
it("blurs the rows past the limit, keeps their content inert, and floats the overlay over them", () => {
// Given
const renderAfterRow = vi.fn(() => null);
render(
<DataTable
columns={columns}
data={[
{ name: "Finding A" },
{ name: "Finding B" },
{ name: "Finding C" },
]}
metadata={metadata}
renderAfterRow={renderAfterRow}
previewLock={{
visibleRowCount: 2,
overlay: <div>Unlock every finding</div>,
onLockedInteraction: vi.fn(),
}}
/>,
);
// When
const visibleRow = screen.getByText("Finding B").closest("tr");
const lockedRow = screen.getByText("Finding C").closest("tr");
// Then
expect(visibleRow?.querySelector("[inert]")).toBeNull();
expect(lockedRow?.querySelector("td > div")).toHaveAttribute("inert");
expect(lockedRow).toHaveAttribute("aria-hidden", "true");
expect(lockedRow).toHaveClass("select-none");
expect(screen.getByText("Unlock every finding")).toBeInTheDocument();
expect(renderAfterRow).toHaveBeenCalledWith(
expect.objectContaining({ index: 1 }),
);
expect(renderAfterRow).not.toHaveBeenCalledWith(
expect.objectContaining({ index: 2 }),
);
});
it("renders no overlay when every row fits in the preview", () => {
// Given / When
render(
<DataTable
columns={columns}
data={[{ name: "Finding A" }]}
metadata={metadata}
previewLock={{
visibleRowCount: 2,
overlay: <div>Unlock every finding</div>,
onLockedInteraction: vi.fn(),
}}
/>,
);
// Then
expect(
screen.queryByText("Unlock every finding"),
).not.toBeInTheDocument();
expect(screen.getByText("Finding A").closest("tr")).not.toHaveAttribute(
"inert",
);
});
});
});