2026-10-05 06:16:21 +08:00
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 ()
})
2026-10-22 23:10:09 +08:00
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 ( '重试原操作' )
2026-10-05 06:16:21 +08:00
})
2026-10-22 23:10:09 +08:00
it . each ([ 'PREPARED' , 'RESERVED' ] as const )( 'continues a %s source from current server state and unlocks other sources after failure' , async status => {
2026-10-05 06:16:21 +08:00
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' } }
2026-10-22 23:10:09 +08:00
command . status = status
2026-10-05 06:16:21 +08:00
const d = detail ( 'OUTBOUNDED' ); d . receiptCommands = [ command ]
2026-10-22 23:10:09 +08:00
d . allocations ! . push ({ ... d . allocations ! [ 0 ] ! , inventoryPositionId : 'other-source' , inboundCode : 'IN-2' })
2026-10-05 06:16:21 +08:00
const completed = detail ( 'RECEIVED' ); completed . allocations ! [ 0 ] ! . status = 'RECEIVED' ; completed . receiptCommands = [{ ... command , status : 'COMPLETED' }]
2026-10-22 23:10:09 +08:00
vi . mocked ( getWarehouseTransfer ). mockResolvedValue ( d )
vi . mocked ( receiveWarehouseTransfer ). mockRejectedValueOnce ( new Error ( 'response lost' )). mockResolvedValueOnce ( completed )
2026-10-05 06:16:21 +08:00
const wrapper = render ( d ); await flushPromises ()
2026-10-22 23:10:09 +08:00
expect ( wrapper . text ()). not . toContain ( '重试原操作' ); expect ( wrapper . text ()). toContain ( '登记目标仓实收' )
expect ( receiveWarehouseTransfer ). not . toHaveBeenCalled ()
expect ( getWarehouseTransfer ). not . toHaveBeenCalled ()
await button ( wrapper , '确认本来源实收' ). trigger ( 'click' ); await flushPromises ()
2026-10-05 06:16:21 +08:00
expect ( receiveWarehouseTransfer ). toHaveBeenCalledExactlyOnceWith ( 'customer' , 'WT-1' , command . request )
2026-10-22 23:10:09 +08:00
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 )
2026-10-05 06:16:21 +08:00
expect ( listPallets ). not . toHaveBeenCalled (); expect ( getPallet ). not . toHaveBeenCalled ()
2026-10-22 23:10:09 +08:00
expect ( wrapper . text ()). not . toContain ( 'hidden-source' )
2026-10-05 06:16:21 +08:00
})
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 ()
})
2026-10-22 23:10:09 +08:00
it ( 'requires outbound permission and releases a failed confirmation for a fresh click' , async () => {
2026-10-05 06:16:21 +08:00
vi . mocked ( outboundWarehouseTransfer ). mockRejectedValueOnce ( new Error ( 'response lost' )). mockResolvedValueOnce ( detail ( 'OUTBOUNDED' ))
2026-10-22 23:10:09 +08:00
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' })
2026-10-05 06:16:21 +08:00
can . mockReturnValue ( false ); const denied = render (); expect ( denied . text ()). not . toContain ( '确认本票出库' )
})
2026-10-22 23:10:09 +08:00
it ( 'queries only selected warehouse page, reads exact bound location and unlocks the selection after response loss' , async () => {
2026-10-05 06:16:21 +08:00
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' })
2026-10-22 23:10:09 +08:00
expect ( button ( wrapper , '查询目标托盘' ). attributes ( 'disabled' )). toBeUndefined ()
2026-10-05 06:16:21 +08:00
expect ( receiveWarehouseTransfer ). toHaveBeenCalledTimes ( 1 )
2026-10-22 23:10:09 +08:00
await button ( wrapper , '确认本来源实收' ). trigger ( 'click' ); await flushPromises ()
expect ( vi . mocked ( receiveWarehouseTransfer ). mock . calls [ 1 ] ! [ 2 ]. idempotencyKey ). not . toBe ( original [ 2 ]. idempotencyKey )
2026-10-05 06:16:21 +08:00
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 ( '登记目标仓实收' )
})
})