/* Copyright 2026 Marimo. All rights reserved. */ import { type ColumnDef, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { fireEvent, render, screen, within } from "@testing-library/react"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { TooltipProvider } from "@/components/ui/tooltip"; import { ColumnExplorerPanel } from "../column-explorer-panel/column-explorer"; import type { FieldTypesWithExternalType } from "../types"; beforeAll(() => { global.HTMLElement.prototype.scrollIntoView = () => {}; if (!global.HTMLElement.prototype.hasPointerCapture) { global.HTMLElement.prototype.hasPointerCapture = () => false; } }); const FIELD_TYPES: FieldTypesWithExternalType = [ ["customer_name", ["string", "str"]], ["cust_age", ["integer", "int"]], ["order_total", ["number", "float"]], ]; type Row = Record; const EMPTY_HIDDEN_COLUMNS: readonly string[] = []; const TEST_COLUMNS: ColumnDef[] = [ { id: "customer_name", accessorKey: "customer_name" }, { id: "cust_age", accessorKey: "cust_age" }, { id: "order_total", accessorKey: "order_total" }, ]; interface HarnessProps { totalColumns?: number; initiallyHidden?: readonly string[]; } function PanelHarness({ totalColumns = 3, initiallyHidden = EMPTY_HIDDEN_COLUMNS, }: HarnessProps) { const table = useReactTable({ data: [], columns: TEST_COLUMNS, getCoreRowModel: getCoreRowModel(), locale: "en-US", initialState: { columnVisibility: Object.fromEntries( initiallyHidden.map((id) => [id, false]), ), }, }); return ( ); } function renderPanel(props?: HarnessProps) { return render( , ); } function getSearchInput() { return screen.getByPlaceholderText("Search columns..."); } function getColumnRow(columnName: string): HTMLElement { const row = screen.getByText(columnName).closest('[role="option"]'); if (!row) { throw new Error(`No row for column ${columnName}`); } return row as HTMLElement; } function getShowOnlyButton(columnName: string): HTMLElement { return within(getColumnRow(columnName)).getByRole("button", { name: "Show only this column", }); } describe("ColumnExplorerPanel search", () => { it("shows all columns when search is empty", () => { renderPanel(); expect(screen.getByText("customer_name")).toBeInTheDocument(); expect(screen.getByText("cust_age")).toBeInTheDocument(); expect(screen.getByText("order_total")).toBeInTheDocument(); }); it("matches a word prefix against any column word", () => { renderPanel(); fireEvent.change(getSearchInput(), { target: { value: "cust" } }); expect(screen.getByText("customer_name")).toBeInTheDocument(); expect(screen.getByText("cust_age")).toBeInTheDocument(); expect(screen.queryByText("order_total")).not.toBeInTheDocument(); }); it("matches multi-word queries across column words in any order", () => { renderPanel(); fireEvent.change(getSearchInput(), { target: { value: "name cust" } }); expect(screen.getByText("customer_name")).toBeInTheDocument(); expect(screen.queryByText("cust_age")).not.toBeInTheDocument(); expect(screen.queryByText("order_total")).not.toBeInTheDocument(); }); it("filters out columns that don't match any needle word", () => { renderPanel(); fireEvent.change(getSearchInput(), { target: { value: "xyz" } }); expect(screen.queryByText("customer_name")).not.toBeInTheDocument(); expect(screen.queryByText("cust_age")).not.toBeInTheDocument(); expect(screen.queryByText("order_total")).not.toBeInTheDocument(); }); }); describe("ColumnExplorerPanel header counts", () => { it("uses rendered-subset total when a clipped column is hidden", () => { // Dataset has 100 columns server-side; only 3 are rendered into the // TanStack table (the clipped subset). Hiding one of the rendered columns // must report "2 visible (1 hidden)", not "99 visible (1 hidden)". renderPanel({ totalColumns: 100, initiallyHidden: ["cust_age"] }); expect(screen.getByText(/2 visible/)).toBeInTheDocument(); expect(screen.getByText(/\(1 hidden\)/)).toBeInTheDocument(); expect(screen.queryByText(/99 visible/)).not.toBeInTheDocument(); }); it("uses dataset-wide total when no column is hidden", () => { renderPanel({ totalColumns: 100 }); expect(screen.getByText(/100 columns/)).toBeInTheDocument(); expect(screen.queryByText(/hidden/)).not.toBeInTheDocument(); }); }); describe("ColumnExplorerPanel visibility actions", () => { const showAll = () => screen.getByRole("button", { name: /Show all/ }); const hideAll = () => screen.getByRole("button", { name: /Hide all/ }); it("disables 'Show all' when every column is visible", () => { renderPanel(); expect(showAll()).toBeDisabled(); expect(hideAll()).toBeEnabled(); }); it("disables 'Hide all' when every column is hidden", () => { renderPanel({ initiallyHidden: ["customer_name", "cust_age", "order_total"], }); expect(showAll()).toBeEnabled(); expect(hideAll()).toBeDisabled(); }); it("enables both actions when some columns are hidden", () => { renderPanel({ initiallyHidden: ["cust_age"] }); expect(showAll()).toBeEnabled(); expect(hideAll()).toBeEnabled(); }); it("show-only icon isolates one column", () => { renderPanel(); fireEvent.click(getShowOnlyButton("customer_name")); expect( within(getColumnRow("customer_name")).getByRole("button", { name: "Hide column", }), ).toBeInTheDocument(); expect( within(getColumnRow("cust_age")).getByRole("button", { name: "Show column", }), ).toBeInTheDocument(); expect( within(getColumnRow("order_total")).getByRole("button", { name: "Show column", }), ).toBeInTheDocument(); }); it("disables show-only icon when the column is already alone", () => { renderPanel({ initiallyHidden: ["cust_age", "order_total"], }); expect(getShowOnlyButton("customer_name")).toHaveAttribute( "aria-disabled", "true", ); }); it("disabled show-only icon does not toggle explorer row expansion", () => { renderPanel({ initiallyHidden: ["cust_age", "order_total"], }); const row = getColumnRow("customer_name"); expect(row.querySelector(".lucide-chevron-down")).not.toBeNull(); const button = getShowOnlyButton("customer_name"); fireEvent.pointerDown(button, { pointerId: 1, bubbles: true }); fireEvent.click(button, { bubbles: true }); expect(row.querySelector(".lucide-chevron-down")).not.toBeNull(); }); });