vi.mock('vue-echarts', () => ({ default: { name: 'VChart', props: ['option'], template: '
' } })) import { defineComponent, h } from 'vue' import { flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import OverviewView from './OverviewView.vue' import DashboardBars from '../components/DashboardBars.vue' import DashboardTrend from '../components/DashboardTrend.vue' import DataTable from '@/shared/components/DataTable.vue' import { dashboardFixture } from './dashboard.fixture' const mocks = vi.hoisted(() => ({ dashboard: vi.fn(), overview: vi.fn(), daily: vi.fn(), push: vi.fn(), query: {} as Record, permitted: true })) vi.mock('../api/analytics', () => ({ analyticsApi: mocks })) vi.mock('vue-router', () => ({ useRoute: () => ({ query: mocks.query }), useRouter: () => ({ push: mocks.push }) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => mocks.permitted }) })) const passthrough = defineComponent({ setup(_, { slots }) { return () => h('div', slots.default?.()) } }) const query = defineComponent({ props: ['modelValue', 'fields'], emits: ['update:modelValue', 'search'], setup(_, { emit }) { return () => h('button', { onClick: () => emit('search') }, '查询') } }) const button = defineComponent({ props: ['disabled'], emits: ['click'], setup(props, { slots, emit }) { return () => h('button', { disabled: props.disabled, onClick: () => emit('click') }, slots.default?.()) } }) const dialog = defineComponent({ props: ['modelValue', 'width', 'title', 'appendToBody'], emits: ['update:modelValue'], setup(props, { slots }) { return () => props.modelValue ? h('section', [h('button', { onClick: () => undefined }, '趋势弹框'), slots.default?.()]) : null } }) const error = defineComponent({ props: ['message'], setup(props) { return () => h('p', props.message) } }) const popover = defineComponent({ props: ['trigger', 'placement', 'title', 'width'], setup(_, { slots }) { return () => h('div', slots.reference?.()) } }) const options = { global: { directives: { loading: () => undefined }, stubs: { AdminPage: passthrough, QueryToolbar: query, AppDialog: dialog, LoadErrorState: error, ApiErrorMeta: true, SkuTrend: true, 'el-button': button, 'el-popover': popover, 'el-table-column': true, 'el-table': true, 'el-option': true, 'el-select': true } } } const wrappers: ReturnType[] = [] function page() { const wrapper = mount(OverviewView, options); wrappers.push(wrapper); return wrapper } function findButton(wrapper: ReturnType, text: string) { return wrapper.findAll('button').find(node => node.text() === text)! } async function choose(wrapper: ReturnType, values: Record) { const toolbar = wrapper.findComponent(query); toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), ...values }); await flushPromises() } beforeEach(() => { vi.clearAllMocks(); mocks.query = {}; mocks.permitted = true vi.spyOn(window, 'scrollTo').mockImplementation(() => undefined) mocks.overview.mockResolvedValue({ warehouses: [], observedAt: '2026-10-13T00:00:00Z' }) mocks.dashboard.mockResolvedValue(dashboardFixture) mocks.daily.mockResolvedValue({ context: dashboardFixture.context, summary: { ...dashboardFixture.salesTrend.summary, customerCode: 'OWNER-A', sku: 'SKU-X' }, points: dashboardFixture.salesTrend.points }) }) afterEach(() => { wrappers.splice(0).forEach(wrapper => wrapper.unmount()); vi.useRealTimers(); vi.restoreAllMocks() }) describe('warehouse operating dashboard', () => { it.each([ ['1896.0666666666667', '1,896.07'], [1896.0666666666667, '1,896.07'], ['1200', '1,200'], ['12.5', '12.5'], ['0', '0'], [null, '—'], ])('formats the daily average %s without changing the returned metric', async (value, expected) => { const response = structuredClone(dashboardFixture) response.kpis.averageDailyOutboundQuantity.value = value mocks.dashboard.mockResolvedValue(response) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) const card = wrapper.findAll('.dashboard-kpis section').find(node => node.find('h3').text() === '所选时段日均出库')! expect(card.find('strong').text()).toBe(expected) expect(card.text()).toContain('件/日') expect(response.kpis.averageDailyOutboundQuantity.value).toBe(value) }) it.each(['PARTIAL', 'UNKNOWN'])('renders omitted nullable KPI fields for %s instead of rejecting the dashboard', async status => { const response = structuredClone(dashboardFixture) for (const metric of Object.values(response.kpis)) { metric.value = null; metric.knownSubtotal = null; metric.unknownSkuCount = null metric.outboundQuantity = null; metric.observedQuantity = null; metric.dataStatus = status } response.kpis.intervalOutboundQuantity.observedQuantity = '8' response.kpis.physicalQuantity.knownSubtotal = '128' // Match a server response serialized with NON_NULL, not an explicit-null fixture. mocks.dashboard.mockResolvedValue(JSON.parse(JSON.stringify(response, (_, value) => value === null ? undefined : value))) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.text()).not.toContain('指标结构不完整') expect(wrapper.findAll('.dashboard-kpis strong').map(node => node.text())).toEqual(['8', '—', '—', '—', '—', '—']) expect(wrapper.text()).toContain('已知小计 128') expect(wrapper.findAllComponents(DashboardBars)).toHaveLength(4) }) it.each(['intervalOutboundQuantity', 'averageDailyOutboundQuantity', 'physicalQuantity', 'availableQuantity', 'physicalSkuCount', 'noOutboundSkuCount'] as const)('keeps the page usable when the response omits %s and recovers after refresh', async key => { const invalid = structuredClone(dashboardFixture) delete (invalid.kpis as Partial)[key] mocks.dashboard.mockResolvedValue(invalid) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.text()).toContain('经营总览返回的指标结构不完整') expect(wrapper.find('.dashboard-kpis').exists()).toBe(false) expect(wrapper.findComponent(query).exists()).toBe(true) mocks.dashboard.mockResolvedValue(dashboardFixture) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6) expect(wrapper.text()).not.toContain('经营总览返回的指标结构不完整') }) it('preserves the last valid result when a refreshed response has a null KPI', async () => { const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) mocks.dashboard.mockResolvedValue({ ...dashboardFixture, kpis: { ...dashboardFixture.kpis, averageDailyOutboundQuantity: null } }) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(wrapper.text()).toContain('经营总览返回的指标结构不完整,请重试或联系管理员;展示上次查询') expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6) }) it('explains today and pending publication using the returned query context', async () => { const data = structuredClone(dashboardFixture) data.context.rebuildRequired = true data.context.salesStatus = 'REBUILD_REQUIRED' mocks.dashboard.mockResolvedValue(data) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.text()).toContain('所选时段包含今日(2026-10-13)') expect(wrapper.text()).toContain('库存评估等待新版销量发布') const refreshed = structuredClone(data) refreshed.context.rangeEndDateExclusive = refreshed.context.anchorDate refreshed.context.rebuildRequired = false refreshed.context.salesStatus = 'VALID' mocks.dashboard.mockResolvedValue(refreshed) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(wrapper.text()).not.toContain('所选时段包含今日(') expect(wrapper.text()).not.toContain('库存评估等待新版销量发布') }) it('offers coverage evidence for both inventory rankings even when they are empty', async () => { const data = structuredClone(dashboardFixture) data.longCoverageSkus = { ...data.longCoverageSkus, items: [], eligibleCount: '0', unassessableCount: '2711', dataStatus: 'PARTIAL' } data.noOutboundSkus = { ...data.noOutboundSkus, items: [], eligibleCount: '0', unassessableCount: '2741', dataStatus: 'PARTIAL' } mocks.dashboard.mockResolvedValue(data) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.findAll('.dashboard-rankings .dashboard-chart-actions button').filter(node => node.text() === '说明')).toHaveLength(3) await wrapper.findAll('.dashboard-rankings .dashboard-chart-actions button').filter(node => node.text() === '说明')[1]!.trigger('click'); await flushPromises() expect(wrapper.text()).toContain('全仓库存分布的待核验原因') expect(wrapper.text()).toContain('待评估 2711 SKU') expect(wrapper.text()).toContain('待评估 2741 SKU') expect(mocks.dashboard).toHaveBeenCalledOnce() }) it('removes today, blocks custom today before querying and drills into the displayed dates', async () => { const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) const toolbar = wrapper.findComponent(query) const options = toolbar.props('fields').find((field: { key: string }) => field.key === 'rangePreset').options expect(options.some((option: { value: string }) => option.value === 'TODAY')).toBe(false) expect(options.find((option: { value: string }) => option.value === 'LAST_15_DAYS').label).toBe('最近15天') const calls = mocks.dashboard.mock.calls.length await choose(wrapper, { rangePreset: 'CUSTOM', dateRange: ['2026-10-01', '2026-10-13'] }) const disabled = toolbar.props('fields').find((field: { key: string }) => field.key === 'dateRange').props.disabledDate expect(disabled(new Date(2026, 9, 13))).toBe(true) expect(disabled(new Date(2026, 9, 12))).toBe(false) await findButton(wrapper, '查询').trigger('click'); await flushPromises() expect(mocks.dashboard).toHaveBeenCalledTimes(calls) expect(wrapper.text()).toContain('结束日期最晚为昨日') await findButton(wrapper, '查看销量').trigger('click') expect(mocks.push.mock.calls.at(-1)?.[0].query).toMatchObject({ rangePreset: 'CUSTOM', startDate: dashboardFixture.context.rangeStartDate, endDate: '2026-10-13' }) }) it('uses one date range field, blocks incomplete ranges and clears obsolete validation errors', async () => { const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A', rangePreset: 'CUSTOM' }) const toolbar = wrapper.findComponent(query) expect(toolbar.props('fields').filter((field: { type: string }) => field.type.includes('date'))).toMatchObject([{ key: 'dateRange', type: 'daterange', label: '日期范围', width: 300 }]) expect(mocks.dashboard).not.toHaveBeenCalled() await findButton(wrapper, '查询').trigger('click'); await flushPromises() expect(mocks.dashboard).not.toHaveBeenCalled() await choose(wrapper, { dateRange: ['2026-10-10'] }) await findButton(wrapper, '查询').trigger('click'); await flushPromises() expect(mocks.dashboard).not.toHaveBeenCalled() expect(wrapper.findComponent(error).exists()).toBe(true) await choose(wrapper, { dateRange: ['2026-10-10', '2026-10-10'] }) expect(wrapper.findComponent(error).exists()).toBe(false) await findButton(wrapper, '查询').trigger('click'); await flushPromises() expect(mocks.dashboard).toHaveBeenCalledOnce() expect(mocks.dashboard.mock.calls[0]?.[0]).toMatchObject({ rangePreset: 'CUSTOM', startDate: '2026-10-10', endDate: '2026-10-10' }) expect(mocks.dashboard.mock.calls[0]?.[0]).not.toHaveProperty('dateRange') await choose(wrapper, { dateRange: null }) await findButton(wrapper, '查询').trigger('click'); await flushPromises() expect(mocks.dashboard).toHaveBeenCalledOnce() await choose(wrapper, { rangePreset: 'LAST_3_DAYS' }) expect(wrapper.findComponent(error).exists()).toBe(false) }) it('restores a custom range and keeps the applied dates when an unsubmitted range is edited', async () => { mocks.query = { warehouseCode: 'WH-A', context: JSON.stringify(dashboardFixture.context), overviewReturn: JSON.stringify({ rangePreset: 'CUSTOM', startDate: '2026-10-10', endDate: '2026-10-10', scroll: 123 }) } const wrapper = page(); await flushPromises() expect(wrapper.findComponent(query).props('modelValue').dateRange).toEqual(['2026-10-10', '2026-10-10']) expect(mocks.dashboard.mock.calls[0]?.[0]).toMatchObject({ startDate: '2026-10-10', endDate: '2026-10-10', salesRunId: '11' }) await choose(wrapper, { dateRange: ['2026-10-11', '2026-10-12'] }) await findButton(wrapper, '查看Top100').trigger('click') expect(JSON.parse(mocks.push.mock.calls.at(-1)?.[0].query.overviewReturn)).toMatchObject({ rangePreset: 'CUSTOM', startDate: '2026-10-10', endDate: '2026-10-10' }) wrapper.findComponent(query).vm.$emit('reset'); await flushPromises() expect(wrapper.findComponent(query).props('modelValue').dateRange).toEqual([]) expect(mocks.dashboard.mock.calls.at(-1)?.[0]).toHaveProperty('startDate') expect(mocks.dashboard.mock.calls.at(-1)?.[0]).toHaveProperty('endDate') }) it('does not replace an invalid warehouse-less return with the default warehouse', async () => { mocks.query = { context: JSON.stringify(dashboardFixture.context), overviewReturn: JSON.stringify({ rangePreset: 'LAST_7_DAYS', scroll: 760 }) } mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'WH-A', warehouseName: 'A仓' }], observedAt: '2026-10-13T00:00:00Z' }) const wrapper = page(); await flushPromises() expect(wrapper.text()).toContain('经营总览返回参数无效') expect(wrapper.findComponent(query).props('modelValue').warehouseCode).toBe('') expect(mocks.dashboard).not.toHaveBeenCalled() await choose(wrapper, { warehouseCode: 'WH-A' }) expect(mocks.dashboard).toHaveBeenCalledOnce() expect(mocks.dashboard.mock.calls[0]?.[0]).not.toHaveProperty('queryAsOf') }) it('uses the common title/actions/filter surface without an empty result table', async () => { const wrapper = page(); await flushPromises() expect(wrapper.findComponent(DataTable).props()).toMatchObject({ appearance: 'integrated', resultsVisible: false, columnSettingsMode: 'hide' }) expect(wrapper.find('.data-table__toolbar-start h2').text()).toBe('经营总览') expect(wrapper.find('.data-table__toolbar-actions').text()).toContain('整体刷新') expect(wrapper.find('.data-table__filters').findComponent(query).exists()).toBe(true) expect(wrapper.findComponent(query).attributes('preference-key')).toBe('self-operated-overview-query') expect(wrapper.find('el-table-stub').exists()).toBe(false) expect(wrapper.findComponent(popover).props()).toMatchObject({ trigger: 'click', placement: 'bottom-end', title: '统计口径与采集时间', width: 460 }) expect(wrapper.find('.data-table__toolbar-actions button[aria-label="统计口径与采集时间"]').exists()).toBe(true) expect(wrapper.find('details.dashboard-data-notes').exists()).toBe(false) }) it('automatically loads the first warehouse and keeps it selected on reset', async () => { mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'WH-A', warehouseName: '仓A' }, { warehouseCode: 'WH-A' }, { warehouseCode: 'WH-B' }] }) const wrapper = page(); await flushPromises() expect(wrapper.findComponent(query).props('modelValue').warehouseCode).toBe('WH-A') expect(mocks.dashboard).toHaveBeenCalledTimes(1) expect(mocks.dashboard.mock.calls[0]?.[0]).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'CUSTOM' }) expect(wrapper.find('.dashboard-data-notes').exists()).toBe(false) await choose(wrapper, { rangePreset: 'LAST_3_DAYS' }) wrapper.findComponent(query).vm.$emit('reset'); await flushPromises() expect(wrapper.findComponent(query).props('modelValue')).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'LAST_15_DAYS' }) expect(mocks.dashboard.mock.calls.at(-1)?.[0]).not.toHaveProperty('queryAsOf') }) it('preserves a warehouse supplied by a direct link instead of replacing it with the default', async () => { mocks.query = { warehouseCode: 'WH-B' } mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'WH-A' }, { warehouseCode: 'WH-B' }] }) const wrapper = page(); await flushPromises() expect(mocks.dashboard).toHaveBeenCalledTimes(1) expect(mocks.dashboard.mock.calls[0]?.[0].warehouseCode).toBe('WH-B') expect(wrapper.findComponent(query).props('modelValue').warehouseCode).toBe('WH-B') }) it('restores the saved warehouse, range, fixed context and scroll when returning from a drilldown', async () => { const scroll = vi.spyOn(window, 'scrollTo').mockImplementation(() => undefined) mocks.query = { warehouseCode: 'WH-A', context: JSON.stringify(dashboardFixture.context), overviewReturn: JSON.stringify({ rangePreset: 'LAST_7_DAYS', scroll: 760 }) } const wrapper = page(); await flushPromises() expect(mocks.dashboard.mock.calls[0]?.[0]).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'CUSTOM', startDate: '2026-10-06', endDate: '2026-10-12', salesRunId: '11', inventoryRunId: '12', queryAsOf: dashboardFixture.context.queryAsOf }) expect(wrapper.findComponent(query).props('modelValue')).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'LAST_7_DAYS' }) expect(scroll).toHaveBeenCalledWith({ top: 760 }) }) it('does not silently refresh an expired return context', async () => { mocks.query = { warehouseCode: 'WH-A', context: JSON.stringify({ ...dashboardFixture.context, queryExpiresAt: '2026-10-13T00:01:00Z' }), overviewReturn: JSON.stringify({ rangePreset: 'LAST_3_DAYS', scroll: 0 }) } vi.useFakeTimers({ toFake: ['Date', 'setInterval', 'clearInterval'] }); vi.setSystemTime(new Date('2026-10-13T00:02:00Z')) const wrapper = page(); await flushPromises() expect(mocks.dashboard).not.toHaveBeenCalled() expect(wrapper.text()).toContain('原图查询已过期') await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(mocks.dashboard.mock.calls[0]?.[0]).not.toHaveProperty('queryAsOf') }) it('keeps sales available when inventory is unavailable, with no fabricated distribution', async () => { mocks.dashboard.mockResolvedValue({ ...dashboardFixture, context: { ...dashboardFixture.context, inventoryDrilldownExpiresAt: null }, coverageDistribution: { ...dashboardFixture.coverageDistribution, candidateSkuCount: null, buckets: [], dataStatus: 'UNKNOWN', reasonCodes: ['INVENTORY_UNAVAILABLE'] } }) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.text()).toContain('无可用库存分布资料') await wrapper.findAll('.dashboard-main-charts button').filter(node => node.text() === '说明')[1]!.trigger('click'); await flushPromises() expect(wrapper.text()).toContain('库存资料不可用') expect(wrapper.findComponent(DashboardTrend).exists()).toBe(true) expect(findButton(wrapper, '查看Top100').attributes('disabled')).toBeUndefined() }) it('keeps inventory present without published sales and never derives a daily average from partial sales', async () => { mocks.dashboard.mockResolvedValue({ ...dashboardFixture, context: { ...dashboardFixture.context, salesRunId: null, salesScopeHash: null }, kpis: { ...dashboardFixture.kpis, averageDailyOutboundQuantity: { ...dashboardFixture.kpis.averageDailyOutboundQuantity, value: null, knownSubtotal: null, dataStatus: 'UNKNOWN', reasonCodes: ['SALES_UNAVAILABLE'] }, physicalQuantity: { ...dashboardFixture.kpis.physicalQuantity, value: '128', dataStatus: 'COMPLETE' } }, salesTrend: { ...dashboardFixture.salesTrend, points: [] } }) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.findAll('.dashboard-kpis strong')[1]!.text()).toBe('—') expect(wrapper.findAll('.dashboard-kpis strong')[2]!.text()).toBe('128') expect(wrapper.text()).toContain('销量资料不可用') expect(wrapper.findAllComponents(DashboardBars)[0]!.props('rows')).toHaveLength(7) }) it('does not request business data without a selected warehouse and renders six metrics/five charts from one snapshot', async () => { const wrapper = page(); await flushPromises() expect(mocks.dashboard).not.toHaveBeenCalled() await choose(wrapper, { warehouseCode: 'WH-A' }) expect(mocks.dashboard).toHaveBeenCalledTimes(1) expect(mocks.dashboard.mock.calls[0]?.[0]).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'CUSTOM' }) expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6) expect(wrapper.findAllComponents(DashboardBars)).toHaveLength(4) expect(wrapper.findAllComponents(DashboardTrend)).toHaveLength(1) expect(wrapper.text()).toContain('所选时段无出库库存Top10') expect(wrapper.text()).not.toContain('近30日无出库') expect(wrapper.findAllComponents(DashboardBars).slice(1).every(chart => !chart.text().includes('2026-10-10 至 2026-10-14'))).toBe(true) expect(wrapper.text()).toContain('区间已采集出库量') expect(wrapper.text()).toContain('已知小计 128') expect(wrapper.text()).toContain('另 1 SKU 待核验') expect(wrapper.find('.dashboard-kpis').text()).not.toContain('另 0 SKU 待核验') expect(wrapper.findAll('.dashboard-kpis small').every(node => node.text() !== '—')).toBe(true) expect(wrapper.text()).not.toContain('lingxing-primary') }) it('changes the shared date range while pinning versions, then explicitly refreshes the whole snapshot', async () => { const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) await choose(wrapper, { rangePreset: 'LAST_3_DAYS' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises() expect(mocks.dashboard.mock.calls.at(-1)?.[0]).toMatchObject({ rangePreset: 'CUSTOM', startDate: '2026-10-10', endDate: '2026-10-12', salesRunId: '11', inventoryRunId: '12', queryAsOf: dashboardFixture.context.queryAsOf }) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(mocks.dashboard.mock.calls.at(-1)?.[0]).not.toHaveProperty('salesRunId') }) it('automatically loads the new warehouse, shows loading and ignores the old late response', async () => { let finish!: (value: unknown) => void let finishNew!: (value: unknown) => void mocks.dashboard.mockImplementationOnce(() => new Promise(resolve => { finish = resolve })) mocks.dashboard.mockImplementationOnce(() => new Promise(resolve => { finishNew = resolve })) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) await choose(wrapper, { warehouseCode: 'WH-B' }) expect(wrapper.find('.dashboard-kpis').exists()).toBe(false) expect(wrapper.find('.dashboard-placeholder').exists()).toBe(true) expect(mocks.dashboard).toHaveBeenCalledTimes(2) expect(mocks.dashboard.mock.calls[1]?.[0]).toMatchObject({ warehouseCode: 'WH-B', rangePreset: 'CUSTOM' }) expect(mocks.dashboard.mock.calls[1]?.[0]).not.toHaveProperty('queryAsOf') finishNew(dashboardFixture); await flushPromises() expect(wrapper.find('.dashboard-kpis').exists()).toBe(true) finish({ ...dashboardFixture, kpis: { ...dashboardFixture.kpis, physicalQuantity: { ...dashboardFixture.kpis.physicalQuantity, value: '999999' } } }); await flushPromises() expect(wrapper.text()).not.toContain('999999') expect(mocks.dashboard.mock.calls[0]?.[1].aborted).toBe(true) }) it('retains the switched warehouse when the query toolbar clears its fields on reset', async () => { mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'WH-A' }, { warehouseCode: 'WH-B' }] }) const wrapper = page(); await flushPromises() await choose(wrapper, { warehouseCode: 'WH-B' }) const toolbar = wrapper.findComponent(query) toolbar.props('modelValue').warehouseCode = '' toolbar.props('modelValue').rangePreset = '' toolbar.vm.$emit('reset'); await flushPromises() expect(toolbar.props('modelValue')).toMatchObject({ warehouseCode: 'WH-B', rangePreset: 'LAST_15_DAYS' }) expect(mocks.dashboard).toHaveBeenCalledTimes(3) expect(mocks.dashboard.mock.calls.at(-1)?.[0]).toMatchObject({ warehouseCode: 'WH-B', rangePreset: 'CUSTOM' }) }) it('preserves same-SKU owner identity in the 1120 dialog and cancels closed requests', async () => { let finish!: (value: unknown) => void mocks.daily.mockImplementationOnce(() => new Promise(resolve => { finish = resolve })) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) wrapper.findAllComponents(DashboardBars)[1]!.vm.$emit('select', '1'); await flushPromises() expect(mocks.daily.mock.calls[0]?.[0]).toMatchObject({ customerCode: 'OWNER-B', sku: 'SKU-X', rangePreset: 'CUSTOM', startDate: '2026-10-10', endDate: '2026-10-13', salesRunId: '11' }) expect(wrapper.findAllComponents(dialog).find(item => item.props('title') === 'SKU每日出库趋势')!.props('width')).toBe('1120px') for (const modal of wrapper.findAllComponents(dialog)) expect(modal.props('appendToBody')).not.toBeUndefined() wrapper.findAllComponents(dialog).find(item => item.props('title') === 'SKU每日出库趋势')!.vm.$emit('update:modelValue', false); await flushPromises() finish({ context: dashboardFixture.context, summary: { ...dashboardFixture.salesTrend.summary, sku: 'LATE-SKU' }, points: [] }); await flushPromises() expect(mocks.daily.mock.calls[0]?.[1].aborted).toBe(true) expect(wrapper.text()).not.toContain('LATE-SKU') }) it('keeps an uncovered selected period empty without changing its dates or claiming zero sales', async () => { mocks.dashboard.mockResolvedValue({ ...dashboardFixture, hotSkus: { ...dashboardFixture.hotSkus, windowStartDate: '2026-10-13', windowEndDateExclusive: '2026-10-14', calculationWindowDays: 1, eligibleCount: '0', items: [], dataStatus: 'UNKNOWN', reasonCodes: ['SALES_WINDOW_INCOMPLETE'] } }) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.findAllComponents(DashboardBars)[1]!.props('rows')).toEqual([]) expect(wrapper.find('.dashboard-rankings').text()).not.toContain('未采集日期不计为零出库') await wrapper.findAll('.dashboard-rankings button').find(node => node.text() === '说明')!.trigger('click'); await flushPromises() expect(wrapper.text()).toContain('未采集日期不计为零出库') await findButton(wrapper, '查看Top100').trigger('click') expect(mocks.push.mock.calls.at(-1)?.[0].query).toMatchObject({ startDate: '2026-10-13', endDate: '2026-10-13', rangePreset: 'CUSTOM' }) }) it('shows partial hot sales for the selected dates and preserves them in Top100 and SKU trends', async () => { mocks.dashboard.mockResolvedValue({ ...dashboardFixture, hotSkus: { ...dashboardFixture.hotSkus, windowStartDate: '2026-10-01', windowEndDateExclusive: '2026-10-14', calculationWindowDays: 13, dataStatus: 'PARTIAL', reasonCodes: ['SALES_WINDOW_INCOMPLETE'], items: [{ ...dashboardFixture.hotSkus.items[0], windowOutboundQuantity: '37', value: '37', averageDailyOutboundQuantity: null, inventoryCoverageDays: null }] } }) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) expect(wrapper.text()).toContain('所选时段热销Top10') expect(wrapper.text()).toContain('数据未采齐,排名仅供参考') const hot = wrapper.findAllComponents(DashboardBars)[1]! expect(hot.props('rows')[0]).toMatchObject({ value: '37' }) expect(hot.props('rows')[0].detail).toContain('13天窗口已采集出库 37 件') expect(hot.props('rows')[0].detail).not.toContain('近30日出库') await findButton(wrapper, '查看Top100').trigger('click') expect(mocks.push.mock.calls.at(-1)?.[0].query).toMatchObject({ analysis: 'HOT', rangePreset: 'CUSTOM', startDate: '2026-10-01', endDate: '2026-10-13' }) hot.vm.$emit('select', '0'); await flushPromises() expect(mocks.daily.mock.calls.at(-1)?.[0]).toMatchObject({ rangePreset: 'CUSTOM', startDate: '2026-10-01', endDate: '2026-10-13', customerCode: 'OWNER-A', sku: 'SKU-X' }) }) it('emits exact bucket/day/ranking navigation and keeps unknown day navigation disabled', async () => { const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) wrapper.findAllComponents(DashboardBars)[0]!.vm.$emit('select', 'DAYS_GT_90'); await flushPromises() expect(mocks.push.mock.calls.at(-1)?.[0]).toMatchObject({ path: '/self-operated/inventory', query: { coverageBucket: 'DAYS_GT_90', bucketRuleVersion: 'COVERAGE_DISTRIBUTION_V1', from: 'overview' } }) const chart = wrapper.findComponent(DashboardTrend) await chart.find('.chart-disclosure button').trigger('click') await chart.findAll('tbody button')[1]!.trigger('click') expect(mocks.push).toHaveBeenCalledTimes(1) expect(chart.text()).toContain('销量未覆盖') await chart.findAll('tbody button')[0]!.trigger('click') expect(mocks.push.mock.calls.at(-1)?.[0].query).toMatchObject({ analysis: 'SKU', rangePreset: 'CUSTOM', startDate: '2026-10-10', endDate: '2026-10-10' }) await findButton(wrapper, '查看Top100').trigger('click') expect(mocks.push.mock.calls.at(-1)?.[0].query).toMatchObject({ analysis: 'HOT', rangePreset: 'CUSTOM', startDate: '2026-10-10', endDate: '2026-10-13' }) expect(JSON.parse(mocks.push.mock.calls.at(-1)?.[0].query.context)).toEqual(dashboardFixture.context) }) it('expires inventory before sales while retaining stock-independent SKU trends', async () => { vi.useFakeTimers({ toFake: ['Date', 'setInterval', 'clearInterval'] }); vi.setSystemTime(new Date('2026-10-13T00:00:00Z')) mocks.dashboard.mockResolvedValue({ ...dashboardFixture, context: { ...dashboardFixture.context, inventoryDrilldownExpiresAt: '2026-10-13T00:00:01Z' } }) const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) await vi.advanceTimersByTimeAsync(2000) expect(wrapper.text()).toContain('原图库存已过期或不可用') expect(wrapper.findAllComponents(DashboardBars)[0]!.props('disabled')).toBe(true) expect(findButton(wrapper, '查看Top100').attributes('disabled')).toBeUndefined() wrapper.findAllComponents(DashboardBars)[2]!.vm.$emit('select', '0'); await flushPromises() expect(mocks.daily).toHaveBeenCalledTimes(1) wrapper.findAllComponents(DashboardBars)[0]!.vm.$emit('select', 'DAYS_GT_90'); await flushPromises() expect(mocks.push).not.toHaveBeenCalled() }) it('retains prior figures on failed refresh and does not render unknown quantities as zero', async () => { const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }) mocks.dashboard.mockRejectedValue(new Error('offline')) await findButton(wrapper, '整体刷新').trigger('click'); await flushPromises() expect(wrapper.text()).toContain('展示上次查询') expect(wrapper.find('.dashboard-kpis').exists()).toBe(true) expect(wrapper.findAll('.dashboard-kpis strong')[2]!.text()).toBe('—') }) })