优化
This commit is contained in:
@@ -8,7 +8,7 @@ vi.mock('vue-router', () => ({ useRoute: () => mocks.route, useRouter: () => ({
|
||||
vi.mock('../api/analytics', () => ({ analyticsApi: mocks }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => mocks.allowed }) }))
|
||||
const toolbar = defineComponent({ name: 'QueryToolbar', props: ['modelValue', 'fields'], emits: ['update:modelValue', 'search', 'reset'], setup: () => () => h('div') })
|
||||
const table = defineComponent({ name: 'DataTable', props: ['data', 'columns', 'total', 'page'], emits: ['update:page', 'update:pageSize'], setup(p, { slots }) { return () => h('div', [slots.filters?.(), slots['toolbar-start']?.(), slots['toolbar-actions']?.(), slots['before-results']?.(), ...(p.data ?? []).flatMap((row: TurnoverRow) => (p.columns ?? []).map((column: { slotName?: string; prop: keyof TurnoverRow }) => column.slotName && slots[column.slotName] ? slots[column.slotName]?.({ row }) : String(row[column.prop] ?? '')))]) } })
|
||||
const table = defineComponent({ name: 'DataTable', props: ['data', 'columns', 'total', 'page'], emits: ['update:page', 'update:pageSize'], setup(p, { slots }) { return () => h('div', [slots.filters?.(), slots['toolbar-start']?.(), slots['toolbar-actions']?.(), slots['before-results']?.(), ...(p.columns ?? []).map((column: { headerSlotName?: string; prop: string; label: string }) => column.headerSlotName ? slots[column.headerSlotName]?.({ column: { property: column.prop, label: column.label } }) : null), ...(p.data ?? []).flatMap((row: TurnoverRow) => (p.columns ?? []).map((column: { slotName?: string; prop: keyof TurnoverRow }) => column.slotName && slots[column.slotName] ? slots[column.slotName]?.({ row }) : String(row[column.prop] ?? '')))]) } })
|
||||
const button = defineComponent({ emits: ['click'], setup(_, { slots, emit }) { return () => h('button', { onClick: () => emit('click') }, slots.default?.()) } })
|
||||
const shell = defineComponent({ setup(_, { slots }) { return () => h('div', slots.default?.()) } })
|
||||
const total = { value: 100, knownSubtotal: 100, unknownSkuCount: 0, reasonCodes: [] }
|
||||
@@ -26,6 +26,94 @@ async function change(w: ReturnType<typeof mount>, patch: object, search = false
|
||||
beforeEach(() => { vi.clearAllMocks(); mocks.allowed = true; mocks.route = reactive({ query: { endDate: '2026-10-12' } }); mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'A' }, { warehouseCode: 'B' }] }); mocks.turnover.mockImplementation(async (q: TurnoverQuery) => response(q)); sessionStorage.clear() })
|
||||
afterEach(() => wrappers.splice(0).forEach(w => w.unmount()))
|
||||
describe('inventory turnover page fixed snapshot behavior', () => {
|
||||
it('sorts every numeric header on the server in both directions with the same snapshot', async () => {
|
||||
const w = page(); await flushPromises()
|
||||
await w.findAll('button').find(b => b.text() === '扩展列')!.trigger('click')
|
||||
const metrics = { physicalQuantity: 'PHYSICAL', availableQuantity: 'AVAILABLE', outbound7: 'OUTBOUND_7', outbound15: 'OUTBOUND_15', outbound30: 'OUTBOUND_30', averageDailyOutboundQuantity: 'AVERAGE_DAILY', inventoryCoverageDays: 'COVERAGE', frozenQuantity: 'FROZEN', defectiveQuantity: 'DEFECTIVE', averagePhysicalQuantity: 'AVERAGE_PHYSICAL', quantityTurnoverCount: 'TURNOVER' }
|
||||
for (const [prop, metric] of Object.entries(metrics)) {
|
||||
// Start on a later page and with another sort to prove that a header resets pagination.
|
||||
await change(w, { sortBy: metric === 'PHYSICAL' ? 'AVAILABLE_DESC' : 'PHYSICAL_DESC' }, true)
|
||||
w.findComponent(table).vm.$emit('update:page', 2); await flushPromises()
|
||||
const header = w.get(`button[data-column="${prop}"]`)
|
||||
for (const direction of ['DESC', 'ASC']) {
|
||||
await header.trigger('click'); await flushPromises()
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ sortBy: `${metric}_${direction}`, current: 1, snapshotContext: 'token-A', includeOverview: false })
|
||||
expect(w.findComponent(toolbar).props('modelValue').sortBy).toBe(`${metric}_${direction}`)
|
||||
expect(header.attributes('aria-label')).toContain(direction === 'ASC' ? '当前升序' : '当前降序')
|
||||
}
|
||||
}
|
||||
await change(w, { baseWindowDays: 7 })
|
||||
expect(w.get('button[data-column="averageDailyOutboundQuantity"]').text()).toContain('7日日均')
|
||||
await w.get('button[data-column="averageDailyOutboundQuantity"]').trigger('click'); await flushPromises()
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ baseWindowDays: 7, sortBy: 'AVERAGE_DAILY_DESC' })
|
||||
})
|
||||
it('keeps the successful sort marker and page when a header query fails', async () => {
|
||||
const w = page(); await flushPromises()
|
||||
w.findComponent(table).vm.$emit('update:page', 2); await flushPromises()
|
||||
mocks.turnover.mockRejectedValueOnce(new Error('offline'))
|
||||
await w.get('button[data-column="outbound15"]').trigger('click'); await flushPromises()
|
||||
expect(w.findComponent(table).props('page')).toBe(2)
|
||||
expect(w.get('button[data-column="physicalQuantity"]').attributes('aria-label')).toContain('当前降序')
|
||||
expect(w.get('button[data-column="outbound15"]').attributes('aria-label')).not.toContain('当前')
|
||||
expect(w.findComponent(table).props('data')).toHaveLength(1)
|
||||
})
|
||||
it('supports owner numeric sorting and restores the default header on reset', async () => {
|
||||
const w = page(); await flushPromises()
|
||||
await change(w, { groupBy: 'OWNER' })
|
||||
expect(w.find('button[data-column="inventoryCoverageDays"]').exists()).toBe(false)
|
||||
await w.get('button[data-column="averageDailyOutboundQuantity"]').trigger('click'); await flushPromises()
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ groupBy: 'OWNER', sortBy: 'AVERAGE_DAILY_DESC' })
|
||||
w.findComponent(toolbar).vm.$emit('reset'); await flushPromises()
|
||||
expect(w.get('button[data-column="physicalQuantity"]').attributes('aria-label')).toContain('当前降序')
|
||||
})
|
||||
it('keeps published historical averages visible while explaining pending rebuild separately', async () => {
|
||||
mocks.turnover.mockImplementation(async (q: TurnoverQuery) => {
|
||||
const result = response(q)
|
||||
result.context.analyticsContext.rebuildRequired = true
|
||||
result.context.analyticsContext.salesPublishedAt = '2026-10-14T01:40:24Z'
|
||||
result.charts!.averageDaily = row.windows.map(window => ({ ...window,
|
||||
averageDailyOutboundQuantity: window.days === 7 ? 1234.567143 : window.days === 15 ? 2100 : null,
|
||||
outboundQuantity: window.days === 30 ? null : window.outboundQuantity,
|
||||
completeDayCount: Math.min(window.days, 18), unknownDayCount: window.days === 30 ? 12 : 0,
|
||||
dataStatus: window.days === 30 ? 'PARTIAL' : 'COMPLETE',
|
||||
}))
|
||||
return result
|
||||
})
|
||||
const w = page(); await flushPromises()
|
||||
expect(w.text()).toContain('1,234.57 件/日')
|
||||
expect(w.text()).toContain('2,100 件/日')
|
||||
expect(w.text()).toContain('完整 18 / 30 日')
|
||||
expect(w.text()).toContain('已发布销量版本计算')
|
||||
expect(w.text()).toContain('新采集数据待重算')
|
||||
expect(w.findAllComponents({ name: 'AnalyticsChart' }).length).toBeGreaterThan(0)
|
||||
mocks.turnover.mockImplementation(async (q: TurnoverQuery) => response(q))
|
||||
await w.findAll('button').find(b => b.text() === '整体刷新')!.trigger('click'); await flushPromises()
|
||||
expect(w.text()).not.toContain('新采集数据待重算')
|
||||
})
|
||||
it('queries fast and slow tabs with a visible default threshold, preserving custom values', async () => {
|
||||
const w = page(); await flushPromises()
|
||||
const select = async (label: string) => { await w.findAll('.turnover-focus button').find(b => b.text() === label)!.trigger('click'); await flushPromises() }
|
||||
await select('消化较慢')
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ focus: 'COVERAGE_GT', coverageDaysThreshold: 30, sortBy: 'COVERAGE_DESC', current: 1 })
|
||||
expect(w.find('.turnover-focus button[aria-pressed="true"]').text()).toBe('消化较慢')
|
||||
await w.get('input[aria-label="快慢阈值天数"]').setValue('60')
|
||||
await select('消化较快')
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ focus: 'COVERAGE_LE', coverageDaysThreshold: 60, sortBy: 'COVERAGE_ASC' })
|
||||
const calls = mocks.turnover.mock.calls.length
|
||||
await w.get('input[aria-label="快慢阈值天数"]').setValue('0')
|
||||
await select('消化较慢')
|
||||
expect(mocks.turnover).toHaveBeenCalledTimes(calls)
|
||||
expect(w.get('.turnover-filter-feedback').text()).toContain('正整数')
|
||||
expect(w.find('.turnover-focus button[aria-pressed="true"]').text()).toBe('消化较快')
|
||||
})
|
||||
it('queries the remaining focus tabs and uses owner-compatible sorting', async () => {
|
||||
const w = page(); await flushPromises()
|
||||
await change(w, { groupBy: 'OWNER' })
|
||||
for (const [label, focus, sortBy] of [['近30日无出库', 'NO_OUTBOUND_30D', 'NO_OUTBOUND_PHYSICAL_DESC'], ['待评估', 'UNASSESSABLE', 'PHYSICAL_DESC'], ['消化较慢', 'COVERAGE_GT', 'PHYSICAL_DESC'], ['消化较快', 'COVERAGE_LE', 'PHYSICAL_DESC'], ['全部', 'ALL', 'PHYSICAL_DESC']]) {
|
||||
await w.findAll('.turnover-focus button').find(b => b.text() === label)!.trigger('click'); await flushPromises()
|
||||
expect(mocks.turnover.mock.calls.at(-1)![0]).toMatchObject({ groupBy: 'OWNER', focus, sortBy, current: 1 })
|
||||
}
|
||||
})
|
||||
it('shows stale inventory and failed collection without presenting unknown totals as zero', async () => {
|
||||
mocks.overview.mockResolvedValue({ warehouses: [{ warehouseCode: 'A', dataFamily: 'INVENTORY', observedAt: '2026-10-14T00:48:00Z', currentTask: { taskNo: 'LXCOL-TEST-STOCK', taskStatus: 'DEAD', latestAttempt: { status: 'FAILED', errorCode: 'SELFOP_STOCK_DUPLICATE' } } }] })
|
||||
mocks.turnover.mockImplementation(async (q: TurnoverQuery) => {
|
||||
|
||||
Reference in New Issue
Block a user