/* Copyright 2026 Marimo. All rights reserved. */ import { Tooltip } from "radix-ui"; const TooltipProvider = Tooltip.Provider; import { act, render, screen, waitFor } from "@testing-library/react"; import { Provider } from "jotai"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { SetupMocks } from "@/__mocks__/common"; import type { DownloadAsArgs } from "@/components/data-table/schemas"; import type { FieldTypesWithExternalType } from "@/components/data-table/types"; import { store } from "@/core/state/jotai"; import { type GetDataUrl, type GetRowIds, LoadingDataTableComponent, } from "../DataTablePlugin"; beforeAll(() => { SetupMocks.resizeObserver(); }); describe("LoadingDataTableComponent", () => { /** * Regression test for https://github.com/marimo-team/marimo/issues/8023 * * When a table is replaced via mo.output.replace() with updated data, * but the initial page data (unsorted first page) hasn't changed, * the useAsyncData hook's deps may all remain the same. * Previously, the `search` function reference was memoized on * [plugin.functions, hostElement] and wouldn't change on reset(), * so the useAsyncData effect wouldn't re-fire. * * The fix adds a resetNonce to the functionMethods memo deps, * so when the plugin is reset (table instance changes), the search * function reference changes, triggering useAsyncData to re-fetch. * * This test verifies that when the search function reference changes * (simulating reset()), the component re-fetches data even if * props.data hasn't changed. */ it("should refetch data when search function reference changes", async () => { const host = document.createElement("div"); const setValue = vi.fn(); // The initial page data string - identical for both renders. // This simulates the case where only a row on page 2 changed, // so the first page data is the same. const initialPageData = JSON.stringify([ { id: 1, status: "pending", value: 10 }, { id: 2, status: "pending", value: 20 }, { id: 3, status: "pending", value: 30 }, ]); const searchResult = { data: [ { id: 1, status: "pending", value: 10 }, { id: 2, status: "pending", value: 20 }, { id: 3, status: "pending", value: 30 }, ], total_rows: 4, cell_styles: null, cell_hover_texts: null, }; const searchFn1 = vi.fn().mockResolvedValue(searchResult); const searchFn2 = vi.fn().mockResolvedValue(searchResult); const fieldTypes: FieldTypesWithExternalType = [ ["id", ["integer", "integer"]], ["status", ["string", "string"]], ["value", ["integer", "integer"]], ]; const commonProps = { label: null, totalRows: 4, pagination: true, pageSize: 3, selection: "single" as const, showDownload: false, showFilters: false, showColumnSummaries: false as const, showDataTypes: false, showPageSizeSelector: false, showColumnExplorer: false, showRowExplorer: false, showChartBuilder: false, rowHeaders: [] as FieldTypesWithExternalType, fieldTypes, totalColumns: 3, maxColumns: "all" as const, hasStableRowId: false, lazy: false, host, enableSearch: true, value: [] as (number | string | { rowId: string; columnName?: string })[], setValue, download_as: vi.fn() as DownloadAsArgs, get_column_summaries: vi.fn().mockResolvedValue({ data: null, stats: {}, bin_values: {}, value_counts: {}, show_charts: false, }), get_data_url: vi.fn() as GetDataUrl, get_row_ids: vi.fn() as GetRowIds, }; const Wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); // Render with first search function const { rerender } = render( , ); // Wait for the table to render with data await waitFor(() => { expect(screen.getAllByRole("row").length).toBeGreaterThan(1); }); // Search was called on initial load (fire-and-forget for canShowInitialPage) expect(searchFn1).toHaveBeenCalled(); // Now rerender with the same data but a NEW search function reference. // This simulates what happens after reset() when resetNonce increments // and functionMethods is recreated. await act(async () => { rerender( , ); }); // The new search function should be called because the search // dependency changed in useAsyncData. await waitFor(() => { expect(searchFn2).toHaveBeenCalled(); }); }); });