Files
admin-web/src/modules/overseas-warehouse/components/WarehouseTransferReceiptPanel.test.ts
T

316 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ElementPlus from 'element-plus'
import { defineComponent, h } from 'vue'
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import WarehouseTransferReceiptPanel from './WarehouseTransferReceiptPanel.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { ApiResponseError } from '@/shared/api/http'
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, increaseWarehouseTransferForecast, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn(), getWarehouseTransferCartonLabel: vi.fn(), increaseWarehouseTransferForecast: vi.fn() }))
const can = vi.hoisted(() => vi.fn(() => true))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
enableAutoUnmount(afterEach)
const DataTableStub = defineComponent({
props: ['data', 'columns'],
setup(props, { slots }) { return () => h('div', { class: 'test-table' }, [JSON.stringify(props.data), ...(props.data || []).flatMap((row: object) => [slots.boxAction?.({ row }), slots.quantityDifference?.({ row }), slots.boxDifference?.({ row })])]) },
})
const DialogStub = defineComponent({ props: ['modelValue', 'confirmText', 'confirmDisabled', 'confirmLoading'], emits: ['confirm'], setup(props, { slots, emit }) { return () => props.modelValue ? h('section', { 'data-test': 'box-dialog' }, [slots.default?.(), props.confirmText ? h('button', { disabled: props.confirmDisabled || props.confirmLoading, onClick: () => emit('confirm') }, props.confirmText) : null]) : null } })
const render = (transferNo = 'WT-SOURCE') => mount(WarehouseTransferReceiptPanel, { props: { customerId: 'CUSTOMER', transferNo }, global: { plugins: [ElementPlus], stubs: { DataTable: DataTableStub, AppDialog: DialogStub } } })
function fixture(): WarehouseTransferDetailDto {
return {
id: '1', transferNo: 'WT-SOURCE', status: 'WAIT_CUSTOMER_CONFIRMATION', needRelabel: false, packageCount: 4,
items: [{ id: '10', skuCode: 'SKU-A', productNameZh: '产品 A', quantity: '120', packageCount: 3 }, { id: '20', skuCode: 'SKU-B', productNameZh: '产品 B', quantity: 30, packageCount: 1 }],
cartons: [
{ cartonMark: 'BOX-1', cartonIndex: 1, totalCartons: 4, status: 'COUNTED', transferItemId: '10', transferPalletId: '100', quantity: '8', length: 100, width: 20, height: 50, weight: '2.5', countedAt: '2026-10-15T12:00:00' },
{ cartonMark: 'BOX-2', cartonIndex: 2, totalCartons: 4, status: 'COUNTED', transferItemId: '10', transferPalletId: '100', quantity: 12, length: 100, width: 20, height: 50, weight: 3.5 },
{ cartonMark: 'BOX-3', cartonIndex: 3, totalCartons: 4, status: 'COUNTED', transferItemId: '20', transferPalletId: '200', quantity: 7, length: 100, width: 20, height: 50, weight: 4 },
{ cartonMark: 'BOX-MISSING', cartonIndex: 4, totalCartons: 4, status: 'NOT_RECEIVED', transferItemId: '10', quantity: 999, weight: 999 },
],
pallets: [{ id: '100', sequenceNo: 1, palletNo: 'PALLET-A', cartonCount: 2, grossWeightKg: 6, volumeCbm: .2, status: 'COMPLETED' }, { id: '200', sequenceNo: 2, palletNo: 'PALLET-B', cartonCount: 1, grossWeightKg: 4, volumeCbm: .1, status: 'COMPLETED' }], relabelBindings: [],
}
}
beforeEach(() => { can.mockReset().mockReturnValue(true); vi.mocked(getWarehouseTransfer).mockReset(); vi.mocked(getWarehouseTransferCartonLabel).mockReset(); vi.mocked(increaseWarehouseTransferForecast).mockReset() })
afterEach(() => vi.restoreAllMocks())
describe('转运预报与实际签收明细', () => {
it('keeps historical receipt totals after discard or shipment and leaves unknown declaration differences blank', async () => {
const value = fixture()
value.items[0]!.quantity = null
value.cartons[0]!.status = 'DISCARD_PENDING'
value.cartons[1]!.status = 'SHIPPED'
value.cartons[2]!.status = 'DISCARDED'
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
const wrapper = render(); await flushPromises()
expect(wrapper.emitted('summary')!.at(-1)![0]).toMatchObject({ boxes: 3, quantity: 27, skus: 2, forecastQuantity: null })
const rows = wrapper.findAllComponents(DataTableStub)[0]!.props('data')
expect(rows[0]).toMatchObject({ receivedQuantity: 20, receivedBoxes: 2, quantityDifference: null })
expect(wrapper.findAll('.test-table')[0]!.text()).toContain('未记录')
})
it('shows every SKU in a mixed box and counts its physical measurements once', async () => {
const value = fixture()
value.cartons = [{ ...value.cartons[0]!, quantity: 10, items: [{ transferItemId: '10', skuCode: 'SKU-A', quantity: 3 }, { transferItemId: '20', skuCode: 'SKU-B', quantity: 7 }] }]
value.pallets = [value.pallets[0]!]
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
const wrapper = render(); await flushPromises()
expect(wrapper.emitted('summary')!.at(-1)![0]).toMatchObject({ boxes: 1, skus: 2, quantity: 10, weight: 2.5, volume: .1 })
const comparison = wrapper.findAllComponents(DataTableStub)[0]!.props('data')
expect(comparison[0]).toMatchObject({ receivedQuantity: 3, receivedBoxes: 1 })
expect(comparison[1]).toMatchObject({ receivedQuantity: 7, receivedBoxes: 1 })
expect(wrapper.findAll('.test-table')[1]!.text()).toContain('SKU-A × 3 件;SKU-B × 7 件')
await wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.trigger('click')
await wrapper.findAll('button').find(button => button.text() === '箱内明细')!.trigger('click')
const contents = wrapper.findAll('[data-test="box-dialog"]')[1]!.text()
expect(contents).toContain('SKU-A'); expect(contents).toContain('SKU-B')
expect(contents).toContain('产品 A'); expect(contents).toContain('产品 B')
})
it('keeps declarations separate and totals only counted boxes across pallets', async () => {
vi.mocked(getWarehouseTransfer).mockResolvedValue(fixture())
const wrapper = render(); await flushPromises()
expect(getWarehouseTransfer).toHaveBeenCalledWith('CUSTOMER', 'WT-SOURCE')
const summary = wrapper.emitted('summary')!.at(-1)![0]
expect(summary).toMatchObject({ forecastBoxes: 4, boxes: 3, quantity: 27, weight: 10 })
expect((summary as { volume: number }).volume).toBeCloseTo(.3)
const rows = wrapper.findAllComponents(DataTableStub)[0]!.props('data')
expect(rows[0]).toMatchObject({ quantity: '120', receivedQuantity: 20, receivedBoxes: 2, quantityDifference: -100, boxDifference: -1 })
expect(rows[1]).toMatchObject({ receivedQuantity: 7, quantityDifference: -23, boxDifference: 0 })
const tables = wrapper.findAll('.test-table')
expect(tables[0]!.text()).toContain('120')
expect(tables[1]!.text()).toContain('SKU-A × 20 件'); expect(tables[1]!.text()).toContain('SKU-B × 7 件')
expect(tables).toHaveLength(2)
await wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.trigger('click')
expect(wrapper.findAll('.test-table')[2]!.text()).not.toContain('BOX-MISSING')
await wrapper.findAll('button').find(button => button.text() === '箱内明细')!.trigger('click')
const box = wrapper.findAll('[data-test="box-dialog"]')[1]!
expect(box.text()).toContain('BOX-1'); expect(box.text()).toContain('产品 A'); expect(box.text()).toContain('"quantity":"8"')
expect(box.text()).not.toContain('120')
})
it('shows zero receipts before signing without treating forecast cartons as actual receipts', async () => {
const value = fixture(); value.pallets = []; value.cartons.forEach(box => { box.status = 'EXPECTED' })
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
const wrapper = render(); await flushPromises()
expect(wrapper.emitted('summary')!.at(-1)![0]).toMatchObject({ boxes: 0, quantity: 0 })
expect(wrapper.findAll('.test-table')[0]!.text()).toContain('待签收')
expect(wrapper.findAll('.test-table')).toHaveLength(2)
expect(wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.attributes('disabled')).toBeDefined()
})
it('shows signed positive and zero differences against each forecast row', async () => {
const value = fixture(); value.items[0]!.quantity = 10; value.items[0]!.packageCount = 2
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
const wrapper = render(); await flushPromises()
const table = wrapper.findAll('.test-table')[0]!
expect(table.text()).toContain('+10')
expect(table.findAll('.receipt-difference').map(cell => cell.text())).toEqual(['+10', '-23'])
expect(wrapper.findAllComponents(DataTableStub)[0]!.props('data')[0]).toMatchObject({ quantityDifference: 10, boxDifference: 0 })
})
it('prints server label HTML with the selected customer and source reference', async () => {
vi.mocked(getWarehouseTransfer).mockResolvedValue(fixture())
vi.mocked(getWarehouseTransferCartonLabel).mockResolvedValue({ transferNo: 'WT-SOURCE', cartons: fixture().cartons, printableHtml: '<html>SERVER-BOX-LABEL</html>' })
const popup = { opener: window, document: { open: vi.fn(), write: vi.fn(), close: vi.fn() }, focus: vi.fn(), print: vi.fn(), close: vi.fn() }
vi.spyOn(window, 'open').mockReturnValue(popup as unknown as Window)
const wrapper = render(); await flushPromises()
await wrapper.findAll('button').find(button => button.text() === '打印箱唛')!.trigger('click'); await flushPromises()
expect(getWarehouseTransferCartonLabel).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE')
expect(popup.document.write).toHaveBeenCalledWith('<html>SERVER-BOX-LABEL</html>')
expect(popup.print).toHaveBeenCalledOnce()
expect(popup.opener).toBeNull()
})
it('closes the print window and leaves receipt details available when labels fail', async () => {
vi.mocked(getWarehouseTransfer).mockResolvedValue(fixture())
vi.mocked(getWarehouseTransferCartonLabel).mockRejectedValue(new Error('标签暂不可用'))
const popup = { close: vi.fn(), print: vi.fn() }
vi.spyOn(window, 'open').mockReturnValue(popup as unknown as Window)
const wrapper = render(); await flushPromises()
await wrapper.findAll('button').find(button => button.text() === '打印箱唛')!.trigger('click'); await flushPromises()
expect(popup.close).toHaveBeenCalledOnce(); expect(popup.print).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('箱唛加载失败,请重试')
expect(wrapper.text()).toContain('签收箱号明细(3 箱)')
})
it('does not request another order when the source reference is missing', async () => {
const wrapper = render(''); await flushPromises()
expect(getWarehouseTransfer).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('缺少客户或来源转运单号')
})
it('discards a stale response after switching transfers', async () => {
let resolveOld!: (value: WarehouseTransferDetailDto) => void
vi.mocked(getWarehouseTransfer).mockImplementationOnce(() => new Promise(resolve => { resolveOld = resolve })).mockResolvedValueOnce({ ...fixture(), transferNo: 'WT-NEW', packageCount: 9 })
const wrapper = render(); await wrapper.setProps({ transferNo: 'WT-NEW' }); await flushPromises()
resolveOld(fixture()); await flushPromises()
expect(wrapper.emitted('summary')!.at(-1)![0]).toMatchObject({ forecastBoxes: 9 })
})
})
// 完成原因直接展示,多个箱保存同一原因时只显示一次。
it('displays saved discrepancy reason once without opening box details', async () => {
const value = fixture()
value.cartons.forEach(box => { box.discrepancyReason = '短收一箱,客户漏发' })
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
const wrapper = render(); await flushPromises()
expect(wrapper.findAll('.receipt-discrepancy-reason')).toHaveLength(1)
expect(wrapper.find('.receipt-discrepancy-reason').text()).toBe('实收差异原因:短收一箱,客户漏发')
})
/** 平台只填新增箱数,保留SKU预报并按原箱数校验。 */
it('平台可补整单预报箱数,无需选择SKU', async () => {
const value = { ...fixture(), inboundCode: 'WT-SOURCE', status: 'WAIT_PDA_RECEIVING' }
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
vi.mocked(increaseWarehouseTransferForecast).mockResolvedValue({ ...value, packageCount: 6 })
const wrapper = render(); await flushPromises()
await wrapper.findAll('button').find(button => button.text() === '增加预报箱数')!.trigger('click')
const number = wrapper.findComponent({ name: 'ElInputNumber' }); await number.vm.$emit('update:modelValue', 2)
await wrapper.findAll('button').find(button => button.text() === '确认增加')!.trigger('click'); await flushPromises()
expect(increaseWarehouseTransferForecast).toHaveBeenCalledWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 })
expect(wrapper.text()).toContain('预报箱数 6 箱')
})
async function nativeReceipt() {
const wrapper = mount(WarehouseTransferReceiptPanel, { attachTo: document.body, props: { customerId: 'CUSTOMER', transferNo: 'WT-SOURCE' }, global: { plugins: [ElementPlus], stubs: { transition: false } } })
await flushPromises()
return wrapper
}
function popup(wrapper: VueWrapper, title: string) { return wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('title').startsWith(title))! }
async function openNativeForecast() {
const wrapper = await nativeReceipt()
await wrapper.findAll('button').find(button => button.text() === '增加预报箱数')!.trigger('click'); await flushPromises()
return { wrapper, dialog: popup(wrapper, '增加预报箱数') }
}
function forecastFixture() { return { ...fixture(), inboundCode: 'WT-SOURCE', status: 'WAIT_PDA_RECEIVING' } }
describe('receipt native popup integration', () => {
it('keeps counted boxes and mixed SKU contents through two independent fullscreen frames', async () => {
const value = fixture()
value.cartons[0] = { ...value.cartons[0]!, quantity: 10, discrepancyReason: '少收两件,客户确认', items: [{ transferItemId: '10', skuCode: 'SKU-A', quantity: 3 }, { transferItemId: '20', skuCode: 'SKU-B', quantity: 7 }] }
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
const wrapper = await nativeReceipt()
await wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.trigger('click'); await flushPromises()
const cartonsDialog = popup(wrapper, '签收箱号明细')
expect(cartonsDialog.props('title')).toBe('签收箱号明细 · WT-SOURCE')
expect(cartonsDialog.get('[aria-label="已清点箱号"]').findAll('.el-table__body-wrapper .el-table__row')).toHaveLength(3)
expect(cartonsDialog.text()).not.toContain('BOX-MISSING')
await cartonsDialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
await cartonsDialog.findAll('.el-table__body-wrapper button').find(button => button.text() === '箱内明细')!.trigger('click'); await flushPromises()
const boxDialog = popup(wrapper, '箱内清点明细')
expect(boxDialog.props('title')).toBe('箱内清点明细 · BOX-1')
await boxDialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
expect(boxDialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true)
expect(boxDialog.findComponent(DataTable).props('data')).toEqual([{ sku: 'SKU-A', name: '产品 A', quantity: 3 }, { sku: 'SKU-B', name: '产品 B', quantity: 7 }])
expect(boxDialog.text()).toContain('少收两件,客户确认')
expect(boxDialog.get('[aria-label="当前签收箱"]').text()).toContain('2026-10-15')
await boxDialog.get('.el-dialog__headerbtn').trigger('click'); await new Promise(resolve => setTimeout(resolve, 80)); await flushPromises()
expect(boxDialog.props('modelValue')).toBe(false)
expect(cartonsDialog.props('modelValue')).toBe(true)
expect(cartonsDialog.findComponent({ name: 'ElDialog' }).props('fullscreen')).toBe(true)
expect(getWarehouseTransfer).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE')
})
it('retains every counted box in the main dialog body without a nested height cap', async () => {
const value = fixture()
value.cartons = Array.from({ length: 30 }, (_, index) => ({ ...value.cartons[0]!, cartonMark: `BOX-${index + 1}`, cartonIndex: index + 1, totalCartons: 30 }))
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
const wrapper = await nativeReceipt()
await wrapper.findAll('button').find(button => button.text() === '查看箱号明细')!.trigger('click'); await flushPromises()
const dialog = popup(wrapper, '签收箱号明细')
expect(dialog.get('[aria-label="实收箱数核对"]').text()).toContain('30 箱')
expect(dialog.findAll('.el-table__body-wrapper .el-table__row')).toHaveLength(30)
expect(dialog.text()).toContain('BOX-30')
expect(dialog.findComponent(DataTable).props('maxHeight')).toBe('')
expect((dialog.get('.el-table__body-wrapper .el-scrollbar__wrap').element as HTMLElement).style.maxHeight).toBe('')
})
it('freezes the original forecast count through fullscreen and sends only the scoped count command', async () => {
const value = forecastFixture()
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
vi.mocked(increaseWarehouseTransferForecast).mockResolvedValue({ ...value, packageCount: 6 })
const { wrapper, dialog } = await openNativeForecast()
expect(dialog.props('title')).toBe('增加预报箱数 · WT-SOURCE')
expect(dialog.get('[aria-label="预报箱数核对"]').findAll('input')).toHaveLength(0)
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2); await flushPromises()
await dialog.get('button[aria-label="展开全屏"]').trigger('click'); await flushPromises()
expect(dialog.get('[aria-label="预报箱数核对"]').text()).toContain('增加后6 箱')
expect(dialog.findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(2)
await dialog.findAll('button').find(button => button.text() === '确认增加')!.trigger('click'); await flushPromises()
expect(increaseWarehouseTransferForecast).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 })
expect(dialog.props('modelValue')).toBe(false)
expect(wrapper.text()).toContain('预报箱数 6 箱')
})
it('locks pending edits and closing, preserves the failed count and retries the same snapshot', async () => {
const value = forecastFixture()
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
let reject!: (reason: unknown) => void
vi.mocked(increaseWarehouseTransferForecast).mockReturnValueOnce(new Promise((_, no) => { reject = no })).mockResolvedValueOnce({ ...value, packageCount: 6 })
const { dialog } = await openNativeForecast()
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2); await flushPromises()
dialog.vm.$emit('confirm'); dialog.vm.$emit('confirm'); await flushPromises()
expect(increaseWarehouseTransferForecast).toHaveBeenCalledTimes(1)
expect(dialog.findComponent({ name: 'ElDialog' }).props()).toMatchObject({ showClose: false, closeOnPressEscape: false })
expect(dialog.get('input[placeholder="请输入新增箱数"]').attributes('disabled')).toBeDefined()
expect(dialog.findAll('button').find(button => button.text() === '取消')!.attributes('disabled')).toBeDefined()
reject(new ApiResponseError('箱数已被其他操作修改', 'A0409', 409, null, 'forecast-frame-request')); await flushPromises()
expect(dialog.props('modelValue')).toBe(true)
expect(dialog.findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(2)
expect(dialog.text()).toContain('forecast-frame-request')
expect(dialog.get('[aria-label="预报箱数核对"]').text()).toContain('原预报箱数4 箱')
await dialog.findAll('button').find(button => button.text() === '重试本次增加')!.trigger('click'); await flushPromises()
expect(vi.mocked(increaseWarehouseTransferForecast).mock.calls).toEqual([
['CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 }],
['CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 }],
])
})
it('keeps whole-box and 5000-box limits at both the frame and submit handler', async () => {
const value = { ...forecastFixture(), packageCount: 4999 }
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
vi.mocked(increaseWarehouseTransferForecast).mockResolvedValue({ ...value, packageCount: 5000 })
const { dialog } = await openNativeForecast()
const input = dialog.findComponent({ name: 'ElInputNumber' })
expect(input.props('max')).toBe(1)
for (const count of [0, -1, 1.5, Number.NaN, 2]) {
input.vm.$emit('update:modelValue', count)
dialog.vm.$emit('confirm'); await flushPromises()
expect(increaseWarehouseTransferForecast).not.toHaveBeenCalled()
input.vm.$emit('update:modelValue', 1); await flushPromises()
}
input.vm.$emit('update:modelValue', 1); await flushPromises()
expect(dialog.props('confirmDisabled')).toBe(false)
dialog.vm.$emit('confirm'); await flushPromises()
expect(increaseWarehouseTransferForecast).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4999, packageCount: 5000 })
})
it('resets a cancelled draft and its failure context when reopened', async () => {
vi.mocked(getWarehouseTransfer).mockResolvedValue(forecastFixture())
vi.mocked(increaseWarehouseTransferForecast).mockRejectedValue(new ApiResponseError('增加失败', 'A0503', 503, null, 'old-forecast-request'))
const { wrapper, dialog } = await openNativeForecast()
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2); await flushPromises()
dialog.vm.$emit('confirm'); await flushPromises()
expect(dialog.text()).toContain('old-forecast-request')
await dialog.findAll('button').find(button => button.text() === '取消')!.trigger('click'); await flushPromises()
await wrapper.findAll('button').find(button => button.text() === '增加预报箱数')!.trigger('click'); await flushPromises()
expect(dialog.findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(1)
expect(dialog.findComponent(LoadErrorState).exists()).toBe(false)
expect(increaseWarehouseTransferForecast).toHaveBeenCalledTimes(1)
})
it('closes the old forecast frame on a scope change and ignores a late failure', async () => {
vi.mocked(getWarehouseTransfer).mockResolvedValueOnce(forecastFixture()).mockResolvedValueOnce({ ...forecastFixture(), transferNo: 'WT-NEW', inboundCode: 'WT-NEW', packageCount: 8 })
let reject!: (reason: unknown) => void
vi.mocked(increaseWarehouseTransferForecast).mockReturnValueOnce(new Promise((_, no) => { reject = no }))
const { wrapper, dialog } = await openNativeForecast()
dialog.findComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 2); await flushPromises()
dialog.vm.$emit('confirm'); await flushPromises()
await wrapper.setProps({ customerId: 'CUSTOMER-NEW', transferNo: 'WT-NEW' }); await flushPromises()
expect(dialog.props('modelValue')).toBe(false)
reject(new ApiResponseError('旧票响应', 'A0503', 503, null, 'stale-forecast-request')); await flushPromises()
expect(wrapper.text()).toContain('预报箱数 8 箱')
expect(wrapper.text()).not.toContain('stale-forecast-request')
await wrapper.findAll('button').find(button => button.text() === '增加预报箱数')!.trigger('click'); await flushPromises()
expect(dialog.props('title')).toBe('增加预报箱数 · WT-NEW')
expect(dialog.get('[aria-label="预报箱数核对"]').text()).toContain('原预报箱数8 箱')
expect(dialog.findComponent({ name: 'ElInputNumber' }).props('modelValue')).toBe(1)
expect(increaseWarehouseTransferForecast).toHaveBeenCalledExactlyOnceWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 })
})
it('keeps forecast modification unavailable without approval permission', async () => {
can.mockReturnValue(false)
vi.mocked(getWarehouseTransfer).mockResolvedValue(forecastFixture())
const wrapper = await nativeReceipt()
expect(wrapper.findAll('button').some(button => button.text() === '增加预报箱数')).toBe(false)
popup(wrapper, '增加预报箱数').vm.$emit('confirm'); await flushPromises()
expect(increaseWarehouseTransferForecast).not.toHaveBeenCalled()
})
})