import { afterEach, expect, it, vi } from 'vitest' import { mount, type VueWrapper } from '@vue/test-utils' import type { EChartsOption, LegendComponentOption, PieSeriesOption } from 'echarts' import TripOperationsCharts from './TripOperationsCharts.vue' import { adminWorkbenchChartTheme as chartTheme } from '@/styles/adminWorkbenchChartTheme' import type { TripOperationsCharts as Charts, TripOperationsSummary } from '../api/tripOperations' vi.mock('vue-echarts', () => ({ default: { name: 'VChart', props: ['option', 'initOptions', 'updateOptions', 'autoresize'], template: '
' } })) const wrappers: VueWrapper[] = [] const emptyCharts = (): Charts => ({ physicalPallets: [], destinationTrips: [], externalAgreedAmounts: [], externalPalletRates: [] }) const summary = (): TripOperationsSummary => ({ trips: 20, inTransit: 5, completed: 15, physicalPallets: 90, palletSpaces: 100, ownPackages: 50, externalPackages: 20, externalAgreedAmounts: [{ currency: 'CNY', amount: 390 }, { currency: 'USD', amount: 100 }], costs: { visible: false, unrecordedTrips: null, unallocatedTrips: null, totals: [] } }) function render(charts: Charts, totals = summary()) { const wrapper = mount(TripOperationsCharts, { props: { charts, summary: totals }, global: { stubs: { ElButton: { template: '' }, } } }) wrappers.push(wrapper) return wrapper } const option = (wrapper: VueWrapper, selector: string) => wrapper.find(selector).findComponent({ name: 'VChart' }).props('option') as EChartsOption const series = (options: EChartsOption) => (options.series as PieSeriesOption[])[0]! afterEach(() => wrappers.splice(0).forEach(wrapper => wrapper.unmount())) it('uses four ECharts pies with compact native legends, accurate server totals and daily rates', () => { const charts = emptyCharts() charts.physicalPallets = [{ label: '每日实运托盘', unit: '托', points: [{ label: '2026-10-02', value: 10 }] }] charts.destinationTrips = [{ label: '目的地涉及车次', unit: '次', points: [{ label: 'ONT8', value: 3 }, { label: 'ABE2', value: 8 }] }] charts.externalAgreedAmounts = [{ label: 'CNY', unit: 'CNY', points: [{ label: '2026-10-02', value: 390 }] }] charts.externalPalletRates = [{ label: 'CNY', unit: 'CNY/标准托位', points: [{ label: '2026-10-07', value: 30 }, { label: '2026-10-02', value: 90 }] }] const original = structuredClone(charts) const wrapper = render(charts) expect(wrapper.findAllComponents({ name: 'VChart' })).toHaveLength(4) for (const chart of wrapper.findAllComponents({ name: 'VChart' })) { expect(series(chart.props('option'))).toMatchObject({ type: 'pie', label: { show: false } }) expect(chart.props('initOptions')).toEqual({ renderer: 'canvas' }) expect(chart.props('updateOptions')).toEqual({ notMerge: true }) } expect(wrapper.find('svg').exists()).toBe(false) expect(wrapper.find('.series-caption').exists()).toBe(false) expect(wrapper.find('.chart > .chart-note').exists()).toBe(false) expect(wrapper.find('.chart--pallets .chart-metric').text()).toBe('90 托') expect(wrapper.find('.chart--rates .chart-metric').text()).toContain('CNY/标准托位 30.00') expect(wrapper.find('.chart--rates .chart-metric').attributes('title')).toContain('最近统计日:2026-10-07') const legend = option(wrapper, '.chart--pallets').legend as LegendComponentOption expect(legend.icon).toBe('circle') expect(typeof legend.formatter === 'function' && legend.formatter('2026-10-02')).toBe('10-02') expect(option(wrapper, '.chart--pallets').tooltip).toMatchObject({ trigger: 'item', confine: true, renderMode: 'richText' }) expect(series(option(wrapper, '.chart--rates')).data).toEqual([ { name: '2026-10-02', value: 90, itemStyle: { color: chartTheme.colors.primary } }, { name: '2026-10-07', value: 30, itemStyle: { color: chartTheme.colors.teal } }, ]) expect(charts).toEqual(original) }) it('switches currency without combining monetary amounts or leaving stale ECharts slices', async () => { const charts = emptyCharts() charts.externalAgreedAmounts = [{ label: 'CNY', unit: 'CNY', points: [{ label: '2026-10-02', value: 390 }] }, { label: 'USD', unit: 'USD', points: [{ label: '2026-10-03', value: 100 }] }] const wrapper = render(charts) expect(wrapper.find('.chart--amounts .chart-metric').text()).toBe('CNY 390.00') expect(wrapper.find('.chart--amounts').findAllComponents({ name: 'VChart' })).toHaveLength(1) const usd = wrapper.findAll('.chart--amounts .series-selector button').find(button => button.text() === 'USD')! await usd.trigger('click') expect(usd.attributes('aria-pressed')).toBe('true') expect(wrapper.find('.chart--amounts .chart-metric').text()).toBe('USD 100.00') expect(series(option(wrapper, '.chart--amounts')).data).toEqual([{ name: '2026-10-03', value: 100, itemStyle: { color: chartTheme.colors.primary } }]) await wrapper.setProps({ charts: { ...charts, externalAgreedAmounts: [charts.externalAgreedAmounts[0]!] } }) expect(wrapper.find('.chart--amounts .chart-metric').text()).toBe('CNY 390.00') }) it('keeps every destination in its pie and exposes full legend data only on demand', async () => { const charts = emptyCharts() charts.destinationTrips = [{ label: '目的地涉及车次', unit: '次', points: Array.from({ length: 7 }, (_, index) => ({ label: `FBA-${index}`, value: index + 1 })) }] const wrapper = render(charts) expect(wrapper.find('.chart--destinations .chart-metric').text()).toBe('7 个目的地') const options = option(wrapper, '.chart--destinations') expect(series(options).data).toHaveLength(7) expect((options.legend as LegendComponentOption).data).toHaveLength(5) expect(wrapper.find('.chart-details').exists()).toBe(false) await wrapper.find('.chart-more').trigger('click') expect(wrapper.emitted('show-details')).toHaveLength(1) await wrapper.setProps({ detailsVisible: true }) expect(wrapper.find('[aria-label="派送目的地明细"]').findAll('tbody tr')).toHaveLength(7) expect(wrapper.find('[aria-label="派送目的地明细"]').text()).toContain('FBA-0') }) it('preserves known zeros and does not draw equal positive shares for an empty sum', () => { const charts = emptyCharts() charts.physicalPallets = [{ label: '托盘', unit: '托', points: [{ label: '2026-10-01', value: 0 }, { label: '2026-10-02', value: 2 }] }] charts.externalPalletRates = [{ label: 'USD', unit: 'USD/标准托位', points: [{ label: '2026-10-01', value: 0 }] }] const wrapper = render(charts) expect(series(option(wrapper, '.chart--rates'))).toMatchObject({ stillShowZeroSum: false, showEmptyCircle: true, data: [{ name: '2026-10-01', value: 0 }] }) expect(option(wrapper, '.chart--rates').graphic).toMatchObject([{ style: { text: '暂无正值数据' } }]) expect(wrapper.find('.chart--rates .chart-metric').text()).toContain('0.00') expect(series(option(wrapper, '.chart--pallets')).data).toHaveLength(2) }) it('shows missing amounts and rates as unknown and omits empty chart widgets', () => { const wrapper = render(emptyCharts(), { ...summary(), physicalPallets: 0, externalAgreedAmounts: [] }) expect(wrapper.findAll('.chart-empty')).toHaveLength(4) expect(wrapper.find('.chart--amounts .chart-metric').text()).toBe('—') expect(wrapper.find('.chart--rates .chart-metric').text()).toBe('—') expect(wrapper.findComponent({ name: 'VChart' }).exists()).toBe(false) })