import { useEffect } from 'react' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { act, render } from '@testing-library/react' import type * as echarts from 'echarts' import { Provider } from '../provider/widget-provider' import { Echart } from './echart' import type { OptionFactory } from './echart' import { clearAllWidgetStores, getEchartInstance, getWidgetStore, } from '../stores' // Capture the option that EchartUI receives, since we don't render real ECharts in tests. const captured: { option: echarts.EChartsOption | null replaceMerge: readonly string[] | null setOptions: number } = { option: null, replaceMerge: null, setOptions: 0, } vi.mock('./echart-ui', () => ({ EchartUI: ({ option, replaceMerge, onInstance, }: { option: echarts.EChartsOption replaceMerge: readonly string[] onInstance?: (chart: echarts.ECharts | null) => void }) => { captured.option = option captured.replaceMerge = replaceMerge captured.setOptions++ // Mimic the real bridge: publish a fake instance on mount, clear on // unmount. Cast through unknown so the test doesn't depend on the full // ECharts surface. useEffect(() => { const fake = { __mock: true } as unknown as echarts.ECharts onInstance?.(fake) return () => onInstance?.(null) }, [onInstance]) return null }, DEFAULT_INIT_OPTS: { renderer: 'svg', height: 304 }, })) beforeEach(() => { clearAllWidgetStores() captured.option = null captured.replaceMerge = null captured.setOptions = 0 }) afterEach(() => clearAllWidgetStores()) // Helper: build an OptionFactory whose structural branch returns `structural` // and whose merge branch passes the post-config-transforms option through // `mergeData(option, data, ctx)`. const makeFactory = ( structural: echarts.EChartsOption, mergeData: ( option: echarts.EChartsOption, data: unknown, ) => echarts.EChartsOption, ): OptionFactory => { return (option, data) => { if (option == null) return structural return mergeData(option, data) } } describe(' bridge — owns the option factory directly', () => { it('forwards the structural option to EchartUI synchronously on first render', () => { const factory = makeFactory( { xAxis: { type: 'category' } } as echarts.EChartsOption, // Merge branch: return the option as-is so the structural option // flows through to ECharts unchanged. (option) => option, ) render( , ) expect(captured.option).toEqual({ xAxis: { type: 'category' } }) // No `rawOptions` on the store — the option pipeline is Echart-local now. expect( (getWidgetStore('e1').getState() as unknown as { rawOptions?: unknown }) .rawOptions, ).toBeUndefined() }) it('fuses state.data into the option via the factory merge branch (dataset)', () => { const factory = makeFactory( { xAxis: { type: 'category' } } as echarts.EChartsOption, (option, data) => ({ ...option, dataset: Array.isArray(data) ? (data as { name: string; value: number }[][]).map((source) => ({ source, })) : [], }) as echarts.EChartsOption, ) render( , ) const captured2 = captured.option as { dataset: { source: unknown }[] } expect(captured2.dataset).toHaveLength(1) expect(captured2.dataset[0]?.source).toEqual([{ name: 'A', value: 1 }]) }) it('updates the option when a data transform changes state.data (no re-init)', () => { interface Row { name: string value: number } const factory = makeFactory( { xAxis: { type: 'category' } } as echarts.EChartsOption, (option, data) => ({ ...option, // Mark each dataset with the record count so changes are observable. dataset: Array.isArray(data) ? (data as Row[][]).map((s) => ({ source: s, id: `len:${s.length}`, })) : [], }) as echarts.EChartsOption, ) render( , ) // Before transform: 3 records. let dataset = (captured.option as { dataset: { id: string }[] }).dataset expect(dataset[0]?.id).toBe('len:3') // Register a data transform that filters out half the records. act(() => { getWidgetStore('e3').setState({ dataTransforms: [ { id: 'cut', type: 'data', order: 1, enabled: true, fn: (input) => { const arr = input as { name: string; value: number }[][] return arr.map((s) => s.slice(0, 1)) }, }, ], }) }) // Bridge should have re-rendered with post-pipeline data: 1 record. dataset = (captured.option as { dataset: { id: string }[] }).dataset expect(dataset[0]?.id).toBe('len:1') }) it('applies configTransforms to the rendered option and forwards replaceMerge', () => { const factory = makeFactory( { xAxis: { type: 'category' } } as echarts.EChartsOption, (option) => option, ) render( , ) // Before transform: untouched structural; replaceMerge is the default. expect(captured.option).toEqual({ xAxis: { type: 'category' } }) expect(captured.replaceMerge).toEqual(['toolbox']) // Register a config transform that stamps a field onto the option and // contributes a `replaceMergeKeys` entry. act(() => { getWidgetStore('e-cfg').setState({ configTransforms: [ { id: 'stamp', type: 'config', order: 1, enabled: true, replaceMergeKeys: ['stamp'], fn: (o) => ({ ...(o as object), stamp: true }), }, ], }) }) // After transform: option carries the mutation, replaceMerge is // deduped+sorted with the default and the transform's keys. expect((captured.option as { stamp?: boolean }).stamp).toBe(true) expect(captured.replaceMerge).toEqual(['stamp', 'toolbox']) }) it('publishes the live ECharts instance to the per-id registry on mount and clears on unmount', () => { const factory = makeFactory({} as echarts.EChartsOption, (option) => option) const { unmount } = render( , ) expect(getEchartInstance('e-instance')).not.toBeNull() unmount() expect(getEchartInstance('e-instance')).toBeNull() }) })