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: 'SERVER-BOX-LABEL' }) 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('SERVER-BOX-LABEL') 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('实收差异原因:短收一箱,客户漏发') })