import { describe, it, expect } from 'vitest' import type { Theme } from '@mui/material' import { mergeEchartWidgetConfig, getEChartZoomConfig, getEChartBrushConfig, getEChartStackConfig, } from './utils' const theme = { spacing: (n: number) => `${n * 8}`, palette: { common: { white: '#ffffff' }, secondary: { main: '#3366ff' }, black: { 60: 'rgba(0,0,0,0.6)' }, }, typography: { overlineDelicate: { fontSize: '11', fontFamily: 'Inter, sans-serif' }, }, } as unknown as Theme describe('mergeEchartWidgetConfig', () => { it('returns a deep-merged base + override when no custom merger is provided', () => { const a = { title: { text: 'A' }, tooltip: { trigger: 'axis' } } as never const b = { title: { subtext: 'sub' } } as never const merged = mergeEchartWidgetConfig(a, b) as unknown as { title: { text: string; subtext: string } } expect(merged.title.text).toBe('A') expect(merged.title.subtext).toBe('sub') }) it('handles both inputs being undefined (defaults to {})', () => { const merged = mergeEchartWidgetConfig(undefined, undefined) expect(merged).toEqual({}) }) it('replaces `color` arrays rather than concatenating', () => { const merged = mergeEchartWidgetConfig( { color: ['#ff0000', '#00ff00'] }, { color: ['#0000ff'] }, ) expect(merged.color).toEqual(['#0000ff']) }) it('composes `axisLabel` formatter functions so base formatter can suppress', () => { const base = { xAxis: { axisLabel: { formatter: (v: unknown) => (v === 0 ? '' : `BASE-${String(v)}`), }, }, } const override = { xAxis: { axisLabel: { formatter: (v: unknown) => `OVR-${String(v)}`, }, }, } const merged = mergeEchartWidgetConfig(base, override) const fmt = ( merged.xAxis as { axisLabel: { formatter: (v: unknown) => unknown } } ).axisLabel.formatter expect(fmt(0)).toBe('') // base returned '' → suppressed expect(fmt(5)).toBe('OVR-5') // base non-empty → fall through to override }) it('merges plain object axisLabel keys when no formatter present', () => { const merged = mergeEchartWidgetConfig( { xAxis: { axisLabel: { color: 'red' } } }, { xAxis: { axisLabel: { fontSize: 12 } } }, ) const al = (merged.xAxis as { axisLabel: Record }) .axisLabel expect(al.color).toBe('red') expect(al.fontSize).toBe(12) }) it('merges `series` element-by-element rather than concatenating', () => { const a = { series: [ { name: 'a1', data: [1, 2] }, { name: 'a2', data: [3] }, ], } as never const b = { series: [{ barWidth: '60%' }] } as never const merged = mergeEchartWidgetConfig(a, b) as unknown as { series: { name?: string; barWidth?: string; data?: number[] }[] } const s = merged.series expect(s).toHaveLength(2) expect(s[0]!.name).toBe('a1') expect(s[0]!.barWidth).toBe('60%') expect(s[1]!.name).toBe('a2') }) it('handles series merge when one side has no series array (defaults to length 0)', () => { const merged = mergeEchartWidgetConfig( { series: [{ name: 'only' }] }, { series: [] }, ) const s = merged.series as { name?: string }[] expect(s).toHaveLength(1) expect(s[0]!.name).toBe('only') }) it('delegates to a custom merger when provided and it returns a function', () => { const merged = mergeEchartWidgetConfig( { title: { text: 'A' } }, { title: { text: 'B' } }, (key) => key === 'title' ? (_a: unknown, b: unknown) => ({ ...(b as object), custom: true }) : undefined, ) expect((merged.title as { text: string; custom: boolean }).text).toBe('B') expect((merged.title as { text: string; custom: boolean }).custom).toBe( true, ) }) it('falls through to default merger when custom merger returns undefined for the key', () => { const merged = mergeEchartWidgetConfig( { color: ['#a'] }, { color: ['#b'] }, () => undefined, ) expect(merged.color).toEqual(['#b']) }) }) describe('getEChartZoomConfig', () => { it('returns a default config (inside + xSlider) with no args', () => { const cfg = getEChartZoomConfig() expect(cfg.dataZoom).toBeDefined() // inside + xSlider both true → 2 entries expect(cfg.dataZoom.length).toBeGreaterThanOrEqual(2) }) it('honours an explicit range start/end', () => { const cfg = getEChartZoomConfig({ start: 25, end: 75 }) const insideCfg = cfg.dataZoom.find( (z) => (z as { type: string }).type === 'inside', ) as { start: number; end: number } expect(insideCfg.start).toBe(25) expect(insideCfg.end).toBe(75) }) it('emits a vertical inside zoom when ySlider=true and inside=true', () => { const cfg = getEChartZoomConfig( { start: 0, end: 100 }, { inside: true, ySlider: true }, ) const verticalInside = cfg.dataZoom.find( (z) => (z as { type: string; orientation?: string }).type === 'inside' && (z as { orientation?: string }).orientation === 'vertical', ) expect(verticalInside).toBeDefined() }) it('omits the inside entry when inside=false', () => { const cfg = getEChartZoomConfig( { start: 0, end: 100 }, { inside: false, xSlider: true }, ) const insideEntries = cfg.dataZoom.filter( (z) => (z as { type: string }).type === 'inside', ) expect(insideEntries).toHaveLength(0) }) it('omits the xSlider entry when xSlider=false', () => { const cfg = getEChartZoomConfig( { start: 0, end: 100 }, { inside: false, xSlider: false, ySlider: false }, ) expect(cfg.dataZoom).toHaveLength(0) }) it('includes a yAxis slider when ySlider=true', () => { const cfg = getEChartZoomConfig( { start: 0, end: 100 }, { inside: false, xSlider: false, ySlider: true }, ) const yAxisSlider = cfg.dataZoom.find((z) => { const c = z as { type: string; yAxisIndex?: number[] } return c.type === 'slider' && Array.isArray(c.yAxisIndex) }) expect(yAxisSlider).toBeDefined() }) it('respects bottomOffset for the xAxis slider', () => { const cfg = getEChartZoomConfig( { start: 0, end: 100 }, { bottomOffset: 16 }, ) const xSlider = cfg.dataZoom.find( (z) => (z as { type: string; bottom?: number }).type === 'slider' && (z as { bottom?: number }).bottom === 16, ) expect(xSlider).toBeDefined() }) it('threads through xAxisLabelFormatter to the xSlider config', () => { const fmt = (v: number) => `lbl-${v}` const cfg = getEChartZoomConfig( { start: 0, end: 100 }, { xAxisLabelFormatter: fmt }, ) const xSlider = cfg.dataZoom.find( (z) => (z as { type: string; xAxisIndex?: number[] }).type === 'slider' && Array.isArray((z as { xAxisIndex?: number[] }).xAxisIndex), ) as { labelFormatter: typeof fmt } expect(xSlider.labelFormatter).toBe(fmt) }) it('hides sliders when showSliders=false (`show` field is false)', () => { const cfg = getEChartZoomConfig( { start: 0, end: 100 }, { showSliders: false }, ) const xSlider = cfg.dataZoom.find( (z) => (z as { type: string }).type === 'slider', ) as { show: boolean } expect(xSlider.show).toBe(false) }) it('falls back to default spacing strings when theme.spacing is missing', () => { const cfg = getEChartZoomConfig(undefined, undefined, undefined) expect(cfg.dataZoom).toBeDefined() }) it('applies theme-derived slider styles when a theme is provided', () => { const cfg = getEChartZoomConfig({ start: 0, end: 100 }, undefined, theme) const xSlider = cfg.dataZoom.find( (z) => (z as { type: string; xAxisIndex?: number[] }).type === 'slider' && Array.isArray((z as { xAxisIndex?: number[] }).xAxisIndex), ) as unknown as { fillerColor: string } expect(xSlider.fillerColor).toBeDefined() }) }) describe('getEChartBrushConfig', () => { it('returns a default brush config when called with no args', () => { const cfg = getEChartBrushConfig() expect(cfg.brush.brushType).toBe('lineX') expect(cfg.brush.brushMode).toBe('single') expect(cfg.brush.xAxisIndex).toBe(0) }) it('respects explicit brushType, brushMode, and xAxisIndex', () => { const cfg = getEChartBrushConfig({ brushType: 'polygon', brushMode: 'multiple', xAxisIndex: 2, }) expect(cfg.brush.brushType).toBe('polygon') expect(cfg.brush.brushMode).toBe('multiple') expect(cfg.brush.xAxisIndex).toBe(2) }) }) describe('getEChartStackConfig', () => { it('uses the default stack group when no arg passed', () => { expect(getEChartStackConfig().stack).toBe('stacked') }) it('respects an explicit stack group name', () => { expect(getEChartStackConfig('foo').stack).toBe('foo') }) })