122 lines
13 KiB
TypeScript
122 lines
13 KiB
TypeScript
import { flushPromises, mount, enableAutoUnmount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import ElementPlus from 'element-plus'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import WarehouseTransferMovementPanel from './WarehouseTransferMovementPanel.vue'
|
|
import { getWarehouseTransfer, outboundWarehouseTransfer, receiveWarehouseTransfer, listTransferReceivingWarehouses, type WarehouseTransferDetailDto, type WarehouseTransferReceiptCommandDto } from '../api/warehouseTransfers'
|
|
import { getPallet, listPallets } from '../api/pallets'
|
|
import { getOverseasLocation } from '../api/locations'
|
|
const { can } = vi.hoisted(() => ({ can: vi.fn() }))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
|
vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn(), outboundWarehouseTransfer: vi.fn(), receiveWarehouseTransfer: vi.fn(), listTransferReceivingWarehouses: vi.fn() }))
|
|
vi.mock('../api/pallets', () => ({ getPallet: vi.fn(), listPallets: vi.fn() }))
|
|
vi.mock('../api/locations', () => ({ getOverseasLocation: vi.fn() }))
|
|
enableAutoUnmount(afterEach)
|
|
const detail = (status = 'ALLOCATED'): WarehouseTransferDetailDto => ({ id: 'hidden', transferNo: 'WT-1', packageCount: 1, status, needRelabel: false, fbaWarehouseCode: 'ABE4', destinationWarehouseCode: 'ABE4', items: [], cartons: [], pallets: [], relabelBindings: [], receiptCommands: [], allocations: [{ inventoryPositionId: 'hidden-source', inboundOrderId: 'hidden-inbound', inboundCode: 'IN-1', palletId: 'hidden-la', palletNo: 'LA-CODE', skuCode: 'NEW', packageCount: 1, status: status === 'ALLOCATED' ? 'RESERVED' : 'SHIPPED' }] })
|
|
const render = (value = detail()) => mount(WarehouseTransferMovementPanel, { props: { customerId: 'customer', detail: value }, global: { plugins: [ElementPlus] } })
|
|
const button = (wrapper: ReturnType<typeof render>, label: string) => wrapper.findAll('button').find(x => x.text() === label)!
|
|
describe('transfer movement UI', () => {
|
|
beforeEach(() => { vi.clearAllMocks(); sessionStorage.clear(); can.mockReturnValue(true) })
|
|
it('uses the overseas resource warehouse permission and does not require warehouse master access', async () => {
|
|
can.mockImplementation(code => code !== 'platform:warehouse:list')
|
|
const permitted = render(detail('OUTBOUNDED')); expect(permitted.text()).toContain('登记目标仓实收')
|
|
can.mockImplementation(code => code !== 'platform:overseas-warehouse:warehouse:list')
|
|
const denied = render(detail('OUTBOUNDED')); expect(denied.text()).not.toContain('登记目标仓实收'); expect(listTransferReceivingWarehouses).not.toHaveBeenCalled()
|
|
})
|
|
it.each(['{bad', JSON.stringify({pending:{type:'receipt',body:{idempotencyKey:'old'}}})])('ignores stored failed receipts without blocking current selection', async stored => {
|
|
sessionStorage.setItem('warehouse-transfer-movement:customer:WT-1', stored)
|
|
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [] })
|
|
const wrapper = render(detail('OUTBOUNDED')); await button(wrapper, '登记目标仓实收').trigger('click'); await flushPromises()
|
|
expect(wrapper.find('[aria-label="目标仓实收选择"]').exists()).toBe(true)
|
|
expect(receiveWarehouseTransfer).not.toHaveBeenCalled(); expect(wrapper.text()).not.toContain('重试原操作')
|
|
})
|
|
it.each(['PREPARED', 'RESERVED'] as const)('continues a %s source from current server state and unlocks other sources after failure', async status => {
|
|
const command: WarehouseTransferReceiptCommandDto = { sourceInventoryPositionId: 'hidden-source', status: 'RESERVED', requestHash: 'server-hash', request: { sourceInventoryPositionId: 'hidden-source', destinationOverseasWarehouseId: 'NY', destinationOverseasWarehouseCode: 'NY-CODE', destinationOverseasWarehouseName: '纽约', destinationWarehouseCode: 'ABE4', destinationPalletId: 'P-NY', destinationPalletNo: 'PALLET-NY', warehouseLocationId: 'L-NY', warehouseLocationCode: 'LOC-NY', batchNo: 'WT-1', idempotencyKey: 'server-original' }, target: { warehouseId: 'NY', warehouseCode: 'NY-CODE', warehouseName: '纽约', palletId: 'P-NY', palletNo: 'PALLET-NY', locationId: 'L-NY', locationCode: 'LOC-NY', fbaCode: 'ABE4' } }
|
|
command.status = status
|
|
const d = detail('OUTBOUNDED'); d.receiptCommands = [command]
|
|
d.allocations!.push({ ...d.allocations![0]!, inventoryPositionId: 'other-source', inboundCode: 'IN-2' })
|
|
const completed = detail('RECEIVED'); completed.allocations![0]!.status = 'RECEIVED'; completed.receiptCommands = [{ ...command, status: 'COMPLETED' }]
|
|
vi.mocked(getWarehouseTransfer).mockResolvedValue(d)
|
|
vi.mocked(receiveWarehouseTransfer).mockRejectedValueOnce(new Error('response lost')).mockResolvedValueOnce(completed)
|
|
const wrapper = render(d); await flushPromises()
|
|
expect(wrapper.text()).not.toContain('重试原操作'); expect(wrapper.text()).toContain('登记目标仓实收')
|
|
expect(receiveWarehouseTransfer).not.toHaveBeenCalled()
|
|
expect(getWarehouseTransfer).not.toHaveBeenCalled()
|
|
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
|
|
expect(receiveWarehouseTransfer).toHaveBeenCalledExactlyOnceWith('customer', 'WT-1', command.request)
|
|
expect(getWarehouseTransfer).toHaveBeenCalledTimes(1)
|
|
expect(button(wrapper, '登记目标仓实收').attributes('disabled')).toBeUndefined()
|
|
expect(button(wrapper, '确认本来源实收').attributes('disabled')).toBeUndefined()
|
|
expect(sessionStorage.length).toBe(0)
|
|
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
|
|
expect(getWarehouseTransfer).toHaveBeenCalledTimes(2)
|
|
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(2)
|
|
expect(wrapper.emitted('updated')?.at(-1)?.[0]).toEqual(completed)
|
|
// 服务端已完成时只展示最新结果,不再次提交。
|
|
vi.mocked(getWarehouseTransfer).mockResolvedValue(completed)
|
|
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
|
|
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(2)
|
|
expect(listPallets).not.toHaveBeenCalled(); expect(getPallet).not.toHaveBeenCalled()
|
|
expect(wrapper.text()).not.toContain('hidden-source')
|
|
})
|
|
it('does not use a browser remembered occupied target when server receipt history is missing', async () => {
|
|
sessionStorage.setItem('warehouse-transfer-movement:customer:WT-1', JSON.stringify({ pending: null, previous: { warehouse: { id: 'OTHER' }, pallet: { id: 'OTHER', status: 'OCCUPIED' } } }))
|
|
const d = detail('PARTIALLY_RECEIVED'); delete d.receiptCommands
|
|
const wrapper = render(d); expect(wrapper.text()).toContain('尚未返回服务端实收操作记录'); expect(wrapper.text()).not.toContain('登记目标仓实收'); expect(getPallet).not.toHaveBeenCalled()
|
|
})
|
|
it('permits same occupied target from completed server history in a fresh browser', async () => {
|
|
const d = detail('PARTIALLY_RECEIVED')
|
|
const completed: WarehouseTransferReceiptCommandDto = { sourceInventoryPositionId: 'already-received', status: 'COMPLETED', requestHash: 'hash', request: { sourceInventoryPositionId: 'already-received', destinationOverseasWarehouseId: 'NY', destinationOverseasWarehouseCode: 'NY-CODE', destinationOverseasWarehouseName: '纽约', destinationWarehouseCode: 'ABE4', destinationPalletId: 'P-NY', destinationPalletNo: 'PALLET-NY', warehouseLocationId: 'L-NY', warehouseLocationCode: 'LOC-NY', batchNo: 'WT-1', idempotencyKey: 'first-key' }, target: { warehouseId: 'NY', warehouseCode: 'NY-CODE', warehouseName: '纽约', palletId: 'P-NY', palletNo: 'PALLET-NY', locationId: 'L-NY', locationCode: 'LOC-NY', fbaCode: 'ABE4' } }
|
|
d.receiptCommands = [completed]
|
|
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [{ id: 'NY', customerId: '0', warehouseCode: 'NY-CODE', warehouseName: '纽约', warehouseType: 'OVERSEAS', status: 'ACTIVE' }] })
|
|
vi.mocked(getPallet).mockResolvedValue({ id: 'P-NY', customerId: '0', warehouseId: 'NY', locationId: 'L-NY', palletCode: 'PALLET-NY', palletType: 'STANDARD', fbaWarehouseCode: 'ABE4', capacityCbm: 2, unitPrice: 0, status: 'OCCUPIED', version: 1 })
|
|
vi.mocked(getOverseasLocation).mockResolvedValue({ id: 'L-NY', warehouseId: 'NY', locationCode: 'LOC-NY', locationType: 'NORMAL', sharedOverflow: false, status: 'OCCUPIED', version: 1 })
|
|
const wrapper = render(d); await button(wrapper, '登记目标仓实收').trigger('click'); await flushPromises()
|
|
expect(wrapper.text()).toContain('目标:NY-CODE / PALLET-NY / LOC-NY')
|
|
expect(getPallet).toHaveBeenCalledExactlyOnceWith('NY', 'P-NY')
|
|
expect(listPallets).not.toHaveBeenCalled()
|
|
await wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'hidden-source'); await flushPromises()
|
|
expect(button(wrapper, '确认本来源实收').attributes('disabled')).toBeUndefined()
|
|
expect(receiveWarehouseTransfer).not.toHaveBeenCalled()
|
|
})
|
|
it('requires outbound permission and releases a failed confirmation for a fresh click', async () => {
|
|
vi.mocked(outboundWarehouseTransfer).mockRejectedValueOnce(new Error('response lost')).mockResolvedValueOnce(detail('OUTBOUNDED'))
|
|
const wrapper = render(); await button(wrapper, '确认本票出库').trigger('click'); expect(outboundWarehouseTransfer).not.toHaveBeenCalled()
|
|
await button(wrapper, '确认实际出库').trigger('click'); await flushPromises()
|
|
expect(button(wrapper, '确认本票出库').attributes('disabled')).toBeUndefined(); expect(sessionStorage.length).toBe(0)
|
|
const original = vi.mocked(outboundWarehouseTransfer).mock.calls[0]![2]
|
|
await button(wrapper, '确认实际出库').trigger('click'); await flushPromises()
|
|
expect(vi.mocked(outboundWarehouseTransfer).mock.calls[1]![2].idempotencyKey).not.toBe(original.idempotencyKey)
|
|
expect(wrapper.emitted('updated')?.[0]?.[0]).toMatchObject({ status: 'OUTBOUNDED' })
|
|
can.mockReturnValue(false); const denied = render(); expect(denied.text()).not.toContain('确认本票出库')
|
|
})
|
|
it('queries only selected warehouse page, reads exact bound location and unlocks the selection after response loss', async () => {
|
|
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [{ id: 'NY', customerId: '0', warehouseCode: 'NY-CODE', warehouseName: '纽约', warehouseType: 'OVERSEAS', status: 'ACTIVE' }] })
|
|
const p = { id: 'P-NY', customerId: '0', warehouseId: 'NY', locationId: 'L-NY', palletCode: 'PALLET-NY', palletType: 'STANDARD', fbaWarehouseCode: 'ABE4', capacityCbm: 2, unitPrice: 0, status: 'IDLE', version: 0 }
|
|
vi.mocked(listPallets).mockResolvedValue({ records: [p], page: 1, size: 20, total: 1, pages: 1 })
|
|
vi.mocked(getPallet).mockResolvedValue(p)
|
|
vi.mocked(getOverseasLocation).mockResolvedValue({ id: 'L-NY', warehouseId: 'NY', locationCode: 'LOC-NY', locationType: 'NORMAL', sharedOverflow: false, status: 'IDLE', version: 0 })
|
|
vi.mocked(receiveWarehouseTransfer).mockRejectedValueOnce(new Error('response lost')).mockResolvedValueOnce(detail('RECEIVED'))
|
|
const wrapper = render(detail('OUTBOUNDED')); await button(wrapper, '登记目标仓实收').trigger('click'); await flushPromises()
|
|
expect(listPallets).not.toHaveBeenCalled()
|
|
await wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'hidden-source'); await wrapper.findAllComponents({ name: 'ElSelect' })[1]!.vm.$emit('update:modelValue', 'NY')
|
|
await wrapper.get('input[aria-label="目标托盘码查询"]').setValue('PALLET-NY'); await button(wrapper, '查询目标托盘').trigger('click'); await flushPromises()
|
|
expect(listPallets).toHaveBeenCalledExactlyOnceWith('NY', { page: 1, size: 20, keyword: 'PALLET-NY', status: 'IDLE' })
|
|
wrapper.findAllComponents({ name: 'ElSelect' })[2]!.vm.$emit('update:modelValue', 'P-NY'); await flushPromises()
|
|
expect(getOverseasLocation).toHaveBeenCalledExactlyOnceWith('NY', 'L-NY')
|
|
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
|
|
const original = JSON.parse(JSON.stringify(vi.mocked(receiveWarehouseTransfer).mock.calls[0]))
|
|
expect(original[2]).toMatchObject({ sourceInventoryPositionId: 'hidden-source', destinationPalletNo: 'PALLET-NY', warehouseLocationCode: 'LOC-NY' })
|
|
expect(button(wrapper, '查询目标托盘').attributes('disabled')).toBeUndefined()
|
|
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(1)
|
|
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
|
|
expect(vi.mocked(receiveWarehouseTransfer).mock.calls[1]![2].idempotencyKey).not.toBe(original[2].idempotencyKey)
|
|
expect(wrapper.text()).not.toContain('hidden-source')
|
|
})
|
|
it('does not request resources when permission is absent or source already received', async () => {
|
|
can.mockImplementation(code => code !== 'platform:overseas-warehouse:location:list')
|
|
const denied = render(detail('OUTBOUNDED')); expect(denied.text()).not.toContain('登记目标仓实收'); expect(listTransferReceivingWarehouses).not.toHaveBeenCalled()
|
|
can.mockReturnValue(true); const done = detail('RECEIVED'); done.allocations![0]!.status = 'RECEIVED'; const wrapper = render(done); expect(wrapper.text()).not.toContain('登记目标仓实收')
|
|
})
|
|
})
|