/* Copyright 2026 Marimo. All rights reserved. */ import { fireEvent, render, waitFor } from "@testing-library/react"; import { createStore, Provider } from "jotai"; import { Tooltip } from "radix-ui"; import type { ComponentProps } from "react"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { SetupMocks } from "@/__mocks__/common"; import { cellId } from "@/__tests__/branded"; import { LazyVegaEmbed } from "@/components/charts/lazy"; import { vegaLoader } from "@/plugins/impl/vega/loader"; import { ChartPanel, TablePanel, type TablePanelProps } from "../charts"; import { getChartTabName, tabsStorageAtom } from "../storage"; import { ChartType, NONE_VALUE } from "../types"; vi.mock("@/components/charts/lazy", () => ({ LazyVegaEmbed: vi.fn(() => null), })); beforeAll(() => { SetupMocks.resizeObserver(); }); describe("ChartPanel", () => { it.each([undefined, null, []] as const)( "infers the full CSV through TablePanel without backend metadata (%s)", async (fieldTypes) => { vi.spyOn(vegaLoader, "load").mockResolvedValue( "value,index\n,0\n2.5,1\n", ); const store = createStore(); const id = cellId("csv-inference"); const tabName = getChartTabName(0, ChartType.BAR); store.set( tabsStorageAtom, new Map([ [ id, [ { tabName, chartType: ChartType.BAR, config: { general: { xColumn: { field: "value", type: "number" }, yColumn: { field: "index", type: "integer", aggregate: NONE_VALUE, }, }, }, }, ], ], ]), ); const sample = [{ value: null }]; const SampleTable = (_props: { data: typeof sample }) => null; const props: TablePanelProps = { cellId: id, data: sample, dataTable: , totalRows: 2, columns: 1, displayHeader: true, fieldTypes: fieldTypes == null ? fieldTypes : [...fieldTypes], rowHeaders: [["index", ["integer", "int64"]]], getDataUrl: vi .fn() .mockResolvedValue({ data_url: "chart.csv", format: "csv" }), }; const { getByRole } = render( , ); fireEvent.click(getByRole("tab", { name: tabName })); await waitFor(() => { expect(vi.mocked(LazyVegaEmbed).mock.lastCall?.[0].spec).toEqual( expect.objectContaining({ data: { values: [ { value: null, index: 0 }, { value: 2.5, index: 1 }, ], }, }), ); }); }, ); it("reloads CSV only when column types change, not their array identity", async () => { vi.spyOn(vegaLoader, "load").mockResolvedValue("value\n001\n"); const getDataUrl = vi.fn().mockResolvedValue({ data_url: "chart.csv", format: "csv", }); const props: ComponentProps = { tableData: [{ value: "001" }], chartConfig: { general: { xColumn: { field: "value", type: "string" }, yColumn: { field: "value", type: "integer", aggregate: NONE_VALUE }, }, }, chartType: ChartType.BAR, saveChart: vi.fn(), saveChartType: vi.fn(), getDataUrl, isLargeDataset: false, }; const panel = ( fieldTypes: ComponentProps["fieldTypes"], ) => ( ); const expectValues = async (value: string | number) => { await waitFor(() => { expect(vi.mocked(LazyVegaEmbed).mock.lastCall?.[0].spec).toEqual( expect.objectContaining({ data: { values: [{ value }] } }), ); }); }; const { rerender } = render(panel([["value", ["string", "object"]]])); await expectValues("001"); expect(getDataUrl).toHaveBeenCalledTimes(1); rerender(panel([["value", ["string", "object"]]])); await expectValues("001"); expect(getDataUrl).toHaveBeenCalledTimes(1); rerender(panel([["value", ["integer", "int64"]]])); await expectValues(1); expect(getDataUrl).toHaveBeenCalledTimes(2); }); it("uses column types to parse numeric CSV values without coercing text", async () => { vi.spyOn(vegaLoader, "load").mockResolvedValue( "a.b,n,label,day,timestamp,active,duration\ninf,1,inf,2024-01-01,2024-01-01T12:00:00Z,True,1 days\n-inf,2,001,2024-01-02,2024-01-02T12:00:00Z,False,2 days\n2.5,3,2024-01-03,2024-01-03,2024-01-03T12:00:00Z,True,3 days\n,4,,,,,\n", ); render( , ); await waitFor(() => { expect(vi.mocked(LazyVegaEmbed).mock.lastCall?.[0].spec).toEqual( expect.objectContaining({ data: { values: [ { "a.b": Infinity, n: 1, label: "inf", day: new Date("2024-01-01"), timestamp: new Date("2024-01-01T12:00:00Z"), active: true, duration: "1 days", }, { "a.b": -Infinity, n: 2, label: "001", day: new Date("2024-01-02"), timestamp: new Date("2024-01-02T12:00:00Z"), active: false, duration: "2 days", }, { "a.b": 2.5, n: 3, label: "2024-01-03", day: new Date("2024-01-03"), timestamp: new Date("2024-01-03T12:00:00Z"), active: true, duration: "3 days", }, { "a.b": null, n: 4, label: null, day: "", timestamp: "", active: null, duration: "", }, ], }, }), ); }); }); it.each([true, false])( "keeps dotted CSV columns aligned with the chart encodings (schema: %s)", async (hasSchema) => { vi.spyOn(vegaLoader, "load").mockResolvedValue("a.b,n\n1,1\n2,2\n3,3\n"); render( , ); await waitFor(() => { expect(vi.mocked(LazyVegaEmbed).mock.lastCall?.[0].spec).toEqual( expect.objectContaining({ data: { values: [ { "a.b": 1, n: 1 }, { "a.b": 2, n: 2 }, { "a.b": 3, n: 3 }, ], }, encoding: expect.objectContaining({ x: expect.objectContaining({ field: "a\\.b" }), y: expect.objectContaining({ field: "n" }), }), }), ); }); }, ); });