import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { createDataset } from '@core/dataset'; import { useChartBuilderStore } from '../stores/ChartBuilderStore'; import { useDatasetStore } from '../stores/DatasetStore'; import { useSnippetStore } from '../stores/SnippetStore'; import { ChartBuilderModal } from './ChartBuilderModal'; // The builder preview embeds a real Vega chart in an effect; stub the renderer so // this render test stays a pure React/DOM check (the loop we guard against happens // during commit, long before any chart is drawn). vi.mock('../services/chart-renderer', () => ({ renderSpec: () => Promise.resolve({ destroy() {}, resize() {} }), })); // React 19 wants this flag set for act() to drive effects without warnings. (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const T = new Date('2026-06-01T00:00:00Z'); let container: HTMLDivElement; let root: Root; beforeEach(() => { useChartBuilderStore.getState().reset(); useDatasetStore.getState().reset(); useSnippetStore.getState().reset(); container = document.createElement('div'); document.body.appendChild(container); root = createRoot(container); }); afterEach(() => { act(() => root.unmount()); container.remove(); }); describe('ChartBuilderModal', () => { test('renders without an infinite update loop when the config has warnings (regression)', async () => { // Two numeric columns → default mark Point (clean). Switching to Bar makes it // "two measures on a non-scatter" → a NON-EMPTY warnings array — the exact // condition that previously looped because the warnings selector returned a // fresh array of objects on every render. The fix derives warnings via useMemo // over the stable `config` reference instead. const ds = createDataset({ name: 'Nums', data: [ { a: 1, b: 2 }, { a: 3, b: 4 }, ], format: 'json', source: 'inline', now: T, }); useDatasetStore.getState().add(ds); useChartBuilderStore.getState().init(ds.id); useChartBuilderStore.getState().setMark('bar'); expect(useChartBuilderStore.getState().config.mark).toBe('bar'); // If the component looped, this act() would throw "Maximum update depth exceeded". await act(async () => { root.render(); await Promise.resolve(); }); expect(container.textContent).toContain('Building from'); expect(container.textContent).toContain('scatter'); // the guidance hint rendered }); test('shows the empty state when no dataset is loaded', async () => { await act(async () => { root.render(); await Promise.resolve(); }); expect(container.textContent).toContain('No dataset loaded'); }); });