2026-10-13 20:44:31 +08:00
vi . mock ( 'vue-echarts' , () => ({ default : { name : 'VChart' , props : [ 'option' ], template : '<div />' } }))
2026-10-13 00:45:41 +08:00
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'
2026-10-13 11:06:20 +08:00
import DataTable from '@/shared/components/DataTable.vue'
2026-10-13 00:45:41 +08:00
import { dashboardFixture } from './dashboard.fixture'
const mocks = vi . hoisted (() => ({ dashboard : vi.fn (), overview : vi.fn (), daily : vi.fn (), push : vi.fn (), query : {} as Record < string , string >, 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 ? .()) } })
2026-10-13 13:43:16 +08:00
const query = defineComponent ({ props : [ 'modelValue' , 'fields' ], emits : [ 'update:modelValue' , 'search' ], setup ( _ , { emit }) { return () => h ( 'button' , { onClick : () => emit ( 'search' ) }, '查询' ) } })
2026-10-13 00:45:41 +08:00
const button = defineComponent ({ props : [ 'disabled' ], emits : [ 'click' ], setup ( props , { slots , emit }) { return () => h ( 'button' , { disabled : props.disabled , onClick : () => emit ( 'click' ) }, slots . default ? .()) } })
2026-10-13 20:44:31 +08:00
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 } })
2026-10-13 00:45:41 +08:00
const error = defineComponent ({ props : [ 'message' ], setup ( props ) { return () => h ( 'p' , props . message ) } })
2026-10-13 16:52:09 +08:00
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 } } }
2026-10-13 00:45:41 +08:00
const wrappers : ReturnType < typeof mount >[] = []
function page() { const wrapper = mount ( OverviewView , options ); wrappers . push ( wrapper ); return wrapper }
function findButton ( wrapper : ReturnType < typeof mount >, text : string ) { return wrapper . findAll ( 'button' ). find ( node => node . text () === text ) ! }
2026-10-13 13:43:16 +08:00
async function choose ( wrapper : ReturnType < typeof mount >, values : Record < string , string | string [] | null >) { const toolbar = wrapper . findComponent ( query ); toolbar . vm . $emit ( 'update:modelValue' , { ... toolbar . props ( 'modelValue' ), ... values }); await flushPromises () }
2026-10-13 00:45:41 +08:00
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' , () => {
2026-10-14 11:22:59 +08:00
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 )
})
2026-10-14 11:02:56 +08:00
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 < typeof invalid.kpis >)[ key ]
mocks . dashboard . mockResolvedValue ( invalid )
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-14 11:22:59 +08:00
expect ( wrapper . text ()). toContain ( '经营总览返回的指标结构不完整' )
2026-10-14 11:02:56 +08:00
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 )
2026-10-14 11:22:59 +08:00
expect ( wrapper . text ()). not . toContain ( '经营总览返回的指标结构不完整' )
2026-10-14 11:02:56 +08:00
})
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 ()
2026-10-14 11:22:59 +08:00
expect ( wrapper . text ()). toContain ( '经营总览返回的指标结构不完整,请重试或联系管理员;展示上次查询' )
2026-10-14 11:02:56 +08:00
expect ( wrapper . findAll ( '.dashboard-kpis section' )). toHaveLength ( 6 )
})
2026-10-14 12:39:16 +08:00
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 ( '库存评估等待新版销量发布' )
})
2026-10-13 20:44:31 +08:00
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-chart-actions button' ). filter ( node => node . text () === '待评估原因' )). toHaveLength ( 2 )
await findButton ( wrapper , '待评估原因' ). trigger ( 'click' ); await flushPromises ()
expect ( wrapper . text ()). toContain ( '全仓库存分布的待核验原因' )
expect ( wrapper . text ()). toContain ( '待评估 2711 SKU' )
expect ( wrapper . text ()). toContain ( '待评估 2741 SKU' )
expect ( mocks . dashboard ). toHaveBeenCalledOnce ()
})
2026-10-14 12:39:16 +08:00
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' })
})
2026-10-13 13:43:16 +08:00
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 )
2026-10-14 12:39:16 +08:00
expect ( toolbar . props ( 'fields' ). filter (( field : { type : string }) => field . type . includes ( 'date' ))). toMatchObject ([{ key : 'dateRange' , type : 'daterange' , label : '日期范围' , width : 300 }])
2026-10-13 13:43:16 +08:00
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 ()
2026-10-14 12:39:16 +08:00
await choose ( wrapper , { rangePreset : 'LAST_3_DAYS' })
2026-10-13 13:43:16 +08:00
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 ([])
2026-10-14 12:39:16 +08:00
expect ( mocks . dashboard . mock . calls . at ( - 1 ) ? .[ 0 ]). toHaveProperty ( 'startDate' )
expect ( mocks . dashboard . mock . calls . at ( - 1 ) ? .[ 0 ]). toHaveProperty ( 'endDate' )
2026-10-13 13:43:16 +08:00
})
2026-10-13 11:06:20 +08:00
it ( 'does not replace an invalid warehouse-less return with the default warehouse' , async () => {
2026-10-14 12:39:16 +08:00
mocks . query = { context : JSON.stringify ( dashboardFixture . context ), overviewReturn : JSON.stringify ({ rangePreset : 'LAST_7_DAYS' , scroll : 760 }) }
2026-10-13 11:06:20 +08:00
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 )
2026-10-13 16:52:09 +08:00
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 )
2026-10-13 11:06:20 +08:00
})
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 )
2026-10-14 12:39:16 +08:00
expect ( mocks . dashboard . mock . calls [ 0 ] ? .[ 0 ]). toMatchObject ({ warehouseCode : 'WH-A' , rangePreset : 'CUSTOM' })
2026-10-13 16:52:09 +08:00
expect ( wrapper . find ( '.dashboard-data-notes' ). exists ()). toBe ( false )
2026-10-14 12:39:16 +08:00
await choose ( wrapper , { rangePreset : 'LAST_3_DAYS' })
2026-10-13 11:06:20 +08:00
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' )
})
2026-10-13 00:45:41 +08:00
it ( 'restores the saved warehouse, range, fixed context and scroll when returning from a drilldown' , async () => {
const scroll = vi . spyOn ( window , 'scrollTo' ). mockImplementation (() => undefined )
2026-10-14 12:39:16 +08:00
mocks . query = { warehouseCode : 'WH-A' , context : JSON.stringify ( dashboardFixture . context ), overviewReturn : JSON.stringify ({ rangePreset : 'LAST_7_DAYS' , scroll : 760 }) }
2026-10-13 00:45:41 +08:00
const wrapper = page (); await flushPromises ()
2026-10-14 12:39:16 +08:00
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' })
2026-10-13 00:45:41 +08:00
expect ( scroll ). toHaveBeenCalledWith ({ top : 760 })
})
it ( 'does not silently refresh an expired return context' , async () => {
2026-10-14 12:39:16 +08:00
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 }) }
2026-10-13 00:45:41 +08:00
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' ] } })
2026-10-13 11:06:20 +08:00
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-13 00:45:41 +08:00
expect ( wrapper . text ()). toContain ( '无可用库存分布资料' )
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 () => {
2026-10-14 11:02:56 +08:00
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 : [] } })
2026-10-13 11:06:20 +08:00
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-13 00:45:41 +08:00
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 ()
2026-10-13 11:06:20 +08:00
await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-13 00:45:41 +08:00
expect ( mocks . dashboard ). toHaveBeenCalledTimes ( 1 )
2026-10-14 12:39:16 +08:00
expect ( mocks . dashboard . mock . calls [ 0 ] ? .[ 0 ]). toMatchObject ({ warehouseCode : 'WH-A' , rangePreset : 'CUSTOM' })
2026-10-13 00:45:41 +08:00
expect ( wrapper . findAll ( '.dashboard-kpis section' )). toHaveLength ( 6 )
expect ( wrapper . findAllComponents ( DashboardBars )). toHaveLength ( 4 )
expect ( wrapper . findAllComponents ( DashboardTrend )). toHaveLength ( 1 )
2026-10-14 11:02:56 +08:00
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 )
2026-10-13 00:45:41 +08:00
expect ( wrapper . text ()). toContain ( '区间已采集出库量' )
expect ( wrapper . text ()). toContain ( '已知小计 128' )
expect ( wrapper . text ()). toContain ( '另 1 SKU 待核验' )
expect ( wrapper . text ()). not . toContain ( 'lingxing-primary' )
})
2026-10-14 11:02:56 +08:00
it ( 'changes the shared date range while pinning versions, then explicitly refreshes the whole snapshot' , async () => {
2026-10-13 11:06:20 +08:00
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-14 12:39:16 +08:00
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 })
2026-10-13 00:45:41 +08:00
await findButton ( wrapper , '整体刷新' ). trigger ( 'click' ); await flushPromises ()
expect ( mocks . dashboard . mock . calls . at ( - 1 ) ? .[ 0 ]). not . toHaveProperty ( 'salesRunId' )
})
2026-10-13 11:06:20 +08:00
it ( 'automatically loads the new warehouse, shows loading and ignores the old late response' , async () => {
2026-10-13 00:45:41 +08:00
let finish !: ( value : unknown ) => void
2026-10-13 11:06:20 +08:00
let finishNew !: ( value : unknown ) => void
2026-10-13 00:45:41 +08:00
mocks . dashboard . mockImplementationOnce (() => new Promise ( resolve => { finish = resolve }))
2026-10-13 11:06:20 +08:00
mocks . dashboard . mockImplementationOnce (() => new Promise ( resolve => { finishNew = resolve }))
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-13 00:45:41 +08:00
await choose ( wrapper , { warehouseCode : 'WH-B' })
expect ( wrapper . find ( '.dashboard-kpis' ). exists ()). toBe ( false )
2026-10-13 11:06:20 +08:00
expect ( wrapper . find ( '.dashboard-placeholder' ). exists ()). toBe ( true )
expect ( mocks . dashboard ). toHaveBeenCalledTimes ( 2 )
2026-10-14 12:39:16 +08:00
expect ( mocks . dashboard . mock . calls [ 1 ] ? .[ 0 ]). toMatchObject ({ warehouseCode : 'WH-B' , rangePreset : 'CUSTOM' })
2026-10-13 11:06:20 +08:00
expect ( mocks . dashboard . mock . calls [ 1 ] ? .[ 0 ]). not . toHaveProperty ( 'queryAsOf' )
finishNew ( dashboardFixture ); await flushPromises ()
expect ( wrapper . find ( '.dashboard-kpis' ). exists ()). toBe ( true )
2026-10-13 00:45:41 +08:00
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 )
})
2026-10-13 11:06:20 +08:00
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 )
2026-10-14 12:39:16 +08:00
expect ( mocks . dashboard . mock . calls . at ( - 1 ) ? .[ 0 ]). toMatchObject ({ warehouseCode : 'WH-B' , rangePreset : 'CUSTOM' })
2026-10-13 11:06:20 +08:00
})
2026-10-13 00:45:41 +08:00
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 }))
2026-10-13 11:06:20 +08:00
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-13 00:45:41 +08:00
wrapper . findAllComponents ( DashboardBars )[ 1 ] ! . vm . $emit ( 'select' , '1' ); await flushPromises ()
2026-10-14 11:02:56 +08:00
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' })
2026-10-13 20:44:31 +08:00
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 ()
2026-10-13 00:45:41 +08:00
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' )
})
2026-10-13 18:47:56 +08:00
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 . 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' })
})
2026-10-13 00:45:41 +08:00
it ( 'emits exact bucket/day/ranking navigation and keeps unknown day navigation disabled' , async () => {
2026-10-13 11:06:20 +08:00
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-13 00:45:41 +08:00
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 )
2026-10-13 20:44:31 +08:00
await chart . find ( '.chart-disclosure button' ). trigger ( 'click' )
await chart . findAll ( 'tbody button' )[ 1 ] ! . trigger ( 'click' )
2026-10-13 00:45:41 +08:00
expect ( mocks . push ). toHaveBeenCalledTimes ( 1 )
expect ( chart . text ()). toContain ( '销量未覆盖' )
2026-10-13 20:44:31 +08:00
await chart . findAll ( 'tbody button' )[ 0 ] ! . trigger ( 'click' )
2026-10-13 00:45:41 +08:00
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' )
2026-10-14 11:02:56 +08:00
expect ( mocks . push . mock . calls . at ( - 1 ) ? .[ 0 ]. query ). toMatchObject ({ analysis : 'HOT' , rangePreset : 'CUSTOM' , startDate : '2026-10-10' , endDate : '2026-10-13' })
2026-10-13 00:45:41 +08:00
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' } })
2026-10-13 11:06:20 +08:00
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-13 00:45:41 +08:00
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 () => {
2026-10-13 11:06:20 +08:00
const wrapper = page (); await choose ( wrapper , { warehouseCode : 'WH-A' })
2026-10-13 00:45:41 +08:00
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 ( '—' )
})
})