Files
admin-web/src/modules/self-operated/dashboardChartOptions.test.ts
T
2026-10-20 23:34:11 +08:00

120 lines
7.2 KiB
TypeScript

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: '<img src=x>', 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('&lt;img src=x&gt;')
expect(content).toContain('A&amp;B &quot;&#39;货主')
expect(content).toContain('12,000.123456')
expect(content).toContain('&lt;件&gt;')
expect(content).not.toContain('<img')
expect(content).not.toContain('very long detail')
expect(tooltip.extraCssText).toContain('white-space: normal')
expect(option.tooltip).toMatchObject({ backgroundColor: adminWorkbenchChartTheme.colors.surface, borderColor: adminWorkbenchChartTheme.colors.border })
})
it('renders unknown progress as an empty track without a zero progress value', () => {
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} 件`)
})
})