import { describe, test, expect, beforeEach, vi } from 'vitest' import { render } from '@testing-library/react' import { Echart } from './echart' import { useWidgetStore } from '../stores/widget-store' import type { EchartWidgetState } from './types' import type { EChartsOption } from 'echarts' import * as echarts from 'echarts' // Mock echarts module vi.mock('echarts', () => { const mockChart = { setOption: vi.fn(), dispose: vi.fn(), resize: vi.fn(), on: vi.fn(), off: vi.fn(), getZr: vi.fn(() => ({ setCursorStyle: vi.fn(), })), } return { init: vi.fn(() => mockChart), } }) // Mock MUI theme vi.mock('@mui/material', async () => { const actual = await vi.importActual('@mui/material') return { ...actual, useTheme: () => ({ spacing: (value: number) => `${value * 8}px`, palette: { secondary: { main: '#47db99', }, grey: { 400: '#bdbdbd', 900: '#212121', }, common: { white: '#ffffff', black: '#000000', }, qualitative: { bold: { 1: '#7F3C8D', 2: '#11A579', 3: '#3969AC', 4: '#F2B701', 5: '#E73F74', 6: '#80BA5A', 7: '#E68310', 8: '#008695', 9: '#CF1C90', 10: '#F97B72', }, }, }, typography: { caption: { fontFamily: 'Roboto, sans-serif', }, }, }), } }) describe('Echart', () => { let mockChart: { setOption: ReturnType dispose: ReturnType resize: ReturnType on: ReturnType off: ReturnType getZr: ReturnType } let mockResizeObserver: { observe: ReturnType disconnect: ReturnType unobserve: ReturnType } const basicOption: EChartsOption = { title: { text: 'Test Chart', }, series: [ { type: 'bar', data: [1, 2, 3, 4, 5], }, ], } beforeEach(() => { vi.clearAllMocks() useWidgetStore.getState().clearWidgets() // Create a fresh mock chart instance for each test mockChart = { setOption: vi.fn(), dispose: vi.fn(), resize: vi.fn(), on: vi.fn(), off: vi.fn(), getZr: vi.fn(() => ({ setCursorStyle: vi.fn(), })), } // Make echarts.init return our mock chart vi.mocked(echarts.init).mockReturnValue( mockChart as unknown as echarts.ECharts, ) // Mock ResizeObserver mockResizeObserver = { observe: vi.fn(), disconnect: vi.fn(), unobserve: vi.fn(), } global.ResizeObserver = class { observe = mockResizeObserver.observe disconnect = mockResizeObserver.disconnect unobserve = mockResizeObserver.unobserve } as unknown as typeof ResizeObserver }) test('returns null when widget does not exist in store', () => { const { container } = render() expect(container.firstChild).toBeNull() }) test('renders EchartUI when widget exists in store', () => { // Set up widget in store useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: basicOption, } as EchartWidgetState) const { container } = render() const div = container.querySelector('div') expect(div).toBeTruthy() expect(echarts.init).toHaveBeenCalled() }) test('passes option from widget data to EchartUI', () => { const customOption: EChartsOption = { title: { text: 'Custom Chart', }, series: [ { type: 'line', data: [10, 20, 30], }, ], } useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: customOption, } as EchartWidgetState) render() expect(mockChart.setOption).toHaveBeenCalledWith( expect.objectContaining({ title: { text: 'Custom Chart', }, series: [ { type: 'line', data: [10, 20, 30], }, ], }), { lazyUpdate: true, notMerge: true, }, ) }) test('passes onEvents from widget to EchartUI', () => { const clickHandler = vi.fn() const hoverHandler = vi.fn() useWidgetStore.getState().setWidget('test-echart', { type: 'echart', option: basicOption, onEvents: { click: clickHandler, mouseover: hoverHandler, }, }) render() expect(mockChart.on).toHaveBeenCalledWith('click', clickHandler) expect(mockChart.on).toHaveBeenCalledWith('mouseover', hoverHandler) }) test('passes init options from widget to EchartUI', () => { useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: basicOption, init: { renderer: 'canvas', height: 500, width: 800, }, } as EchartWidgetState) render() expect(echarts.init).toHaveBeenCalledWith( expect.any(HTMLDivElement), null, expect.objectContaining({ renderer: 'canvas', height: 500, width: 800, }), ) }) test('handles widget with undefined onEvents', () => { useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: basicOption, onEvents: undefined, } as EchartWidgetState) expect(() => { render() }).not.toThrow() }) test('handles widget with undefined init', () => { useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: basicOption, init: undefined, } as EchartWidgetState) render() expect(echarts.init).toHaveBeenCalledWith( expect.any(HTMLDivElement), null, expect.objectContaining({ renderer: 'svg', height: 304, }), ) }) test('re-renders when widget data changes', () => { useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: basicOption, } as EchartWidgetState) const { rerender } = render() expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, { lazyUpdate: true, notMerge: true, }) const newOption: EChartsOption = { series: [ { type: 'pie', data: [{ value: 100 }, { value: 200 }], }, ], } useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: newOption, } as EchartWidgetState) rerender() expect(mockChart.setOption).toHaveBeenCalledWith(newOption, { lazyUpdate: true, notMerge: true, }) }) test('returns null when widget is removed from store', () => { useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: basicOption, } as EchartWidgetState) const { container, rerender } = render() expect(container.querySelector('div')).toBeTruthy() useWidgetStore.getState().removeWidget('test-echart') rerender() expect(container.firstChild).toBeNull() }) test('handles complex chart options with multiple series', () => { const complexOption: EChartsOption = { title: { text: 'Multi-Series Chart', }, tooltip: { trigger: 'axis', }, legend: { data: ['Series 1', 'Series 2', 'Series 3'], }, xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'], }, yAxis: { type: 'value', }, series: [ { name: 'Series 1', type: 'line', data: [10, 20, 30, 40, 50], }, { name: 'Series 2', type: 'bar', data: [15, 25, 35, 45, 55], }, { name: 'Series 3', type: 'scatter', data: [5, 15, 25, 35, 45], }, ], } useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: complexOption, } as EchartWidgetState) render() expect(mockChart.setOption).toHaveBeenCalledWith(complexOption, { lazyUpdate: true, notMerge: true, }) }) test('handles widget with all optional properties', () => { const clickHandler = vi.fn() const fullWidget: EchartWidgetState = { id: 'test-echart', type: 'echart', data: [], isFetching: false, option: basicOption, isLoading: false, visible: true, onEvents: { click: clickHandler, }, init: { renderer: 'canvas', height: 600, }, } useWidgetStore.getState().setWidget('test-echart', fullWidget) render() expect(echarts.init).toHaveBeenCalledWith( expect.any(HTMLDivElement), null, expect.objectContaining({ renderer: 'canvas', height: 600, }), ) expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, { lazyUpdate: true, notMerge: true, }) expect(mockChart.on).toHaveBeenCalledWith('click', clickHandler) }) test('handles multiple Echart instances with different IDs', () => { useWidgetStore.getState().setWidget('chart-1', { id: 'chart-1', type: 'echart', option: basicOption, } as EchartWidgetState) useWidgetStore.getState().setWidget('chart-2', { id: 'chart-2', type: 'echart', option: { series: [{ type: 'line', data: [5, 10, 15] }], }, } as EchartWidgetState) const { container: container1 } = render() const { container: container2 } = render() expect(container1.querySelector('div')).toBeTruthy() expect(container2.querySelector('div')).toBeTruthy() expect(echarts.init).toHaveBeenCalledTimes(2) }) test('handles empty series data', () => { const emptyOption: EChartsOption = { series: [], } useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: emptyOption, } as EchartWidgetState) render() expect(mockChart.setOption).toHaveBeenCalledWith(emptyOption, { lazyUpdate: true, notMerge: true, }) }) test('handles widget data as unknown type (runtime typing)', () => { // widget.data is typed as unknown at compile time but EChartsOption at runtime const widgetData: unknown = basicOption useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: widgetData as EChartsOption, } as EchartWidgetState) render() expect(mockChart.setOption).toHaveBeenCalledWith( expect.objectContaining({ title: basicOption.title, series: basicOption.series, }), { lazyUpdate: true, notMerge: true, }, ) }) test('passes through all event handlers from widget', () => { const handlers = { click: vi.fn(), dblclick: vi.fn(), mousedown: vi.fn(), mouseup: vi.fn(), mouseover: vi.fn(), mouseout: vi.fn(), mousemove: vi.fn(), } useWidgetStore.getState().setWidget('test-echart', { type: 'echart', option: basicOption, onEvents: handlers, }) render() Object.entries(handlers).forEach(([eventName, handler]) => { expect(mockChart.on).toHaveBeenCalledWith(eventName, handler) }) }) test('component updates when switching between different widget IDs', () => { useWidgetStore.getState().setWidget('chart-a', { id: 'chart-a', type: 'echart', option: basicOption, } as EchartWidgetState) useWidgetStore.getState().setWidget('chart-b', { id: 'chart-b', type: 'echart', option: { series: [{ type: 'line', data: [1, 2, 3] }] }, } as EchartWidgetState) const { rerender } = render() expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, { lazyUpdate: true, notMerge: true, }) rerender() expect(mockChart.setOption).toHaveBeenCalledWith( { series: [{ type: 'line', data: [1, 2, 3] }] }, { lazyUpdate: true, notMerge: true, }, ) }) test('disposes chart when component unmounts', () => { useWidgetStore.getState().setWidget('test-echart', { id: 'test-echart', type: 'echart', option: basicOption, } as EchartWidgetState) const { unmount } = render() unmount() expect(mockChart.dispose).toHaveBeenCalled() }) })