134 lines
10 KiB
TypeScript
134 lines
10 KiB
TypeScript
import ElementPlus from 'element-plus'
|
||
import { defineComponent, h } from 'vue'
|
||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import WarehouseTransferReceiptPanel from './WarehouseTransferReceiptPanel.vue'
|
||
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||
|
||
vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn(), getWarehouseTransferCartonLabel: vi.fn() }))
|
||
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'], setup(props, { slots }) { return () => props.modelValue ? h('section', { 'data-test': 'box-dialog' }, slots.default?.()) : 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(() => { vi.mocked(getWarehouseTransfer).mockReset(); vi.mocked(getWarehouseTransferCartonLabel).mockReset() })
|
||
afterEach(() => vi.restoreAllMocks())
|
||
describe('转运预报与实际签收明细', () => {
|
||
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('实收差异原因:短收一箱,客户漏发')
|
||
})
|