import { describe, expect, it, vi } from 'vitest' import type { GaugeSeriesOption, BarSeriesOption } from 'echarts' import { adminWorkbenchChartTheme } from '@/styles/adminWorkbenchChartTheme' import { barChartOption, progressGaugeOption } from './dashboardChartOptions' describe('self-operated ECharts configurations', () => { it('wraps a concise tooltip and escapes source text rather than inserting markup', () => { const option = barChartOption([{ key: 'sku', label: '', secondary: 'A&B "\'货主', value: '12000.123456', detail: 'very long detail belongs in the dialog' }], '<件>', '热销', true) const tooltip = option.tooltip as { formatter: (value: { dataIndex: number }) => string; extraCssText: string } const content = tooltip.formatter({ dataIndex: 0 }) expect(content).toContain('<img src=x>') expect(content).toContain('A&B "'货主') expect(content).toContain('12,000.123456') expect(content).toContain('<件>') expect(content).not.toContain(' { const unknown = (progressGaugeOption(null).series as GaugeSeriesOption[])[0]! expect(unknown.progress?.show).toBe(false) expect(unknown.data).toEqual([]) const known = (progressGaugeOption(12.1).series as GaugeSeriesOption[])[0]! expect(known.progress?.show).toBe(true) expect(known.data).toEqual([{ value: 12.1 }]) expect(known.itemStyle).toMatchObject({ color: adminWorkbenchChartTheme.colors.primary }) expect(known.axisLine?.lineStyle?.color).toEqual([[1, adminWorkbenchChartTheme.colors.primarySoft]]) const zero = (progressGaugeOption(0).series as GaugeSeriesOption[])[0]! expect(zero.progress?.show).toBe(true) expect(zero.data).toEqual([{ value: 0 }]) }) it('keeps indeterminate activity distinct from known completion and honors reduced motion', () => { const matchMedia = vi.fn(() => ({ matches: true })) vi.stubGlobal('window', { matchMedia }) try { const running = progressGaugeOption(null, true) expect(running.animation).toBe(false) expect((running.series as GaugeSeriesOption[])[0]).toMatchObject({ progress: { show: true, roundCap: true }, data: [{ value: 18 }], }) expect(progressGaugeOption(12.1).animation).toBe(false) expect(matchMedia).toHaveBeenCalledWith('(prefers-reduced-motion: reduce)') matchMedia.mockReturnValue({ matches: false }) expect(progressGaugeOption(12.1).animation).toBe(true) expect(progressGaugeOption(null, true).animation).toBe(false) } finally { vi.unstubAllGlobals() } }) it('keeps the seven bucket order, zero, null and common numeric scale', () => { const rows = Array.from({ length: 7 }, (_, index) => ({ key: `bucket-${index}`, label: `分组${index}`, value: index === 6 ? null : index, detail: '' })) const option = barChartOption(rows, 'SKU', '库存分布') expect(option.yAxis).toMatchObject({ inverse: true, data: rows.map(row => row.key) }) expect(option.xAxis).toMatchObject({ type: 'value', min: 0 }) expect((option.series as BarSeriesOption[])[0]?.data?.[0]).toMatchObject({ value: 0 }) expect((option.series as BarSeriesOption[])[0]?.data?.[6]).toMatchObject({ value: null }) expect((option.series as BarSeriesOption[])[0]).not.toHaveProperty('barMinHeight') expect((option.series as BarSeriesOption[])[0]).not.toHaveProperty('showBackground') }) it('assigns bucket colors by semantic key even when the rows are reordered', () => { const expected: Array<[string, string]> = [ ['UNKNOWN', adminWorkbenchChartTheme.colors.unknown], ['DAYS_GT_90', adminWorkbenchChartTheme.colors.teal], ['NO_AVAILABLE', adminWorkbenchChartTheme.colors.rose], ['DAYS_30_60', adminWorkbenchChartTheme.colors.primaryLight], ['NO_OUTBOUND_IN_WINDOW', adminWorkbenchChartTheme.colors.amber], ['DAYS_0_30', adminWorkbenchChartTheme.colors.primary], ['DAYS_60_90', adminWorkbenchChartTheme.colors.tealLight], ] const rows = expected.map(([key]) => ({ key, label: key, value: 1, detail: '' })) for (const orderedRows of [rows, [...rows].reverse()]) { const option = barChartOption(orderedRows, 'SKU', '库存分布') const data = (option.series as BarSeriesOption[])[0]!.data! for (const [index, row] of orderedRows.entries()) { expect(data[index]).toMatchObject({ itemStyle: { color: expected.find(([key]) => key === row.key)?.[1] } }) } } }) it('uses one blue for ranking and preserves real values instead of normalizing bar lengths', () => { const option = barChartOption([ { key: 'large', label: 'SKU-LARGE', value: '15000', detail: '' }, { key: 'small', label: 'SKU-SMALL', value: '150', detail: '' }, { key: 'zero', label: 'SKU-ZERO', value: 0, detail: '' }, { key: 'unknown', label: 'SKU-UNKNOWN', value: null, detail: '' }, ], '件', '热销', true) const series = (option.series as BarSeriesOption[])[0]! expect(series.data).toEqual([15000, 150, 0, null].map(value => ({ value, itemStyle: { color: adminWorkbenchChartTheme.colors.primary, borderRadius: [0, 6, 6, 0] }, }))) expect(series).not.toHaveProperty('barMinHeight') expect(series).not.toHaveProperty('showBackground') expect(option.xAxis).toMatchObject({ min: 0 }) const label = series.label as { formatter: (value: { dataIndex: number }) => string } expect(label.formatter({ dataIndex: 0 })).toBe('15,000') expect(label.formatter({ dataIndex: 2 })).toBe('0') expect(label.formatter({ dataIndex: 3 })).toBe('—') }) it('rounds day labels and tooltips to two decimals while retaining the full chart value', () => { const option = barChartOption([{ key: 'sku', label: 'SKU', value: '1234.56789', detail: '' }], '天', '预计消化时间', true) const series = (option.series as BarSeriesOption[])[0]! expect(series.data?.[0]).toMatchObject({ value: 1234.56789 }) const label = series.label as { formatter: (value: { dataIndex: number }) => string } const tooltip = option.tooltip as { formatter: (value: { dataIndex: number }) => string } expect(label.formatter({ dataIndex: 0 })).toBe('1,234.57') expect(tooltip.formatter({ dataIndex: 0 })).toContain('1,234.57 天') }) it('retains every quantity digit in chart labels and tooltips for values beyond safe integers', () => { const value = '12345678901234567890.123450000001' const expected = '12,345,678,901,234,567,890.123450000001' const option = barChartOption([{ key: 'sku', label: 'SKU', value, detail: '' }], '件', '热销', true) const series = (option.series as BarSeriesOption[])[0]! const label = series.label as { formatter: (value: { dataIndex: number }) => string } const tooltip = option.tooltip as { formatter: (value: { dataIndex: number }) => string } expect(label.formatter({ dataIndex: 0 })).toBe(expected) expect(tooltip.formatter({ dataIndex: 0 })).toContain(`${expected} 件`) }) })