fix(selfop): preserve warehouse navigation and refine overview presentation
This commit is contained in:
@@ -4,6 +4,7 @@ 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<string, string>, permitted: true }))
|
||||
@@ -15,7 +16,7 @@ const query = defineComponent({ props: ['modelValue'], emits: ['update:modelValu
|
||||
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'], 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 options = { global: { stubs: { AdminPage: passthrough, QueryToolbar: query, AppDialog: dialog, LoadErrorState: error, ApiErrorMeta: true, SkuTrend: true, 'el-button': button } } }
|
||||
const options = { global: { directives: { loading: () => undefined }, stubs: { AdminPage: passthrough, QueryToolbar: query, AppDialog: dialog, LoadErrorState: error, ApiErrorMeta: true, SkuTrend: true, 'el-button': button, 'el-popover': true, 'el-table-column': true, 'el-table': true, 'el-option': true, 'el-select': true } } }
|
||||
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)! }
|
||||
@@ -29,6 +30,46 @@ beforeEach(() => {
|
||||
})
|
||||
afterEach(() => { wrappers.splice(0).forEach(wrapper => wrapper.unmount()); vi.useRealTimers(); vi.restoreAllMocks() })
|
||||
describe('warehouse operating dashboard', () => {
|
||||
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: 'THIS_MONTH', 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)
|
||||
})
|
||||
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: 'LAST_15_DAYS' })
|
||||
expect(wrapper.find('.dashboard-data-notes').attributes('open')).toBeUndefined()
|
||||
await choose(wrapper, { rangePreset: 'TODAY' })
|
||||
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: 'THIS_MONTH', scroll: 760 }) }
|
||||
@@ -48,7 +89,7 @@ describe('warehouse operating dashboard', () => {
|
||||
})
|
||||
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' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' })
|
||||
expect(wrapper.text()).toContain('无可用库存分布资料')
|
||||
expect(wrapper.text()).toContain('库存资料不可用')
|
||||
expect(wrapper.findComponent(DashboardTrend).exists()).toBe(true)
|
||||
@@ -56,7 +97,7 @@ describe('warehouse operating dashboard', () => {
|
||||
})
|
||||
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, averageDailyOutboundQuantity30d: { ...dashboardFixture.kpis.averageDailyOutboundQuantity30d, 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' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
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('销量资料不可用')
|
||||
@@ -65,7 +106,7 @@ describe('warehouse operating dashboard', () => {
|
||||
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' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
await choose(wrapper, { warehouseCode: 'WH-A' })
|
||||
expect(mocks.dashboard).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.dashboard.mock.calls[0]?.[0]).toMatchObject({ warehouseCode: 'WH-A', rangePreset: 'LAST_15_DAYS' })
|
||||
expect(wrapper.findAll('.dashboard-kpis section')).toHaveLength(6)
|
||||
@@ -77,27 +118,46 @@ describe('warehouse operating dashboard', () => {
|
||||
expect(wrapper.text()).not.toContain('lingxing-primary')
|
||||
})
|
||||
it('changes only trend range while pinning versions, then explicitly refreshes the whole snapshot', async () => {
|
||||
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' })
|
||||
await choose(wrapper, { rangePreset: 'TODAY' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
expect(mocks.dashboard.mock.calls.at(-1)?.[0]).toMatchObject({ rangePreset: 'TODAY', 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('clears the old warehouse and ignores its late response', async () => {
|
||||
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 }))
|
||||
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
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)
|
||||
await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
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: 'LAST_15_DAYS' })
|
||||
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: 'LAST_15_DAYS' })
|
||||
})
|
||||
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' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
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-09-13', endDate: '2026-10-12', salesRunId: '11' })
|
||||
expect(wrapper.findComponent(dialog).props('width')).toBe('1120px')
|
||||
@@ -107,7 +167,7 @@ describe('warehouse operating dashboard', () => {
|
||||
expect(wrapper.text()).not.toContain('LATE-SKU')
|
||||
})
|
||||
it('emits exact bucket/day/ranking navigation and keeps unknown day navigation disabled', async () => {
|
||||
const wrapper = page(); await choose(wrapper, { warehouseCode: 'WH-A' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
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)
|
||||
@@ -123,7 +183,7 @@ describe('warehouse operating dashboard', () => {
|
||||
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 findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
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)
|
||||
@@ -134,7 +194,7 @@ describe('warehouse operating dashboard', () => {
|
||||
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' }); await findButton(wrapper, '查询').trigger('click'); await flushPromises()
|
||||
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('展示上次查询')
|
||||
|
||||
Reference in New Issue
Block a user