2026-10-20 23:34:11 +08:00
import { describe , expect , it , vi } from 'vitest'
2026-10-13 20:44:31 +08:00
import type { GaugeSeriesOption , BarSeriesOption } from 'echarts'
2026-10-20 23:34:11 +08:00
import { adminWorkbenchChartTheme } from '@/styles/adminWorkbenchChartTheme'
2026-10-13 20:44:31 +08:00
import { barChartOption , progressGaugeOption } from './dashboardChartOptions'
describe ( 'self-operated ECharts configurations' , () => {
it ( 'wraps a concise tooltip and escapes source text rather than inserting markup' , () => {
2026-10-20 23:34:11 +08:00
const option = barChartOption ([{ key : 'sku' , label : '<img src=x>' , secondary : 'A&B "\'货主' , value : '12000.123456' , detail : 'very long detail belongs in the dialog' }], '<件>' , '热销' , true )
2026-10-13 20:44:31 +08:00
const tooltip = option . tooltip as { formatter : ( value : { dataIndex : number }) => string ; extraCssText : string }
const content = tooltip . formatter ({ dataIndex : 0 })
expect ( content ). toContain ( '<img src=x>' )
2026-10-20 23:34:11 +08:00
expect ( content ). toContain ( 'A&B "'货主' )
expect ( content ). toContain ( '12,000.123456' )
expect ( content ). toContain ( '<件>' )
expect ( content ). not . toContain ( '<img' )
2026-10-13 20:44:31 +08:00
expect ( content ). not . toContain ( 'very long detail' )
expect ( tooltip . extraCssText ). toContain ( 'white-space: normal' )
2026-10-20 23:34:11 +08:00
expect ( option . tooltip ). toMatchObject ({ backgroundColor : adminWorkbenchChartTheme.colors.surface , borderColor : adminWorkbenchChartTheme.colors.border })
2026-10-13 20:44:31 +08:00
})
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 }])
2026-10-20 23:34:11 +08:00
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 ()
}
2026-10-13 20:44:31 +08:00
})
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 })
2026-10-20 23:34:11 +08:00
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 } 件` )
2026-10-13 20:44:31 +08:00
})
})