/* Copyright 2026 Marimo. All rights reserved. */ import type { ColumnDef, PaginationState, RowSelectionState, SortingState, } from "@tanstack/react-table"; import { render, screen, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { TooltipProvider } from "@/components/ui/tooltip"; import { DataTable } from "../data-table"; interface TestData { id: number; name: string; } describe("DataTable", () => { it("should maintain selection state when remounted", () => { const mockOnRowSelectionChange = vi.fn(); const testData: TestData[] = [ { id: 1, name: "Test 1" }, { id: 2, name: "Test 2" }, ]; const columns: ColumnDef[] = [ { accessorKey: "name", header: "Name" }, ]; const initialRowSelection: RowSelectionState = { "0": true }; const commonProps = { data: testData, columns, selection: "single" as const, totalRows: 2, totalColumns: 1, pagination: false, rowSelection: initialRowSelection, onRowSelectionChange: mockOnRowSelectionChange, }; const { rerender } = render( , ); // Verify initial selection is not cleared expect(mockOnRowSelectionChange).not.toHaveBeenCalledWith({}); // Simulate remount (as would happen in accordion toggle) rerender( , ); // Verify selection is still not cleared after remount expect(mockOnRowSelectionChange).not.toHaveBeenCalledWith({}); // Verify the rowSelection prop is maintained expect(commonProps.rowSelection).toEqual(initialRowSelection); }); it("applies hoverTemplate to the row title using row values", () => { interface RowData { id: number; first: string; last: string; } const testData: RowData[] = [ { id: 1, first: "Michael", last: "Scott" }, { id: 2, first: "Jim", last: "Halpert" }, ]; const columns: ColumnDef[] = [ { accessorKey: "first", header: "First" }, { accessorKey: "last", header: "Last" }, ]; render( , ); // Grab all rows and assert title attribute computed from template const rows = screen.getAllByRole("row"); // The first row is header; subsequent rows correspond to data expect(rows[1]).toHaveAttribute("title", "Michael Scott"); expect(rows[2]).toHaveAttribute("title", "Jim Halpert"); }); it("does not virtualize small datasets without pagination", () => { const testData = Array.from({ length: 50 }, (_, i) => ({ id: i, name: `Item ${i}`, })); const columns: ColumnDef[] = [ { accessorKey: "id", header: "ID" }, { accessorKey: "name", header: "Name" }, ]; render( , ); // All 50 data rows + 1 header row should be in the DOM (no virtualization) const rows = screen.getAllByRole("row"); expect(rows).toHaveLength(51); }); it("virtualizes large datasets — renders fewer rows than the full dataset", () => { const testData = Array.from({ length: 200 }, (_, i) => ({ id: i, name: `Item ${i}`, })); const columns: ColumnDef[] = [ { accessorKey: "id", header: "ID" }, { accessorKey: "name", header: "Name" }, ]; render( , ); // In jsdom the virtualizer sees a 0-height container and renders 0 data // rows (no layout engine). The key assertion is that significantly fewer // than 200 rows are in the DOM, which catches regressions where // virtualization is accidentally disabled and all rows are rendered. const rows = screen.getAllByRole("row"); // Subtract 1 for the header row const dataRows = rows.length - 1; expect(dataRows).toBeLessThan(200); }); it("should display updated data after rerender with manual sorting and pagination", () => { // Simulates the bug from issue #8023: // When a user sorts a table, rows that moved from page 2 to page 1 // don't visually refresh after the underlying data is updated. interface RowData { id: number; status: string; value: number; } // Initial data: 4 rows, page_size=3 const initialData: RowData[] = [ { id: 4, status: "pending", value: 40 }, { id: 3, status: "pending", value: 30 }, { id: 2, status: "pending", value: 20 }, ]; const columns: ColumnDef[] = [ { id: "id", accessorFn: (row) => row.id, header: "id" }, { id: "status", accessorFn: (row) => row.status, header: "status" }, { id: "value", accessorFn: (row) => row.value, header: "value" }, ]; // Simulate sorted state (value descending) - manual sorting means // data comes pre-sorted from backend const sorting: SortingState = [{ id: "value", desc: true }]; const setSorting = vi.fn(); const paginationState: PaginationState = { pageIndex: 0, pageSize: 3 }; const setPaginationState = vi.fn(); const commonProps = { columns, selection: null as "single" | "multi" | null, totalRows: 4, totalColumns: 3, pagination: true, manualPagination: true, paginationState, setPaginationState, manualSorting: true, sorting, setSorting, }; const { rerender } = render( , ); // Verify initial data is displayed - look for "pending" in cells const rows = screen.getAllByRole("row"); // Row 0 is header, rows 1-3 are data rows expect(rows).toHaveLength(4); // 1 header + 3 data rows // All rows should show "pending" expect(within(rows[1]).getByText("pending")).toBeTruthy(); expect(within(rows[2]).getByText("pending")).toBeTruthy(); expect(within(rows[3]).getByText("pending")).toBeTruthy(); // Now simulate data update: row with id=4 is now "approved" // Backend returns sorted data with the update applied const updatedData: RowData[] = [ { id: 4, status: "approved", value: 40 }, { id: 3, status: "pending", value: 30 }, { id: 2, status: "pending", value: 20 }, ]; // Rerender with updated data (same sorting, same pagination) rerender( , ); // BUG: The row should show "approved" but might show stale "pending" const updatedRows = screen.getAllByRole("row"); expect(updatedRows).toHaveLength(4); // The first data row (id=4) should now show "approved" expect(within(updatedRows[1]).getByText("approved")).toBeTruthy(); // Other rows should still show "pending" expect(within(updatedRows[2]).getByText("pending")).toBeTruthy(); expect(within(updatedRows[3]).getByText("pending")).toBeTruthy(); }); });