2026-10-08 17:08:03 +08:00
const { submitExport } = vi . hoisted (() => ({ submitExport : vi.fn (). mockResolvedValue ( undefined ) }))
vi . mock ( '@/modules/downloads' , () => ({ useExportTask : () => submitExport }))
2026-10-04 16:57:16 +08:00
import ElementPlus , { ElMessageBox , ElMessage } from 'element-plus'
2026-09-07 20:25:14 +08:00
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
2026-08-27 15:56:13 +08:00
import { createPinia } from 'pinia'
import { enableAutoUnmount , flushPromises , mount } from '@vue/test-utils'
import { afterAll , afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
2026-08-27 17:16:38 +08:00
import { ApiResponseError } from '@/shared/api/http'
2026-08-27 17:49:05 +08:00
import DataTable from '@/shared/components/DataTable.vue'
2026-09-07 20:25:14 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-09-07 11:01:00 +08:00
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
2026-10-28 16:56:32 +08:00
import LayoutPreferenceList from '@/shared/components/LayoutPreferenceList.vue'
import { preferenceStorageKey } from '@/shared/utils/uiLayoutPreference'
2026-08-28 13:54:44 +08:00
import { listAllPlatformWarehouses } from '@/modules/warehouses'
2026-10-22 14:08:06 +08:00
import { listContainerArrivalTimes } from '@/modules/container-loading'
2026-08-27 17:49:05 +08:00
import {
2026-09-07 15:07:39 +08:00
changeContainerReceivingWarehouse ,
2026-09-07 23:58:18 +08:00
createManualInboundPlan ,
2026-11-02 17:15:37 +08:00
createExceptionReturnInboundPlan ,
2026-08-27 17:49:05 +08:00
convertLastMileContainerToInboundPlans ,
2026-09-09 00:44:07 +08:00
getContainerInboundPlanPreview ,
2026-08-28 13:54:44 +08:00
getContainerPlanProgress ,
listContainerPlanProgress ,
2026-08-27 17:49:05 +08:00
listLastMileOrders ,
2026-10-20 14:00:49 +08:00
getReturnOrderStatistics ,
2026-10-04 16:57:16 +08:00
reviewCargoDiscard ,
2026-08-28 13:54:44 +08:00
transferLastMileOrders ,
2026-10-10 14:58:15 +08:00
updateLastMileOrderPalletMerge ,
2026-09-07 23:58:18 +08:00
getLastMileOrderManualNodes ,
updateLastMileOrderHold ,
updateLastMileOrderManualNodes ,
2026-08-27 17:49:05 +08:00
} from '../api/lastMileOrders'
2026-08-27 15:56:13 +08:00
import LastMileOrdersView from './LastMileOrdersView.vue'
2026-09-08 11:45:30 +08:00
const canAccess = vi . hoisted (() => vi . fn < ( permissionCode : string ) => boolean > (() => true ))
vi . mock ( '@/core/access/accessPreview' , () => ({ useAccessPreviewStore : () => ({ can : canAccess }) }))
2026-10-08 15:12:49 +08:00
vi . mock ( '@/shared/downloadCsv' , () => ({ saveCsv : vi.fn () }))
2026-10-09 18:03:34 +08:00
const routerPush = vi . hoisted (() => vi . fn ())
vi . mock ( 'vue-router' , () => ({ useRouter : () => ({ push : routerPush }) }))
2026-08-28 13:54:44 +08:00
vi . mock ( '@/modules/warehouses' , () => ({ listAllPlatformWarehouses : vi.fn () }))
2026-10-22 14:08:06 +08:00
vi . mock ( '@/modules/container-loading' , async () => ({
... await import ( '../../container-loading/warehouseArrival' ),
listContainerArrivalTimes : vi.fn (),
}))
2026-08-27 15:56:13 +08:00
vi . mock ( '../api/lastMileOrders' , () => ({
2026-10-20 14:00:49 +08:00
getReturnOrderStatistics : vi.fn (),
2026-08-28 13:54:44 +08:00
listLastMileOrders : vi.fn (), listContainerPlanProgress : vi.fn (), getContainerPlanProgress : vi.fn (),
2026-09-07 15:07:39 +08:00
changeContainerReceivingWarehouse : vi.fn (),
2026-11-02 17:15:37 +08:00
createExceptionReturnInboundPlan : vi.fn (), createManualInboundPlan : vi.fn (), convertLastMileContainerToInboundPlans : vi.fn (), getContainerInboundPlanPreview : vi.fn (), transferLastMileOrders : vi.fn (),
2026-09-07 23:58:18 +08:00
getLastMileOrderManualNodes : vi.fn (), updateLastMileOrderHold : vi.fn (), updateLastMileOrderManualNodes : vi.fn (),
2026-10-10 14:58:15 +08:00
reviewCargoDiscard : vi.fn (), updateLastMileOrderPalletMerge : vi.fn (),
2026-08-27 15:56:13 +08:00
}))
class ResizeObserverStub implements ResizeObserver { observe() { } unobserve() { } disconnect() { } }
function memoryStorage () : Storage {
const values = new Map < string , string >()
return {
2026-08-27 16:42:32 +08:00
get length() { return values . size }, clear : () => values . clear (), getItem : ( key ) => values . get ( key ) ?? null ,
key : ( index ) => [... values . keys ()][ index ] ?? null , removeItem : ( key ) => values . delete ( key ), setItem : ( key , value ) => values . set ( key , value ),
2026-08-27 15:56:13 +08:00
}
}
2026-08-27 17:49:05 +08:00
const bookingId = '2092873463568597001'
2026-10-22 14:08:06 +08:00
const arrivalTime = {
sourceContainerBookingId : bookingId , estimatedWarehouseArrivalAt : '2026-09-01T08:00:00' ,
actualWarehouseArrivalAt : null , version : 1 , planCount : 0 , plannedDate : '2026-09-01' , isDefault : false ,
}
2026-10-20 14:00:49 +08:00
const emptyReturnStatistics = { totals : { orderCount : 0 , skuTypes : 0 , expectedQuantity : 0 , receivedQuantity : 0 , remainingQuantity : 0 , receivedBoxCount : 0 , receivedParcelCount : 0 , receivedPalletCount : 0 }, pageDimensions : [] }
beforeEach (() => {
vi . mocked ( getReturnOrderStatistics ). mockReset (). mockResolvedValue ( emptyReturnStatistics )
})
2026-08-27 17:49:05 +08:00
const customerId = '2092618717861048321'
2026-08-27 17:16:38 +08:00
const summary = {
2026-08-28 13:54:44 +08:00
sourceContainerBookingId : bookingId , containerNo : 'CONT-15' , totalOrderCount : 15 ,
2026-09-07 15:07:39 +08:00
overseasWarehouseId : '9007199254740995' , overseasWarehouseCode : 'LAX01' , overseasWarehouseName : '洛杉矶接柜仓' ,
assignmentVersion : 7 , canChangeReceivingWarehouse : true , receivingWarehouseLockReason : null ,
2026-08-28 13:54:44 +08:00
inboundCodeCount : 15 , missingInboundCodeCount : 0 , planCount : 10 , plannedInboundCodeCount : 10 ,
2026-09-10 16:33:43 +08:00
pendingPlanCount : 5 , resourcePendingPlanCount : 0 , plannedArrivalAt : '2026-09-01T08:00:00' , allPlansReady : false ,
2026-08-28 13:54:44 +08:00
blockingReason : '存在5票尚未生成有效入库计划' ,
}
const fbaWarehouse = {
id : '9007199254740993' , tenantId : '0' , warehouseCode : 'ONT8' , warehouseName : 'Ontario FBA' ,
warehouseType : 'FBA' , countryCode : 'US' , timezone : 'America/Los_Angeles' , contactName : null ,
contactPhone : null , contactEmail : null , status : 'ACTIVE' , remark : null ,
primaryAddress : {
id : '9007199254740994' , tenantId : '0' , warehouseId : '9007199254740993' , addressType : 'OPERATING' ,
countryCode : 'US' , stateProvince : 'CA' , city : 'Ontario' , district : null , postalCode : '91761' ,
addressLine1 : 'FBA address' , addressLine2 : null , latitude : null , longitude : null ,
},
addressSummary : 'CA Ontario' ,
2026-08-27 17:16:38 +08:00
}
2026-09-09 00:44:07 +08:00
const preview = {
sourceContainerBookingId : bookingId , containerNo : 'CONT-15' ,
overseasWarehouseId : summary.overseasWarehouseId , overseasWarehouseCode : summary.overseasWarehouseCode ,
overseasWarehouseName : summary.overseasWarehouseName , snapshotVersion : 9 , snapshotHash : 'a' . repeat ( 64 ),
totalOrderCount : 5 , readyOrderCount : 5 , blockedOrderCount : 0 , pendingOrderCount : 5 ,
2026-09-11 15:39:47 +08:00
pendingPalletCount : 5 , sourcePalletCount : 4 , supplementPalletCount : 1 ,
existingPlannedPalletCount : 0 , totalSuggestedPalletCount : 5 ,
2026-09-09 00:44:07 +08:00
totalPlannedPalletCount : 0 , ready : true ,
groups : [{
destinationType : 'FBA' , destinationWarehouseId : null , destinationWarehouseCode : 'ONT8' ,
destinationWarehouseName : 'Ontario FBA' , orderCount : 5 , pendingOrderCount : 5 , pendingPalletCount : 5 ,
2026-09-11 15:39:47 +08:00
sourcePalletCount : 4 , supplementPalletCount : 1 ,
2026-09-09 00:44:07 +08:00
existingPlannedPalletCount : 0 , suggestedPalletCount : 5 , plannedPalletCount : 0 ,
orders : [{
customerId , customerName : '测试客户' , orderId : '2092873463568596994' , businessOrderNo : 'LMO-1' ,
inboundCode : 'IN-1' , orderType : 'FIRST_LEG' , destinationType : 'FBA' , destinationWarehouseId : null ,
destinationWarehouseCode : 'ONT8' , destinationWarehouseName : 'Ontario FBA' , packageCount : 10 ,
2026-09-11 15:39:47 +08:00
volumeCbm : 1 , suggestedPalletCount : 5 , sourcePalletCount : 4 , targetPalletCount : 5 ,
supplementPalletCount : 1 , plannedPalletCount : null , existingPlanNo : null ,
2026-09-09 00:44:07 +08:00
planStatus : null , orderVersion : 3 , readiness : 'READY' , blockers : [],
}],
}],
}
2026-10-05 05:40:21 +08:00
// Recorded LOCAL_COMMITTED shape: complete local plan/pallet, zero new work, PLANNED,
// and an explicit server resourceRetry holding the original command identity.
const resourceCommittedPreview = {
... preview , snapshotVersion : 10 , snapshotHash : 'b' . repeat ( 64 ),
totalOrderCount : 1 , readyOrderCount : 1 , pendingOrderCount : 0 , pendingPalletCount : 0 ,
sourcePalletCount : 0 , supplementPalletCount : 0 , existingPlannedPalletCount : 1 ,
totalSuggestedPalletCount : 1 , totalPlannedPalletCount : 1 ,
groups : [{
... preview . groups [ 0 ], destinationType : 'FBA' as const ,
orderCount : 1 , pendingOrderCount : 0 , pendingPalletCount : 0 , sourcePalletCount : 0 ,
supplementPalletCount : 0 , existingPlannedPalletCount : 1 , suggestedPalletCount : 1 , plannedPalletCount : 1 ,
orders : [{ ... preview . groups [ 0 ]. orders [ 0 ], destinationType : 'FBA' as const , fbaWarehouseCode : 'ONT8' ,
packageCount : 1 , suggestedPalletCount : 1 , sourcePalletCount : 1 , targetPalletCount : 1 ,
supplementPalletCount : 0 , plannedPalletCount : 1 , existingPlanNo : 'LIP-ORIGINAL' ,
planStatus : 'PENDING_ASSIGNMENT' , readiness : 'PLANNED' as const , blockers : [],
}],
}],
resourceRetry : { idempotencyKey : 'original-container-command' , snapshotVersion : 9 , snapshotHash : 'a' . repeat ( 64 ) },
}
const originalResourcePlans = [{ planNo : 'LIP-ORIGINAL' , pallets : [{ id : 'PALLET-ORIGINAL' , palletNo : 'PLT-ORIGINAL' }] }]
2026-09-07 15:07:39 +08:00
const receivingWarehouse = {
... fbaWarehouse ,
id : '9007199254740996' , warehouseCode : 'LAX02' , warehouseName : '洛杉矶二号接柜仓' , warehouseType : 'OVERSEAS' ,
}
2026-09-07 20:25:14 +08:00
const currentReceivingWarehouse = {
... receivingWarehouse ,
id : summary.overseasWarehouseId , warehouseCode : summary.overseasWarehouseCode , warehouseName : summary.overseasWarehouseName ,
}
2026-08-27 17:49:05 +08:00
function selectedRow ( overrides : Record < string , unknown > = {}) {
2026-09-08 10:54:36 +08:00
return {
id : '2092873463568596994' , customerId , sourceContainerBookingId : bookingId , orderNo : 'LMO-260827-0001' ,
overseasWarehouseId : summary.overseasWarehouseId , overseasWarehouseCode : summary.overseasWarehouseCode ,
warehouse : summary.overseasWarehouseName , inboundCode : 'IN-LOCKED' , version : 3 , pallets : 2 ,
2026-09-15 12:35:46 +08:00
estimatedWarehouseAt : '2026-09-01T08:00:00' , warehouseEta : '2026-09-01 08:00' ,
2026-09-08 10:54:36 +08:00
... overrides ,
}
2026-08-27 17:16:38 +08:00
}
2026-08-27 15:56:13 +08:00
enableAutoUnmount ( afterEach )
afterAll (() => vi . unstubAllGlobals ())
2026-08-27 17:49:05 +08:00
async function mountView() {
2026-08-27 17:16:38 +08:00
const wrapper = mount ( LastMileOrdersView , { attachTo : document.body , global : { plugins : [ createPinia (), ElementPlus ] } })
await flushPromises ()
2026-08-27 17:49:05 +08:00
return wrapper
}
2026-10-26 15:51:07 +08:00
async function openAfterDeparture() {
2026-08-27 17:49:05 +08:00
const wrapper = await mountView ()
2026-10-26 15:51:07 +08:00
const departedTab = wrapper . findAll ( '[role="tab"]' ). find (( item ) => item . text () === '开船后' )
await departedTab ! . trigger ( 'click' )
2026-08-27 17:16:38 +08:00
await flushPromises ()
return wrapper
}
2026-09-10 15:40:55 +08:00
async function openHiddenContainerWorkbench ( wrapper : Awaited < ReturnType < typeof mountView >>) {
const view = wrapper . vm as unknown as {
2026-10-08 15:12:49 +08:00
statusTab : '全部' | '开船前' | '开船后' | '柜子'
2026-09-10 15:40:55 +08:00
changeStatusTab () : void
}
view . statusTab = '柜子'
view . changeStatusTab ()
await flushPromises ()
}
2026-08-27 17:49:05 +08:00
async function selectRows ( wrapper : Awaited < ReturnType < typeof mountView >>, rows : object []) {
wrapper . findComponent ( DataTable ). vm . $emit ( 'selection-change' , rows )
await wrapper . vm . $nextTick ()
}
2026-10-22 14:08:06 +08:00
/** 定向验证入库弹框,独立于列表页签和批量操作入口。 */
async function openArrivalPlan ( wrapper : Awaited < ReturnType < typeof mountView >>, source = selectedRow ()) {
;( wrapper . vm as unknown as { openInboundPlan ( source : object ) : void }). openInboundPlan ( source )
await flushPromises ()
}
2026-08-27 17:49:05 +08:00
function button ( wrapper : Awaited < ReturnType < typeof mountView >>, text : string ) {
return wrapper . findAll ( 'button' ). find (( item ) => item . text (). includes ( text ))
}
2026-10-05 05:40:21 +08:00
function cabinetDialog ( wrapper : Awaited < ReturnType < typeof mountView >>) {
2026-10-20 10:11:31 +08:00
return wrapper . findAllComponents ( AppDialog ). find ( dialog => dialog . props ( 'title' ). startsWith ( '按柜生成入库计划预览' )) !
2026-10-05 05:40:21 +08:00
}
2026-08-27 17:49:05 +08:00
async function openContainerDialog ( wrapper : Awaited < ReturnType < typeof mountView >>) {
await button ( wrapper , '按柜批量生成' ) ! . trigger ( 'click' )
await flushPromises ()
}
async function chooseContainer ( wrapper : Awaited < ReturnType < typeof mountView >>) {
const selects = wrapper . findAllComponents ({ name : 'ElSelect' })
const containerSelect = selects [ selects . length - 1 ]
expect ( containerSelect ). toBeDefined ()
containerSelect . vm . $emit ( 'update:modelValue' , bookingId )
2026-09-09 00:44:07 +08:00
containerSelect . vm . $emit ( 'change' , bookingId )
await flushPromises ()
2026-08-27 17:49:05 +08:00
await wrapper . vm . $nextTick ()
}
describe ( 'LastMileOrdersView pending order and container conversion' , () => {
2026-10-26 13:43:08 +08:00
it ( 'opens the same inbound-code detail from all and before departure despite different order IDs' , async () => {
const wrapper = await mountView ()
const vm = wrapper . vm as unknown as { openDetail ( row : object ) : void }
const row = { ... selectedRow (), orderTypeCode : 'FIRST_LEG' , containerNode : '—' }
routerPush . mockClear ()
vm . openDetail ( row )
expect ( routerPush ). toHaveBeenLastCalledWith ({ name : 'unified-order-detail-by-inbound-code' , params : { inboundCode : row.inboundCode }, query : { tenantId : customerId , from : 'last-mile-orders' } })
await wrapper . findAll ( '[role="tab"]' ). find ( tab => tab . text () === '开船前' ) ! . trigger ( 'click' )
await flushPromises ()
vm . openDetail ({ ... row , id : '9007199254740999' , orderNo : '—' })
expect ( routerPush ). toHaveBeenLastCalledWith ({ name : 'unified-order-detail-by-inbound-code' , params : { inboundCode : row.inboundCode }, query : { tenantId : customerId , from : 'last-mile-orders' } })
vm . openDetail ({ ... row , containerNode : 'ACTUAL_DEPARTURE' })
expect ( routerPush ). toHaveBeenLastCalledWith ({ name : 'overseas-last-mile-order-detail' , params : { orderId : row.id } })
})
it ( 'only uses the domestic ID fallback for source rows without an inbound code' , async () => {
const wrapper = await mountView ()
const vm = wrapper . vm as unknown as { openDetail ( row : object ) : void }
const row = { ... selectedRow (), orderTypeCode : 'FIRST_LEG' , containerNode : '—' , inboundCode : '—' , orderNo : '—' }
routerPush . mockClear ()
vm . openDetail ( row )
expect ( routerPush ). toHaveBeenCalledWith ({ name : 'unified-order-detail' , params : { orderId : row.id }, query : { tenantId : customerId , from : 'last-mile-orders' } })
routerPush . mockClear ()
vm . openDetail ({ ... row , orderNo : 'WC-1' })
expect ( routerPush ). not . toHaveBeenCalled ()
})
2026-10-20 23:34:11 +08:00
it ( 'keeps return row statistics across pages without rendering the long summary blocks' , async () => {
2026-10-20 14:00:49 +08:00
const totals = { orderCount : 3 , skuTypes : 3 , expectedQuantity : 19 , receivedQuantity : 14 , remainingQuantity : 5 , receivedBoxCount : 3 , receivedParcelCount : 2 , receivedPalletCount : 1 }
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [], page : 1 , size : 10 , total : 30 , pages : 3 })
vi . mocked ( getReturnOrderStatistics ). mockResolvedValue ({ totals , pageDimensions : [] })
const wrapper = await mountView ()
const summary = () => wrapper . find ( '[aria-label="退货全筛选汇总"]' )
2026-10-20 23:34:11 +08:00
expect ( summary (). exists ()). toBe ( false )
expect ( wrapper . find ( '[aria-label="仓储转运全筛选汇总"]' ). exists ()). toBe ( false )
expect ( wrapper . text ()). not . toContain ( '当前筛选中的退货' )
2026-10-20 14:00:49 +08:00
const table = wrapper . findComponent ( DataTable )
table . vm . $emit ( 'update:page' , 2 )
await flushPromises ()
expect ( getReturnOrderStatistics ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 2 }))
2026-10-20 23:34:11 +08:00
expect ( summary (). exists ()). toBe ( false )
2026-10-20 14:00:49 +08:00
vi . mocked ( getReturnOrderStatistics ). mockRejectedValueOnce ( new Error ( '统计暂不可用' ))
table . vm . $emit ( 'update:page' , 3 )
await flushPromises ()
2026-10-20 17:24:50 +08:00
expect ( wrapper . find ( '[aria-label="退货全筛选汇总"]' ). exists ()). toBe ( false )
2026-10-20 14:00:49 +08:00
expect ( wrapper . text ()). toContain ( '尾程订单加载失败' )
expect ( table . props ( 'data' )). toEqual ([])
})
2026-10-09 16:11:59 +08:00
it ( 'shows commercial source alongside fulfillment type and sends source filters to the server' , async () => {
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{ id : '1' , customerId : 'A' , orderNo : 'WT-1' , orderType : 'WAREHOUSE_TRANSFER' , businessSource : { businessType : 'LOCAL_LOOSEBOARD' , label : '本土散板 · 拼车需求池' , businessOrderNo : 'LB-1' , externalBookingNo : 'FB-1' } }], page : 1 , size : 10 , total : 1 , pages : 1 })
const wrapper = await mountView ()
const table = wrapper . findComponent ( DataTable )
expect ( table . props ( 'data' )[ 0 ]). toMatchObject ({ businessSource : '本土散板 · 拼车需求池' , businessOrderNo : 'LB-1' , externalBookingNo : 'FB-1' , orderType : '仓储转运' , orderTypeCode : 'WAREHOUSE_TRANSFER' })
const toolbar = wrapper . findComponent ( QueryToolbar )
toolbar . vm . $emit ( 'search' , { ... toolbar . props ( 'modelValue' ), businessType : 'LOCAL_LOOSEBOARD' , businessOrderNo : ' LB-1 ' , externalBookingNo : ' FB-1 ' })
await flushPromises ()
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ businessType : 'LOCAL_LOOSEBOARD' , businessOrderNo : 'LB-1' , externalBookingNo : 'FB-1' }))
toolbar . vm . $emit ( 'reset' ); await flushPromises ()
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ businessType : undefined , businessOrderNo : undefined , externalBookingNo : undefined }))
})
2026-10-03 04:35:55 +08:00
it ( 'supports keyboard status switching and keeps selection and queries in sync' , async () => {
const wrapper = await mountView ()
2026-10-20 23:34:11 +08:00
const tabs = wrapper . get ( '.data-table__toolbar-start .workbench-list-tabs' ). findAll ( '[role="tab"]' )
2026-10-08 15:12:49 +08:00
expect ( tabs . map (( tab ) => tab . text ())). toEqual ([ '全部' , '开船前' , '开船后' ])
2026-10-03 04:35:55 +08:00
expect ( tabs [ 0 ] ! . attributes ( 'aria-selected' )). toBe ( 'true' )
expect ( tabs [ 1 ] ! . attributes ( 'tabindex' )). toBe ( '-1' )
await selectRows ( wrapper , [ selectedRow ()])
await tabs [ 0 ] ! . trigger ( 'keydown' , { key : 'ArrowRight' })
await flushPromises ()
expect ( tabs [ 1 ] ! . attributes ( 'aria-selected' )). toBe ( 'true' )
expect ( tabs [ 1 ] ! . classes ()). toContain ( 'is-active' )
expect ( document . activeElement ). toBe ( tabs [ 1 ] ! . element )
2026-10-08 15:12:49 +08:00
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 1 , orderType : 'FIRST_LEG' , sailingStage : 'BEFORE_DEPARTURE' , currentStatus : undefined }))
2026-10-03 04:35:55 +08:00
expect (( wrapper . vm as unknown as { selectedRows : unknown [] }). selectedRows ). toEqual ([])
const callCount = vi . mocked ( listLastMileOrders ). mock . calls . length
await tabs [ 1 ] ! . trigger ( 'click' )
await flushPromises ()
expect ( vi . mocked ( listLastMileOrders )). toHaveBeenCalledTimes ( callCount )
await tabs [ 1 ] ! . trigger ( 'keydown' , { key : 'Home' })
await flushPromises ()
expect ( tabs [ 0 ] ! . attributes ( 'aria-selected' )). toBe ( 'true' )
2026-10-08 15:12:49 +08:00
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ currentStatus : undefined , sailingStage : undefined , orderType : undefined }))
2026-10-03 04:35:55 +08:00
await tabs [ 0 ] ! . trigger ( 'keydown' , { key : 'End' })
await flushPromises ()
2026-10-08 15:12:49 +08:00
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ sailingStage : 'AFTER_DEPARTURE' , orderType : 'FIRST_LEG' }))
expect ( tabs [ 2 ] ! . attributes ( 'aria-selected' )). toBe ( 'true' )
})
it ( 'exports the same stage and filters as the list and restores other types in all' , async () => {
const wrapper = await mountView ()
const toolbar = wrapper . findComponent ( QueryToolbar )
toolbar . vm . $emit ( 'update:modelValue' , { ... toolbar . props ( 'modelValue' ), orderType : 'RETURN_RELABEL' , detained : 'false' })
await wrapper . vm . $nextTick ()
for ( const [ label , stage ] of [[ '开船前' , 'BEFORE_DEPARTURE' ], [ '开船后' , 'AFTER_DEPARTURE' ]] as const ) {
await wrapper . findAll ( '[role="tab"]' ). find (( tab ) => tab . text () === label ) ! . trigger ( 'click' )
await flushPromises ()
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ orderType : 'FIRST_LEG' , sailingStage : stage , detained : false }))
expect ( toolbar . props ( 'availableFields' ) ? . some (( field ) => field . key === 'orderType' )). toBe ( false )
2026-10-26 15:51:07 +08:00
if ( stage === 'BEFORE_DEPARTURE' ) {
expect ( button ( wrapper , '导出订单' )). toBeUndefined ()
} else {
await button ( wrapper , '导出订单' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( submitExport ). toHaveBeenLastCalledWith ( 'LAST_MILE_ORDER_LIST' , '尾程订单' , vi . mocked ( listLastMileOrders ). mock . lastCall ! [ 0 ])
}
2026-10-08 15:12:49 +08:00
}
await wrapper . findAll ( '[role="tab"]' ). find (( tab ) => tab . text () === '全部' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ orderType : 'RETURN_RELABEL' , sailingStage : undefined }))
expect ( toolbar . props ( 'availableFields' ) ? . some (( field ) => field . key === 'orderType' )). toBe ( true )
2026-10-03 04:35:55 +08:00
})
2026-10-28 16:56:32 +08:00
it ( 'uses the saved first three filters when collapsed and keeps reordered fields after remounting' , async () => {
localStorage . setItem ( preferenceStorageKey ( 'query' , 'anonymous' , 'overseas-last-mile-orders-query' ), JSON . stringify ({
version : 1 , order : [ 'orderNo' , 'containerNo' , 'inboundCode' ], hidden : [],
}))
const wrapper = await mountView ()
const toolbar = wrapper . findComponent ( QueryToolbar )
const placeholders = () => toolbar . findAll ( '.query-toolbar__field' ). map ( field => field . get ( 'input' ). attributes ( 'placeholder' ))
expect ( placeholders ()). toEqual ([ '请输入尾程单号' , '请输入集装箱号' , '请输入入仓码' ])
const initialCalls = vi . mocked ( listLastMileOrders ). mock . calls . length
toolbar . vm . $emit ( 'update:modelValue' , { ... toolbar . props ( 'modelValue' ), containerNo : 'CONT-15' })
await wrapper . vm . $nextTick ()
await button ( wrapper , '高级筛选' ) ! . trigger ( 'click' )
expect ( placeholders ()). toHaveLength ( 16 )
expect ( placeholders (). slice ( 0 , 3 )). toEqual ([ '请输入尾程单号' , '请输入集装箱号' , '请输入入仓码' ])
await toolbar . get ( 'button[aria-label="调整筛选布局"]' ). trigger ( 'click' )
await flushPromises ()
toolbar . findComponent ( LayoutPreferenceList ). vm . $emit ( 'move' , 'externalBookingNo' , 0 )
await wrapper . vm . $nextTick ()
await button ( wrapper , '收起筛选' ) ! . trigger ( 'click' )
expect ( placeholders ()). toEqual ([ '请输入外接预订号' , '请输入尾程单号' , '请输入集装箱号' ])
expect ( toolbar . props ( 'modelValue' ). containerNo ). toBe ( 'CONT-15' )
expect ( vi . mocked ( listLastMileOrders )). toHaveBeenCalledTimes ( initialCalls )
wrapper . unmount ()
const restored = await mountView ()
const restoredToolbar = restored . findComponent ( QueryToolbar )
const restoredPlaceholders = () => restoredToolbar . findAll ( '.query-toolbar__field' ). map ( field => field . get ( 'input' ). attributes ( 'placeholder' ))
expect ( restoredPlaceholders ()). toEqual ([ '请输入外接预订号' , '请输入尾程单号' , '请输入集装箱号' ])
await restoredToolbar . get ( 'button[aria-label="调整筛选布局"]' ). trigger ( 'click' )
await flushPromises ()
restoredToolbar . findComponent ( LayoutPreferenceList ). vm . $emit ( 'reset' )
await restored . vm . $nextTick ()
expect ( restoredPlaceholders ()). toEqual ([ '请输入尾程单号' , '请输入入仓码' , '请输入仓库名称或编码' ])
})
2026-10-03 04:35:55 +08:00
it ( 'preserves advanced conditions when collapsed and clears them on reset' , async () => {
const wrapper = await mountView ()
const toolbar = wrapper . findComponent ( QueryToolbar )
2026-10-28 16:56:32 +08:00
expect ( toolbar . findAll ( '.query-toolbar__field input' ). map ( input => input . attributes ( 'placeholder' )))
. toEqual ([ '请输入尾程单号' , '请输入入仓码' , '请输入仓库名称或编码' ])
2026-10-03 04:35:55 +08:00
const initialCalls = vi . mocked ( listLastMileOrders ). mock . calls . length
await button ( wrapper , '高级筛选' ) ! . trigger ( 'click' )
2026-10-09 16:11:59 +08:00
expect ( toolbar . props ( 'fields' )). toHaveLength ( 16 )
2026-10-03 04:35:55 +08:00
toolbar . vm . $emit ( 'update:modelValue' , {
... toolbar . props ( 'modelValue' ), containerNo : ' CONT-15 ' , abnormal : 'false' ,
createdAt : [ '2026-09-01' , '2026-09-30' ],
})
await wrapper . vm . $nextTick ()
await button ( wrapper , '收起筛选' ) ! . trigger ( 'click' )
expect ( vi . mocked ( listLastMileOrders )). toHaveBeenCalledTimes ( initialCalls )
expect ( toolbar . props ( 'modelValue' ). containerNo ). toBe ( ' CONT-15 ' )
await button ( wrapper , '查询' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({
containerNo : 'CONT-15' , abnormal : false ,
createdFrom : '2026-09-01T00:00:00' , createdTo : '2026-09-30T23:59:59' ,
}))
await button ( wrapper , '重置' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( vi . mocked ( listLastMileOrders )). toHaveBeenCalledTimes ( initialCalls + 2 )
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({
page : 1 , containerNo : undefined , abnormal : undefined , createdFrom : undefined , createdTo : undefined ,
}))
await button ( wrapper , '高级筛选' ) ! . trigger ( 'click' )
expect ( toolbar . props ( 'modelValue' ). containerNo ). toBe ( '' )
expect ( toolbar . props ( 'modelValue' ). createdAt ). toEqual ([])
})
it ( 'keeps container filters separate from the advanced order fields' , async () => {
const wrapper = await mountView ()
await button ( wrapper , '高级筛选' ) ! . trigger ( 'click' )
await openHiddenContainerWorkbench ( wrapper )
expect ( wrapper . findComponent ( QueryToolbar ). props ( 'fields' ). map (( field ) => field . key )). toEqual ([ 'containerNo' , 'createdAt' ])
expect ( button ( wrapper , '收起筛选' )). toBeUndefined ()
expect ( button ( wrapper , '高级筛选' )). toBeUndefined ()
})
2026-10-26 15:51:07 +08:00
it . each ([ null , 'ESTIMATED_DEPARTURE' ])( 'keeps a first-leg order without actual departure read-only: %s' , async containerNode => {
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{ id : 'before-1' , customerId , orderNo : 'LM-BEFORE' , currentStatus : 'PENDING_ASSIGNMENT' , orderType : 'FIRST_LEG' , containerNode }], page : 1 , size : 10 , total : 1 , pages : 1 })
const wrapper = await mountView ()
const actions = wrapper . get ( '.el-table__body-wrapper .row-actions' )
expect ( actions . findAll ( 'button' ). map ( button => button . text (). trim ())). toEqual ([ '查看' ])
await selectRows ( wrapper , [( wrapper . findComponent ( DataTable ). props ( 'data' ) as object [])[ 0 ]])
expect (( wrapper . vm as unknown as { selectedRows : object [] }). selectedRows ). toEqual ([ expect . objectContaining ({ orderTypeCode : 'FIRST_LEG' , containerNode : containerNode || '—' })])
expect ( wrapper . find ( '.data-table__business-actions' ). exists ()). toBe ( false )
expect ( createManualInboundPlan ). not . toHaveBeenCalled ()
expect ( transferLastMileOrders ). not . toHaveBeenCalled ()
})
2026-09-17 13:36:31 +08:00
it ( 'keeps the original carton count and adds only the signed difference in the list' , async () => {
canAccess . mockReturnValue ( false )
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{
id : 'transfer-1' , customerId , orderNo : 'WT-1' , orderType : 'WAREHOUSE_TRANSFER' , currentStatus : 'WAIT_CUSTOMER_CONFIRMATION' ,
packageCount : 10 , receivedPackageCount : 8 , packageDifference : - 2 ,
}], page : 1 , size : 10 , total : 1 , pages : 1 })
const wrapper = await mountView ()
const table = wrapper . findComponent ( DataTable )
const columns = table . props ( 'columns' ) as Array < { key : string ; label : string } >
expect ( columns ). toEqual ( expect . arrayContaining ([ expect . objectContaining ({ key : 'cartonDifference' , label : '差异(箱)' })]))
expect ( columns . some (( column ) => [ 'declaredCartons' , 'receivedCartons' ]. includes ( column . key ))). toBe ( false )
expect (( table . props ( 'data' ) as Array < { cartons : unknown } > )[ 0 ] ? . cartons ). toBe ( 10 )
expect ( wrapper . get ( '.package-difference--nonzero' ). text ()). toBe ( '-2' )
})
2026-09-14 20:21:02 +08:00
it . each ([
[[], [ '查看' ], false ],
2026-10-19 17:56:31 +08:00
[[ 'inbound-order:create' ], [ '查看' , '入库' ], false ],
[[ 'last-mile-order:edit' ], [ '查看' , '修改节点' ], false ],
[[ 'inbound-order:create' , 'last-mile-order:edit' ], [ '查看' , '入库' , '修改节点' ], false ],
2026-09-14 20:21:02 +08:00
])( 'only collapses row actions above three for %j' , async ( allowed , labels , overflow ) => {
canAccess . mockImplementation ( code => allowed . some ( action => code === 'platform:overseas-warehouse:' + action ))
2026-10-26 15:51:07 +08:00
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{ id : 'order-1' , customerId , orderNo : 'LM-1' , currentStatus : 'PENDING_ASSIGNMENT' , orderType : 'FIRST_LEG' , containerNode : 'ACTUAL_DEPARTURE' }], page : 1 , size : 10 , total : 1 , pages : 1 })
2026-09-14 20:21:02 +08:00
const wrapper = await mountView ()
const actions = wrapper . get ( '.el-table__body-wrapper .row-actions' )
expect ( actions . findAll ( 'button' ). map ( button => button . text (). trim ())). toEqual ( labels )
expect ( actions . find ( '.el-dropdown' ). exists ()). toBe ( overflow )
})
2026-08-27 15:56:13 +08:00
beforeEach (() => {
2026-10-04 16:57:16 +08:00
vi . restoreAllMocks ()
vi . mocked ( reviewCargoDiscard ). mockReset (). mockResolvedValue ({})
2026-09-08 11:45:30 +08:00
canAccess . mockReset (). mockReturnValue ( true )
2026-08-27 15:56:13 +08:00
vi . stubGlobal ( 'ResizeObserver' , ResizeObserverStub )
vi . stubGlobal ( 'localStorage' , memoryStorage ())
vi . mocked ( listLastMileOrders ). mockReset (). mockResolvedValue ({ records : [], page : 1 , size : 10 , total : 0 , pages : 0 })
2026-08-28 13:54:44 +08:00
vi . mocked ( listContainerPlanProgress ). mockReset (). mockResolvedValue ({ records : [ summary ], page : 1 , size : 50 , total : 1 , pages : 1 })
vi . mocked ( getContainerPlanProgress ). mockReset (). mockResolvedValue ({
summary ,
orders : [{ orderId : '2092873463568596994' , customerId , orderNo : 'LMO-1' , inboundCode : 'IN-1' , orderStatus : 'INBOUND_PLANNED' , planId : '2092873463568597010' , planNo : 'LIP-1' , planStatus : 'PENDING_RECEIVING' , planValid : true , plannedArrivalAt : '2026-09-01T08:00:00' }],
})
2026-11-02 17:15:37 +08:00
vi . mocked ( createExceptionReturnInboundPlan ). mockReset (). mockResolvedValue ({} as never )
2026-09-07 23:58:18 +08:00
vi . mocked ( createManualInboundPlan ). mockReset (). mockResolvedValue ({} as never )
2026-10-22 14:08:06 +08:00
vi . mocked ( listContainerArrivalTimes ). mockReset (). mockResolvedValue ([ arrivalTime ])
2026-09-09 00:44:07 +08:00
vi . mocked ( getContainerInboundPlanPreview ). mockReset (). mockResolvedValue ( preview as never )
2026-09-07 23:58:18 +08:00
vi . mocked ( getLastMileOrderManualNodes ). mockReset (). mockResolvedValue ({ orderId : '2092873463568596994' , manualOverrideEnabled : false , nodes : [], version : 0 })
vi . mocked ( updateLastMileOrderHold ). mockReset (). mockResolvedValue ({ orderId : '2092873463568596994' , held : true , chargeable : true , version : 1 })
vi . mocked ( updateLastMileOrderManualNodes ). mockReset (). mockResolvedValue ({ orderId : '2092873463568596994' , manualOverrideEnabled : true , nodes : [], version : 1 })
2026-09-09 00:44:07 +08:00
vi . mocked ( convertLastMileContainerToInboundPlans ). mockReset (). mockResolvedValue ([
{ planNo : 'LIP-1' , pallets : [{ id : 'PALLET-1' , palletNo : 'PLT-001' }] },
{ planNo : 'LIP-2' , pallets : [{ id : 'PALLET-2' , palletNo : 'PLT-002' }] },
] as never )
2026-09-07 15:07:39 +08:00
vi . mocked ( changeContainerReceivingWarehouse ). mockReset (). mockResolvedValue ({ ... summary , overseasWarehouseId : '9007199254740996' , overseasWarehouseCode : 'LAX02' , overseasWarehouseName : '洛杉矶二号接柜仓' , assignmentVersion : 8 })
2026-08-28 13:54:44 +08:00
vi . mocked ( listAllPlatformWarehouses ). mockReset (). mockResolvedValue ([ fbaWarehouse ] as never )
vi . mocked ( transferLastMileOrders ). mockReset (). mockResolvedValue ( 2 )
2026-08-27 15:56:13 +08:00
})
2026-10-04 16:57:16 +08:00
async function openDiscard ( overrides : Record < string , unknown > = {}) {
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{
id : '2092873463568596994' , customerId , orderNo : 'CD-TEST-1' , sourceOrderNo : 'CD-TEST-1' ,
orderType : 'CARGO_DISCARD' , currentStatus : 'PENDING_REVIEW' , inboundOrderNo : 'CD-TEST-1' , ... overrides ,
}], page : 1 , size : 10 , total : 1 , pages : 1 })
return mountView ()
}
it . each ([
[ 'lastleg:cargo-discard:approve' , 'PENDING_REVIEW' , true ],
[ 'platform:overseas-warehouse:last-mile-order:edit' , 'PENDING_REVIEW' , false ],
[ 'lastleg:cargo-discard:approve' , 'APPROVED' , false ],
[ 'lastleg:cargo-discard:approve' , 'CANCELLED' , false ],
])( 'gates discard review on exact permission %s and status %s' , async ( code , currentStatus , visible ) => {
canAccess . mockImplementation ( permissionCode => permissionCode === code )
const wrapper = await openDiscard ({ currentStatus })
expect ( Boolean ( button ( wrapper , '审核' ))). toBe ( visible )
expect ( reviewCargoDiscard ). not . toHaveBeenCalled ()
})
it . each ([{ orderNo : '' }, { customerId : '' }])( 'does not expose discard review without a business target %j' , async ( overrides ) => {
const wrapper = await openDiscard ( overrides )
expect ( button ( wrapper , '审核' )). toBeUndefined ()
expect ( reviewCargoDiscard ). not . toHaveBeenCalled ()
})
it ( 'approves the selected discard once and reloads the server status' , async () => {
const confirm = vi . spyOn ( ElMessageBox , 'confirm' ). mockResolvedValue ( 'confirm' as Awaited < ReturnType < typeof ElMessageBox.confirm >>)
const wrapper = await openDiscard ()
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{
id : '2092873463568596994' , customerId , orderNo : 'CD-TEST-1' ,
orderType : 'CARGO_DISCARD' , currentStatus : 'APPROVED' , inboundOrderNo : 'CD-TEST-1' ,
}], page : 1 , size : 10 , total : 1 , pages : 1 })
await button ( wrapper , '审核' ) ! . trigger ( 'click' )
await flushPromises ()
2026-10-20 10:11:31 +08:00
expect ( confirm ). toHaveBeenCalledWith ( expect . stringContaining ( 'CD-TEST-1' ), '废弃申请审核 · CD-TEST-1' , expect . objectContaining ({ distinguishCancelAndClose : true }))
2026-10-04 16:57:16 +08:00
expect ( reviewCargoDiscard ). toHaveBeenCalledExactlyOnceWith ( 'CD-TEST-1' , customerId , 'APPROVE' , undefined )
expect ( button ( wrapper , '审核' )). toBeUndefined ()
})
it ( 'rejects with a trimmed mandatory reason without approving first' , async () => {
vi . spyOn ( ElMessageBox , 'confirm' ). mockRejectedValue ( 'cancel' )
const prompt = vi . spyOn ( ElMessageBox , 'prompt' ). mockResolvedValue ({ value : ' 箱数不符 ' , action : 'confirm' } as Awaited < ReturnType < typeof ElMessageBox.prompt >>)
const wrapper = await openDiscard ()
await button ( wrapper , '审核' ) ! . trigger ( 'click' )
await flushPromises ()
const options = prompt . mock . calls [ 0 ] ? .[ 2 ]
expect ( options ? . inputValidator ? .( ' ' )). not . toBe ( true )
expect ( options ? . inputValidator ? .( '甲' . repeat ( 1001 ))). not . toBe ( true )
expect ( options ? . inputValidator ? .( '箱数不符' )). toBe ( true )
expect ( reviewCargoDiscard ). toHaveBeenCalledExactlyOnceWith ( 'CD-TEST-1' , customerId , 'REJECT' , '箱数不符' )
})
it ( 'closing review or cancelling the reason makes no request' , async () => {
vi . spyOn ( ElMessageBox , 'confirm' ). mockRejectedValueOnce ( 'close' ). mockRejectedValueOnce ( 'cancel' )
vi . spyOn ( ElMessageBox , 'prompt' ). mockRejectedValue ( 'cancel' )
const wrapper = await openDiscard ()
await button ( wrapper , '审核' ) ! . trigger ( 'click' )
await flushPromises ()
await button ( wrapper , '审核' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( reviewCargoDiscard ). not . toHaveBeenCalled ()
expect ( listLastMileOrders ). toHaveBeenCalledTimes ( 1 )
})
it ( 'locks repeat clicks while review is pending and allows retry after an API failure' , async () => {
vi . spyOn ( ElMessageBox , 'confirm' ). mockResolvedValue ( 'confirm' as Awaited < ReturnType < typeof ElMessageBox.confirm >>)
const errorToast = vi . spyOn ( ElMessage , 'error' )
let rejectRequest !: ( error : Error ) => void
vi . mocked ( reviewCargoDiscard ). mockImplementationOnce (() => new Promise (( _ , reject ) => { rejectRequest = reject }))
const wrapper = await openDiscard ()
await button ( wrapper , '审核' ) ! . trigger ( 'click' )
await flushPromises ()
expect (( button ( wrapper , '审核' ) ! . element as HTMLButtonElement ). disabled ). toBe ( true )
await button ( wrapper , '审核' ) ! . trigger ( 'click' )
expect ( reviewCargoDiscard ). toHaveBeenCalledTimes ( 1 )
rejectRequest ( new Error ( '审批请求失败' ))
await flushPromises ()
expect ( errorToast ). toHaveBeenCalled ()
expect (( button ( wrapper , '审核' ) ! . element as HTMLButtonElement ). disabled ). toBe ( false )
expect ( listLastMileOrders ). toHaveBeenCalledTimes ( 1 )
await button ( wrapper , '审核' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( reviewCargoDiscard ). toHaveBeenCalledTimes ( 2 )
})
2026-09-18 09:59:12 +08:00
it . each ([
2026-09-20 22:04:59 +08:00
[ 'PENDING_ASSIGNMENT' , false ],
[ 'INBOUND_COMPLETED' , true ],
])( 'only shows a new hold after inbound completion for %s' , async ( currentStatus , visible ) => {
2026-09-18 09:59:12 +08:00
canAccess . mockImplementation ( code => code === 'platform:overseas-warehouse:last-mile-order:edit' )
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{
2026-10-26 15:51:07 +08:00
id : 'order-1' , customerId , orderNo : 'LM-1' , currentStatus , orderType : 'FIRST_LEG' , containerNode : 'ACTUAL_DEPARTURE' , detained : false ,
2026-09-18 09:59:12 +08:00
}], page : 1 , size : 10 , total : 1 , pages : 1 })
const wrapper = await mountView ()
const holdButton = wrapper . get ( '.el-table__body-wrapper .row-actions' ). findAll ( 'button' )
2026-09-20 22:04:59 +08:00
. find ( item => item . text (). includes ( '暂扣订单' ))
expect ( Boolean ( holdButton )). toBe ( visible )
if ( holdButton ) expect (( holdButton . element as HTMLButtonElement ). disabled ). toBe ( false )
})
it ( 'keeps release hold available for an already detained order' , async () => {
canAccess . mockImplementation ( code => code === 'platform:overseas-warehouse:last-mile-order:edit' )
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{
2026-10-26 15:51:07 +08:00
id : 'order-held' , customerId , orderNo : 'LM-HELD' , currentStatus : 'READY_FOR_DISPATCH' , orderType : 'FIRST_LEG' , containerNode : 'ACTUAL_DEPARTURE' , detained : true ,
2026-09-20 22:04:59 +08:00
}], page : 1 , size : 10 , total : 1 , pages : 1 })
const wrapper = await mountView ()
const releaseButton = wrapper . get ( '.el-table__body-wrapper .row-actions' ). findAll ( 'button' )
. find ( item => item . text (). includes ( '解除暂扣' ))
expect ( releaseButton ). toBeTruthy ()
expect (( releaseButton ! . element as HTMLButtonElement ). disabled ). toBe ( false )
2026-09-18 09:59:12 +08:00
})
2026-09-20 21:52:12 +08:00
it ( 'removes generation and hold actions after an inbound plan has been submitted' , async () => {
canAccess . mockReturnValue ( true )
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{
2026-10-26 15:51:07 +08:00
id : 'order-planned' , customerId , orderNo : 'LM-PLANNED' , currentStatus : 'INBOUND_PLANNED' , orderType : 'FIRST_LEG' , containerNode : 'ACTUAL_DEPARTURE' , detained : false ,
2026-09-20 21:52:12 +08:00
}], page : 1 , size : 10 , total : 1 , pages : 1 })
const wrapper = await mountView ()
const actions = wrapper . get ( '.el-table__body-wrapper .row-actions' )
expect ( actions . text ()). toContain ( '查看' )
2026-10-19 17:56:31 +08:00
expect ( actions . text ()). not . toContain ( '入库' )
2026-09-20 21:52:12 +08:00
expect ( actions . text ()). not . toContain ( '暂扣订单' )
await selectRows ( wrapper , [( wrapper . findComponent ( DataTable ). props ( 'data' ) as object [])[ 0 ]])
2026-10-19 17:56:31 +08:00
expect (( button ( wrapper , '入库' ) ! . element as HTMLButtonElement ). disabled ). toBe ( true )
2026-09-20 21:52:12 +08:00
})
2026-10-26 15:51:07 +08:00
it ( 'keeps after-departure order actions in the existing table without a container view switch' , async () => {
const wrapper = await openAfterDeparture ()
2026-08-27 17:49:05 +08:00
expect ( wrapper . text ()). toContain ( '尾程单号' )
expect ( wrapper . text ()). toContain ( '海外仓' )
expect ( wrapper . text ()). toContain ( '转仓' )
2026-10-19 17:56:31 +08:00
expect ( wrapper . text ()). toContain ( '入库' )
2026-08-27 17:49:05 +08:00
expect ( wrapper . text ()). toContain ( '按柜批量生成' )
expect ( wrapper . text ()). not . toContain ( '按订单' )
expect ( wrapper . text ()). not . toContain ( '待分配整柜' )
2026-09-10 15:40:55 +08:00
expect ( wrapper . text ()). not . toContain ( '柜子工作台' )
2026-10-26 15:51:07 +08:00
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 1 , size : 10 , currentStatus : undefined , orderType : 'FIRST_LEG' , sailingStage : 'AFTER_DEPARTURE' }))
2026-08-28 13:54:44 +08:00
expect ( listContainerPlanProgress ). not . toHaveBeenCalled ()
2026-08-27 17:16:38 +08:00
})
2026-09-07 11:01:00 +08:00
it ( 'forwards complete searches and switches every status tab without stale filter failures' , async () => {
const wrapper = await mountView ()
const toolbar = wrapper . findComponent ( QueryToolbar )
const model = {
orderNo : 'LMO-SEARCH' , inboundCode : 'IN-SEARCH' , lxCode : 'LX-SEARCH' , orderType : 'FIRST_LEG' ,
warehouse : 'LAX' , sourceOrderNo : 'SOURCE-SEARCH' , containerNo : 'CONT-SEARCH' , transferWarehouseCode : 'ONT8' ,
status : 'DELIVERED' , abnormal : 'true' , detained : 'false' , createdAt : [ '2026-09-01' , '2026-09-07' ],
}
toolbar . vm . $emit ( 'search' , model )
await flushPromises ()
2026-09-08 13:08:54 +08:00
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ({
page : 1 , size : 10 , orderNo : 'LMO-SEARCH' , inboundCode : 'IN-SEARCH' , lxCode : 'LX-SEARCH' , orderType : 'FIRST_LEG' ,
2026-09-07 11:01:00 +08:00
warehouse : 'LAX' , sourceOrderNo : 'SOURCE-SEARCH' , containerNo : 'CONT-SEARCH' , transferWarehouseCode : 'ONT8' ,
currentStatus : 'DELIVERED' , abnormal : true , detained : false ,
createdFrom : '2026-09-01T00:00:00' , createdTo : '2026-09-07T23:59:59' ,
2026-09-08 13:08:54 +08:00
})
const availableFields = toolbar . props ( 'availableFields' ) as Array < { key : string ; label : string ; placeholder? : string } >
expect ( availableFields . find (( field ) => field . key === 'lxCode' )). toMatchObject ({
label : 'LX代码' , placeholder : '请输入LX代码' ,
})
2026-09-18 11:41:27 +08:00
expect ( availableFields . filter (( field ) => [ 'orderType' , 'status' , 'abnormal' , 'detained' ]. includes ( field . key )))
. toEqual ( expect . arrayContaining ([
2026-10-09 16:11:59 +08:00
expect . objectContaining ({ key : 'orderType' , placeholder : '请选择履约类型' }),
2026-09-18 11:41:27 +08:00
expect . objectContaining ({ key : 'status' , placeholder : '请选择当前状态' }),
expect . objectContaining ({ key : 'abnormal' , placeholder : '请选择是否异常' }),
expect . objectContaining ({ key : 'detained' , placeholder : '请选择是否暂扣' }),
]))
2026-09-08 13:08:54 +08:00
expect ( availableFields . map (( field ) => field . key )). not . toEqual ( expect . arrayContaining ([ 'planNo' , 'inboundOrderNo' ]))
2026-09-07 11:01:00 +08:00
2026-10-08 15:12:49 +08:00
await wrapper . findAll ( '[role="tab"]' ). find (( item ) => item . text () === '开船前' ) ! . trigger ( 'click' )
2026-09-07 11:01:00 +08:00
await flushPromises ()
2026-10-08 15:12:49 +08:00
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ currentStatus : 'DELIVERED' , sailingStage : 'BEFORE_DEPARTURE' , orderType : 'FIRST_LEG' , lxCode : 'LX-SEARCH' }))
2026-09-07 11:01:00 +08:00
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-08 13:08:54 +08:00
expect ( listContainerPlanProgress ). toHaveBeenLastCalledWith ({
page : 1 , size : 10 , containerNo : 'CONT-SEARCH' ,
createdFrom : '2026-09-01T00:00:00' , createdTo : '2026-09-07T23:59:59' ,
})
2026-09-07 11:01:00 +08:00
2026-10-03 04:35:55 +08:00
await wrapper . findAll ( '[role="tab"]' ). find (( item ) => item . text () === '全部' ) ! . trigger ( 'click' )
2026-09-07 11:01:00 +08:00
await flushPromises ()
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ currentStatus : 'DELIVERED' , lxCode : 'LX-SEARCH' }))
2026-09-07 11:15:37 +08:00
const callsBeforeReset = vi . mocked ( listLastMileOrders ). mock . calls . length
;( toolbar . vm as unknown as { reset () : void }). reset ()
await flushPromises ()
expect ( vi . mocked ( listLastMileOrders ). mock . calls ). toHaveLength ( callsBeforeReset + 1 )
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 1 }))
expect ( vi . mocked ( listLastMileOrders ). mock . calls . at ( - 1 ) ? .[ 0 ]. lxCode ). toBeUndefined ()
2026-09-07 11:01:00 +08:00
expect ( wrapper . text ()). not . toContain ( '尾程订单加载失败' )
})
2026-11-02 17:15:37 +08:00
it ( 'requires an explicit return ETA and plans the current tail order without a source container' , async () => {
const wrapper = await openAfterDeparture ()
await selectRows ( wrapper , [ selectedRow ({ orderNo : 'WC1' , sourceOrderNo : 'ZY1' , orderTypeCode : 'DELIVERY_RETURN' , currentStatusCode : 'PENDING_INBOUND' , sourceContainerBookingId : null , inboundCode : null , estimatedWarehouseAt : '' , warehouseEta : '—' })])
await button ( wrapper , '入库' ) ! . trigger ( 'click' )
await flushPromises ()
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '入库计划 ·' )) !
expect ( dialog . get ( '[aria-label="入库来源"]' ). text ()). toContain ( 'WC1' )
expect ( dialog . text ()). toContain ( 'ZY1' )
expect ( dialog . props ( 'confirmDisabled' )). toBe ( true )
dialog . vm . $emit ( 'confirm' )
await flushPromises ()
expect ( createExceptionReturnInboundPlan ). not . toHaveBeenCalled ()
dialog . findComponent ({ name : 'ElDatePicker' }). vm . $emit ( 'update:modelValue' , '2026-11-05T14:30:00' )
await flushPromises ()
expect ( dialog . props ( 'confirmDisabled' )). toBe ( false )
dialog . vm . $emit ( 'confirm' )
await flushPromises ()
expect ( createExceptionReturnInboundPlan ). toHaveBeenCalledWith ( expect . objectContaining ({ customerId , orderId : '2092873463568596994' , expectedOrderVersion : 3 }), '2026-11-05T14:30:00' , expect . any ( String ))
expect ( createManualInboundPlan ). not . toHaveBeenCalled ()
expect ( listContainerArrivalTimes ). not . toHaveBeenCalled ()
expect ( getContainerInboundPlanPreview ). not . toHaveBeenCalled ()
})
2026-09-07 23:58:18 +08:00
it ( 'opens the required single-order inbound-plan form without invoking the cabinet conversion' , async () => {
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-08-27 17:49:05 +08:00
await selectRows ( wrapper , [ selectedRow ()])
2026-10-19 17:56:31 +08:00
await button ( wrapper , '入库' ) ! . trigger ( 'click' )
2026-08-27 17:16:38 +08:00
await flushPromises ()
2026-09-07 23:58:18 +08:00
expect ( document . body . textContent ). toContain ( '计划入库仓' )
2026-09-15 12:35:46 +08:00
expect ( document . body . textContent ). toContain ( '预计到仓时间' )
2026-09-07 23:58:18 +08:00
expect ( document . body . textContent ). toContain ( '托盘类型' )
2026-10-20 10:11:31 +08:00
const sourceFacts = wrapper . get ( '[aria-label="入库来源"]' )
expect ( sourceFacts . find ( 'input' ). exists ()). toBe ( false )
expect ( sourceFacts . text ()). toContain ( 'IN-LOCKED' )
expect ( sourceFacts . text ()). toContain ( 'LAX01 · 洛杉矶接柜仓' )
expect ( sourceFacts . text ()). toContain ( '2026-09-01 08:00' )
expect ( wrapper . find ( 'button[aria-label="查看入库来源说明"]' ). exists ()). toBe ( true )
2026-09-08 10:54:36 +08:00
expect ( listAllPlatformWarehouses ). not . toHaveBeenCalled ()
2026-09-07 23:58:18 +08:00
expect ( createManualInboundPlan ). not . toHaveBeenCalled ()
2026-08-27 17:49:05 +08:00
expect ( convertLastMileContainerToInboundPlans ). not . toHaveBeenCalled ()
2026-08-28 13:54:44 +08:00
expect ( listContainerPlanProgress ). not . toHaveBeenCalled ()
2026-08-27 17:16:38 +08:00
})
2026-09-15 12:35:46 +08:00
it ( 'shows cabinet-frozen facts but never sends arrival time or per-order warehouse overrides' , async () => {
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-09-08 10:54:36 +08:00
await selectRows ( wrapper , [ selectedRow ()])
2026-10-19 17:56:31 +08:00
await button ( wrapper , '入库' ) ! . trigger ( 'click' )
2026-09-08 10:54:36 +08:00
await flushPromises ()
const confirm = [... document . body . querySelectorAll ( 'button' )]
. find (( item ) => item . textContent ? . includes ( '保存并生成入库单' )) as HTMLButtonElement
confirm . click ()
await flushPromises ()
expect ( createManualInboundPlan ). toHaveBeenCalledWith ( expect . objectContaining ({
customerId , orderId : '2092873463568596994' , expectedOrderVersion : 3 ,
2026-09-15 12:35:46 +08:00
palletCount : 5 , palletType : 'WOOD' ,
2026-09-08 10:54:36 +08:00
}), expect . stringContaining ( 'manual-inbound-plan-' ))
const payload = vi . mocked ( createManualInboundPlan ). mock . calls [ 0 ][ 0 ] as unknown as Record < string , unknown >
expect ( payload ). not . toHaveProperty ( 'inboundCode' )
2026-09-15 12:35:46 +08:00
expect ( payload ). not . toHaveProperty ( 'plannedArrivalDate' )
2026-09-08 10:54:36 +08:00
expect ( payload ). not . toHaveProperty ( 'plannedWarehouseId' )
expect ( payload ). not . toHaveProperty ( 'plannedWarehouseCode' )
expect ( payload ). not . toHaveProperty ( 'plannedWarehouseName' )
})
2026-09-20 21:52:12 +08:00
it ( 'blocks a held order from generating its inbound plan' , async () => {
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-09-18 09:59:12 +08:00
await selectRows ( wrapper , [ selectedRow ({
detained : true ,
orderTypeCode : 'ONE_CLICK_DROPSHIP' ,
currentStatusCode : 'PENDING_ASSIGNMENT' ,
})])
2026-10-19 17:56:31 +08:00
await button ( wrapper , '入库' ) ! . trigger ( 'click' )
2026-09-18 09:59:12 +08:00
await flushPromises ()
2026-09-20 21:52:12 +08:00
expect ( getContainerInboundPlanPreview ). not . toHaveBeenCalled ()
2026-09-18 09:59:12 +08:00
const confirm = [... document . body . querySelectorAll ( 'button' )]
. find (( item ) => item . textContent ? . includes ( '保存并生成入库单' )) as HTMLButtonElement
2026-09-20 21:52:12 +08:00
expect ( confirm ). toBeUndefined ()
2026-09-18 09:59:12 +08:00
})
2026-10-22 14:08:06 +08:00
it . each ([
[{ ... arrivalTime , estimatedWarehouseArrivalAt : '2026-10-25T09:30:00' , plannedDate : '2026-10-25' }, '2026-10-25 09:30' ],
2026-10-28 11:06:42 +08:00
[{ ... arrivalTime , estimatedWarehouseArrivalAt : null , version : 0 , plannedDate : '2026-11-01' , isDefault : true }, '2026-11-01' ],
2026-10-22 14:08:06 +08:00
[{ ... arrivalTime , estimatedWarehouseArrivalAt : null , version : 0 , planCount : 1 , plannedDate : '2026-11-02' }, '2026-11-02' ],
])( 'reads the latest cabinet arrival on opening and permits planning with %s' , async ( arrival , expectedText ) => {
vi . mocked ( listContainerArrivalTimes ). mockResolvedValue ([ arrival ])
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-10-22 14:08:06 +08:00
await openArrivalPlan ( wrapper , arrival . estimatedWarehouseArrivalAt ? selectedRow () : selectedRow ({ estimatedWarehouseAt : '' , warehouseEta : '—' }))
expect ( listContainerArrivalTimes ). toHaveBeenCalledWith ([ bookingId ])
const source = wrapper . get ( '[aria-label="入库来源"]' )
expect ( source . text ()). toContain ( expectedText )
2026-10-28 11:06:42 +08:00
expect ( source . get ( '.inbound-plan-arrival' ). text ()). not . toContain ( '默认推算' )
2026-10-22 14:08:06 +08:00
if ( ! arrival . estimatedWarehouseArrivalAt ) expect ( source . text ()). not . toContain ( '00:00' )
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '入库计划 ·' )) !
expect ( dialog . props ( 'confirmDisabled' )). toBe ( false )
dialog . vm . $emit ( 'confirm' )
2026-09-15 12:35:46 +08:00
await flushPromises ()
2026-10-22 14:08:06 +08:00
expect ( createManualInboundPlan ). toHaveBeenCalledTimes ( 1 )
expect ( vi . mocked ( createManualInboundPlan ). mock . calls [ 0 ][ 0 ]). not . toHaveProperty ( 'plannedArrivalDate' )
})
it . each ([
{ arrivals : [] },
{ arrivals : [{ ... arrivalTime , estimatedWarehouseArrivalAt : null , plannedDate : null }] },
{ arrivals : [{ ... arrivalTime , version : - 1 }] },
])( 'blocks manual planning when the authoritative cabinet arrival is missing or inconsistent: $arrivals' , async ({ arrivals }) => {
vi . mocked ( listContainerArrivalTimes ). mockResolvedValue ( arrivals )
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-10-22 14:08:06 +08:00
await openArrivalPlan ( wrapper )
2026-09-15 12:35:46 +08:00
const confirm = [... document . body . querySelectorAll ( 'button' )]
. find (( item ) => item . textContent ? . includes ( '保存并生成入库单' )) as HTMLButtonElement
expect ( confirm . disabled ). toBe ( true )
expect ( createManualInboundPlan ). not . toHaveBeenCalled ()
})
2026-10-22 14:08:06 +08:00
it ( 'shows arrival read failures and does not reuse the list timestamp' , async () => {
vi . mocked ( listContainerArrivalTimes ). mockRejectedValue ( new ApiResponseError ( '到仓来源暂不可读' , 'A0503' , 503 ))
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-10-22 14:08:06 +08:00
await openArrivalPlan ( wrapper )
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '入库计划 ·' )) !
expect ( dialog . text ()). toContain ( '到仓来源暂不可读' )
expect ( dialog . get ( '[aria-label="入库来源"]' ). text ()). not . toContain ( '2026-09-01' )
expect ( dialog . props ( 'confirmDisabled' )). toBe ( true )
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( createManualInboundPlan ). not . toHaveBeenCalled ()
})
it ( 'keeps confirmation blocked during arrival loading and ignores an older opening of the same order' , async () => {
let resolveOld !: ( value : typeof arrivalTime []) => void
vi . mocked ( listContainerArrivalTimes ). mockReturnValueOnce ( new Promise ( resolve => { resolveOld = resolve }))
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-10-22 14:08:06 +08:00
await openArrivalPlan ( wrapper )
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '入库计划 ·' )) !
expect ( dialog . props ( 'confirmDisabled' )). toBe ( true )
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( createManualInboundPlan ). not . toHaveBeenCalled ()
dialog . vm . $emit ( 'update:modelValue' , false )
await wrapper . vm . $nextTick ()
vi . mocked ( listContainerArrivalTimes ). mockResolvedValueOnce ([{ ... arrivalTime , estimatedWarehouseArrivalAt : '2026-10-26T10:00:00' , plannedDate : '2026-10-26' }])
await openArrivalPlan ( wrapper )
expect ( dialog . get ( '[aria-label="入库来源"]' ). text ()). toContain ( '2026-10-26 10:00' )
resolveOld ([ arrivalTime ]); await flushPromises ()
expect ( dialog . get ( '[aria-label="入库来源"]' ). text ()). toContain ( '2026-10-26 10:00' )
expect ( dialog . props ( 'confirmDisabled' )). toBe ( false )
})
2026-09-14 16:38:51 +08:00
it ( 'locks one-click dropship manual plans to the frozen source pallet count' , async () => {
vi . mocked ( getContainerInboundPlanPreview ). mockResolvedValue ({
... preview ,
sourcePalletCount : 1 ,
groups : [{
... preview . groups [ 0 ], destinationType : 'ONE_CLICK_DROPSHIP' , sourcePalletCount : 1 ,
orders : [{
... preview . groups [ 0 ]. orders [ 0 ], orderType : 'ONE_CLICK_DROPSHIP' ,
destinationType : 'ONE_CLICK_DROPSHIP' , sourcePalletCount : 1 ,
targetPalletCount : 1 , supplementPalletCount : 0 ,
}],
}],
} as never )
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-09-14 16:38:51 +08:00
await selectRows ( wrapper , [ selectedRow ({ orderTypeCode : 'ONE_CLICK_DROPSHIP' , pallets : 2 })])
2026-10-19 17:56:31 +08:00
await button ( wrapper , '入库' ) ! . trigger ( 'click' )
2026-09-14 16:38:51 +08:00
await flushPromises ()
2026-10-20 10:11:31 +08:00
expect ( wrapper . find ( '.command-form' ). findComponent ({ name : 'ElInputNumber' }). exists ()). toBe ( false )
expect ( wrapper . get ( '.frozen-pallet-count' ). text ()). toBe ( '1' )
2026-09-14 16:38:51 +08:00
expect ( document . body . textContent ). toContain ( '固定按头程已封托托盘数整体入库' )
;([... document . body . querySelectorAll ( 'button' )]
. find (( item ) => item . textContent ? . includes ( '保存并生成入库单' )) as HTMLButtonElement ). click ()
await flushPromises ()
expect ( createManualInboundPlan ). toHaveBeenCalledWith ( expect . objectContaining ({ palletCount : 1 }),
expect . stringContaining ( 'manual-inbound-plan-' ))
})
2026-08-27 17:49:05 +08:00
it ( 'opens a cabinet selector without requiring order selection and loads server summaries only then' , async () => {
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-08-27 17:49:05 +08:00
await openContainerDialog ( wrapper )
2026-08-28 13:54:44 +08:00
expect ( listContainerPlanProgress ). toHaveBeenCalledTimes ( 1 )
expect ( listContainerPlanProgress ). toHaveBeenCalledWith ({ page : 1 , size : 50 , containerNo : undefined })
2026-08-27 17:49:05 +08:00
expect ( document . body . textContent ). toContain ( '选择待分配柜' )
2026-10-20 10:11:31 +08:00
await cabinetDialog ( wrapper ). get ( 'button[aria-label="查看按柜生成说明"]' ). trigger ( 'focus' )
await new Promise ( resolve => setTimeout ( resolve , 200 )); await flushPromises ()
2026-09-09 00:44:07 +08:00
expect ( document . body . textContent ). toContain ( '确认后整柜只提交一次生成命令' )
expect ( getContainerInboundPlanPreview ). not . toHaveBeenCalled ()
2026-08-27 17:49:05 +08:00
expect ( convertLastMileContainerToInboundPlans ). not . toHaveBeenCalled ()
2026-08-27 17:16:38 +08:00
})
2026-08-27 17:49:05 +08:00
it ( 'selects one cabinet and sends one long-id convert-container request without using current-page row counts' , async () => {
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-08-27 17:49:05 +08:00
await openContainerDialog ( wrapper )
await chooseContainer ( wrapper )
2026-09-09 00:44:07 +08:00
const confirm = [... document . body . querySelectorAll ( 'button' )]. find (( item ) => item . textContent ? . includes ( '确认生成' )) as HTMLButtonElement
2026-08-27 17:49:05 +08:00
expect ( confirm . disabled ). toBe ( false )
confirm . click ()
await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledTimes ( 1 )
2026-09-09 00:44:07 +08:00
expect ( getContainerInboundPlanPreview ). toHaveBeenCalledTimes ( 2 )
expect ( getContainerInboundPlanPreview ). toHaveBeenNthCalledWith ( 1 , bookingId )
expect ( getContainerInboundPlanPreview ). toHaveBeenNthCalledWith ( 2 , bookingId )
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledWith ({
sourceContainerBookingId : bookingId ,
snapshotVersion : 9 ,
snapshotHash : 'a' . repeat ( 64 ),
}, expect . stringContaining ( 'container-inbound-plan-' ))
2026-09-07 23:58:18 +08:00
expect ( createManualInboundPlan ). not . toHaveBeenCalled ()
2026-09-09 00:44:07 +08:00
expect ( document . body . textContent ). toContain ( '新建 2 个 / 已有 0 个' )
expect ( document . body . textContent ). toContain ( 'PLT-001' )
2026-08-27 17:49:05 +08:00
expect ( listLastMileOrders ). toHaveBeenCalledTimes ( 3 )
})
it . each ([ 'A0404' , 'A0409' ])( 'keeps %s request evidence visible after cabinet conversion failure' , async ( code ) => {
2026-08-27 17:16:38 +08:00
vi . mocked ( convertLastMileContainerToInboundPlans ). mockRejectedValueOnce ( new ApiResponseError (
'柜级生成失败' , code , 200 , undefined , `request- ${ code } ` , `trace- ${ code } ` ,
))
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-08-27 17:49:05 +08:00
await openContainerDialog ( wrapper )
await chooseContainer ( wrapper )
2026-09-09 00:44:07 +08:00
const confirm = [... document . body . querySelectorAll ( 'button' )]. find (( item ) => item . textContent ? . includes ( '确认生成' )) as HTMLButtonElement
2026-08-27 17:49:05 +08:00
confirm . click ()
2026-08-27 17:16:38 +08:00
await flushPromises ()
2026-08-27 17:49:05 +08:00
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledTimes ( 1 )
2026-08-27 17:16:38 +08:00
expect ( wrapper . text ()). toContain ( code )
expect ( wrapper . text ()). toContain ( `request- ${ code } ` )
expect ( wrapper . text ()). toContain ( `trace- ${ code } ` )
2026-08-27 15:56:13 +08:00
})
2026-08-28 13:54:44 +08:00
2026-09-09 00:44:07 +08:00
it ( 'keeps the latest cabinet preview when an older request resolves last' , async () => {
let resolveFirst !: ( value : typeof preview ) => void
let resolveSecond !: ( value : typeof preview ) => void
vi . mocked ( getContainerInboundPlanPreview ). mockImplementation (( id ) => new Promise (( resolve ) => {
if ( id === bookingId ) resolveFirst = resolve
else resolveSecond = resolve
}) as never )
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-09-09 00:44:07 +08:00
await openContainerDialog ( wrapper )
const selects = wrapper . findAllComponents ({ name : 'ElSelect' })
const containerSelect = selects [ selects . length - 1 ]
containerSelect . vm . $emit ( 'update:modelValue' , bookingId )
containerSelect . vm . $emit ( 'change' , bookingId )
await wrapper . vm . $nextTick ()
containerSelect . vm . $emit ( 'update:modelValue' , '2092873463568597002' )
containerSelect . vm . $emit ( 'change' , '2092873463568597002' )
await wrapper . vm . $nextTick ()
resolveSecond ({ ... preview , sourceContainerBookingId : '2092873463568597002' , containerNo : 'CONT-NEW' , pendingPalletCount : 2 })
await flushPromises ()
expect ( document . body . querySelector ( '.container-preview__identity' ) ? . textContent ). toContain ( 'CONT-NEW' )
resolveFirst ( preview )
await flushPromises ()
expect ( document . body . querySelector ( '.container-preview__identity' ) ? . textContent ). toContain ( 'CONT-NEW' )
})
it ( 'reuses the same idempotency key when an uncertain confirmation is retried' , async () => {
vi . mocked ( convertLastMileContainerToInboundPlans )
. mockRejectedValueOnce ( new Error ( 'network uncertain' ))
. mockResolvedValueOnce ([{ planNo : 'LIP-RETRY' , pallets : [{ id : 'PALLET-R' , palletNo : 'PLT-RETRY' }] }] as never )
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-09-09 00:44:07 +08:00
await openContainerDialog ( wrapper )
await chooseContainer ( wrapper )
const findConfirm = () => [... document . body . querySelectorAll ( 'button' )]
. find (( item ) => item . textContent ? . includes ( '确认生成' )) as HTMLButtonElement
findConfirm (). click ()
await flushPromises ()
findConfirm (). click ()
await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledTimes ( 2 )
expect ( vi . mocked ( convertLastMileContainerToInboundPlans ). mock . calls [ 0 ][ 1 ]). toBe (
vi . mocked ( convertLastMileContainerToInboundPlans ). mock . calls [ 1 ][ 1 ],
)
expect ( document . body . textContent ). toContain ( 'PLT-RETRY' )
})
2026-09-08 11:50:32 +08:00
it ( 'renders all cabinets as an arrival-ordered workbench with the remaining inbound-code count' , async () => {
2026-08-28 13:54:44 +08:00
const nullArrival = { ... summary , sourceContainerBookingId : '2092873463568597002' , containerNo : 'CONT-NULL' , plannedArrivalAt : null , planCount : 0 , pendingPlanCount : 15 }
2026-09-07 20:45:31 +08:00
vi . mocked ( listContainerPlanProgress ). mockResolvedValueOnce ({ records : [ summary , nullArrival ], page : 1 , size : 10 , total : 2 , pages : 1 })
2026-08-28 13:54:44 +08:00
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-08-28 13:54:44 +08:00
expect ( listContainerPlanProgress ). toHaveBeenLastCalledWith ({ page : 1 , size : 10 , containerNo : undefined , createdFrom : undefined , createdTo : undefined })
2026-09-10 15:40:55 +08:00
expect ( wrapper . text ()). not . toContain ( '柜子工作台(2) ' )
2026-09-08 11:50:32 +08:00
expect ( wrapper . text ()). toContain ( '货柜工作台' )
2026-10-20 15:48:27 +08:00
expect ( wrapper . text ()). not . toContain ( '按柜查看和修改海外接柜仓;整柜计划齐全后自动生成托盘码' )
2026-09-02 14:39:34 +08:00
expect ( wrapper . text ()). toContain ( '5个入仓码待生成计划' )
expect ( wrapper . text ()). toContain ( '共 15 个入仓码' )
expect ( wrapper . text ()). not . toContain ( '待补入仓码' )
expect ( wrapper . text ()). not . toContain ( '缺入仓码' )
expect ( wrapper . text ()). not . toContain ( '已生成 10 个计划' )
expect ( wrapper . text ()). not . toContain ( '存在5票尚未生成有效入库计划' )
2026-09-08 11:50:32 +08:00
expect ( wrapper . text ()). toContain ( '2 个货柜' )
2026-08-28 13:54:44 +08:00
expect ( wrapper . text (). indexOf ( 'CONT-15' )). toBeLessThan ( wrapper . text (). indexOf ( 'CONT-NULL' ))
})
2026-09-10 17:05:48 +08:00
it ( 'presents a resource-pending cabinet as an exceptional pallet-code retry' , async () => {
2026-09-08 11:50:32 +08:00
const ready = {
... summary ,
planCount : summary.inboundCodeCount ,
plannedInboundCodeCount : summary.inboundCodeCount ,
pendingPlanCount : 0 ,
2026-09-10 16:33:43 +08:00
resourcePendingPlanCount : 1 ,
2026-09-08 11:50:32 +08:00
allPlansReady : true ,
blockingReason : null ,
}
vi . mocked ( listContainerPlanProgress ). mockResolvedValueOnce ({ records : [ ready ], page : 1 , size : 10 , total : 1 , pages : 1 })
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-08 11:50:32 +08:00
expect ( wrapper . text ()). toContain ( 'CONT-15' )
expect ( wrapper . text ()). toContain ( '0个入仓码待生成计划' )
2026-09-10 17:05:48 +08:00
expect ( wrapper . text ()). toContain ( '托盘码自动生成异常,可重试' )
expect ( wrapper . text ()). not . toContain ( '待补真实托盘' )
expect (( button ( wrapper , '重试生成托盘码' ) ! . element as HTMLButtonElement ). disabled ). toBe ( false )
2026-09-08 11:50:32 +08:00
expect (( button ( wrapper , '修改接柜仓' ) ! . element as HTMLButtonElement ). disabled ). toBe ( false )
})
2026-10-05 02:13:24 +08:00
it ( 'restores the original resource command after reopening a locally committed cabinet' , async () => {
vi . mocked ( listContainerPlanProgress ). mockResolvedValueOnce ({
records : [{ ... summary , pendingPlanCount : 0 , resourcePendingPlanCount : 1 , allPlansReady : true }],
page : 1 , size : 10 , total : 1 , pages : 1 ,
})
2026-10-05 05:40:21 +08:00
vi . mocked ( getContainerInboundPlanPreview ). mockResolvedValueOnce ( resourceCommittedPreview )
vi . mocked ( convertLastMileContainerToInboundPlans ). mockResolvedValue ( originalResourcePlans as never )
2026-10-05 02:13:24 +08:00
const wrapper = await mountView ()
await openHiddenContainerWorkbench ( wrapper )
await button ( wrapper , '重试生成托盘码' ) ! . trigger ( 'click' )
await flushPromises ()
2026-10-05 05:40:21 +08:00
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( false )
expect ( cabinetDialog ( wrapper ). props ( 'confirmText' )). toBe ( '重试资源确认' )
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' )
2026-10-05 02:13:24 +08:00
await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledWith ({
sourceContainerBookingId : bookingId , snapshotVersion : 9 , snapshotHash : 'a' . repeat ( 64 ),
}, 'original-container-command' )
})
2026-10-05 05:40:21 +08:00
it ( 'reopens a complete local plan and confirms only the original resource key/version/hash without showing zero generation' , async () => {
const success = vi . spyOn ( ElMessage , 'success' ). mockImplementation (() => ({ close : vi.fn () } as never ))
vi . mocked ( getContainerInboundPlanPreview )
. mockResolvedValueOnce ( resourceCommittedPreview )
. mockResolvedValueOnce ( resourceCommittedPreview )
. mockResolvedValue ({ ... resourceCommittedPreview , resourceRetry : undefined })
vi . mocked ( convertLastMileContainerToInboundPlans ). mockResolvedValue ( originalResourcePlans as never )
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-10-05 05:40:21 +08:00
await openContainerDialog ( wrapper ); await chooseContainer ( wrapper )
cabinetDialog ( wrapper ). vm . $emit ( 'update:modelValue' , false ); await flushPromises ()
await openContainerDialog ( wrapper ); await chooseContainer ( wrapper )
const dialog = cabinetDialog ( wrapper )
expect ( dialog . props ( 'confirmDisabled' )). toBe ( false )
expect ( dialog . props ( 'confirmText' )). toBe ( '重试资源确认' )
expect ( document . body . textContent ). toContain ( '资源仍待确认' )
expect ( document . body . textContent ). not . toContain ( '最终生成 0 个托盘' )
expect ( document . body . textContent ). not . toContain ( '最终 0 托' )
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledExactlyOnceWith ({
sourceContainerBookingId : bookingId , snapshotVersion : 9 , snapshotHash : 'a' . repeat ( 64 ),
}, 'original-container-command' )
expect ( document . body . textContent ). toContain ( '资源确认完成' )
expect ( document . body . textContent ). toContain ( 'PLT-ORIGINAL' )
expect ( cabinetDialog ( wrapper ). props ( 'confirmText' )). toBe ( '已确认' )
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( true )
expect ( success ). toHaveBeenCalledWith ( '已完成 1 个已有计划的资源确认' )
expect ( createManualInboundPlan ). not . toHaveBeenCalled ()
})
it ( 'keeps ordinary zero-pending previews without resourceRetry disabled in both UI and handler' , async () => {
vi . mocked ( getContainerInboundPlanPreview ). mockResolvedValue ({ ... resourceCommittedPreview , resourceRetry : undefined })
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture (); await openContainerDialog ( wrapper ); await chooseContainer ( wrapper )
2026-10-05 05:40:21 +08:00
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( true )
expect ( cabinetDialog ( wrapper ). props ( 'confirmText' )). toBe ( '确认生成' )
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). not . toHaveBeenCalled ()
})
it ( 'uses the visible missing-container label in the preview tooltip without exposing the booking ID' , async () => {
vi . mocked ( getContainerInboundPlanPreview ). mockResolvedValue ({ ... preview , containerNo : null } as never )
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture (); await openContainerDialog ( wrapper ); await chooseContainer ( wrapper )
2026-10-05 05:40:21 +08:00
const identity = document . body . querySelector ( '.container-preview__identity' ) !
expect ( identity . textContent ). toBe ( '未录入货柜号' )
expect ( identity . getAttribute ( 'title' )). toBe ( '未录入货柜号' )
expect ( identity . getAttribute ( 'title' )). not . toContain ( bookingId )
})
it ( 'keeps server readiness blocks effective even for a resource retry' , async () => {
vi . mocked ( getContainerInboundPlanPreview ). mockResolvedValue ({ ... resourceCommittedPreview , ready : false , blockedOrderCount : 1 })
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture (); await openContainerDialog ( wrapper ); await chooseContainer ( wrapper )
2026-10-05 05:40:21 +08:00
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( true )
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). not . toHaveBeenCalled ()
})
it ( 'guards resource retry at the handler when the caller lacks create permission' , async () => {
canAccess . mockImplementation ( code => code !== 'platform:overseas-warehouse:inbound-order:create' )
vi . mocked ( getContainerInboundPlanPreview ). mockResolvedValue ( resourceCommittedPreview )
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-10-05 05:40:21 +08:00
const view = wrapper . vm as unknown as { openContainerConversion () : void }
view . openContainerConversion (); await flushPromises (); await chooseContainer ( wrapper )
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( true )
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). not . toHaveBeenCalled ()
})
it ( 'keeps preview and candidate loading guards effective for recovery commands' , async () => {
let resolvePreview !: ( value : typeof resourceCommittedPreview ) => void
let resolveOptions !: ( value : Awaited < ReturnType < typeof listContainerPlanProgress >>) => void
vi . mocked ( getContainerInboundPlanPreview ). mockReturnValue ( new Promise ( resolve => { resolvePreview = resolve }))
vi . mocked ( listContainerPlanProgress ). mockReturnValue ( new Promise ( resolve => { resolveOptions = resolve }))
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture (); await openContainerDialog ( wrapper ); await chooseContainer ( wrapper )
2026-10-05 05:40:21 +08:00
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( true )
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). not . toHaveBeenCalled ()
resolvePreview ( resourceCommittedPreview ); await flushPromises ()
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( true )
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). not . toHaveBeenCalled ()
resolveOptions ({ records : [ summary ], page : 1 , size : 50 , total : 1 , pages : 1 }); await flushPromises ()
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( false )
})
it ( 'preserves the original recovery command on failure and blocks duplicate or completed submissions' , async () => {
const success = vi . spyOn ( ElMessage , 'success' ). mockImplementation (() => ({ close : vi.fn () } as never ))
vi . mocked ( getContainerInboundPlanPreview ). mockResolvedValue ( resourceCommittedPreview )
vi . mocked ( convertLastMileContainerToInboundPlans ). mockRejectedValueOnce ( new ApiResponseError ( '资源确认失败' , 'S0503' , 503 ))
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture (); await openContainerDialog ( wrapper ); await chooseContainer ( wrapper )
2026-10-05 05:40:21 +08:00
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( cabinetDialog ( wrapper ). props ( 'modelValue' )). toBe ( true )
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( false )
expect ( success ). not . toHaveBeenCalled ()
let resolveCommand !: ( value : Awaited < ReturnType < typeof convertLastMileContainerToInboundPlans >>) => void
vi . mocked ( convertLastMileContainerToInboundPlans ). mockReturnValueOnce ( new Promise ( resolve => { resolveCommand = resolve }))
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledTimes ( 2 )
expect ( cabinetDialog ( wrapper ). props ( 'confirmDisabled' )). toBe ( true )
resolveCommand ( originalResourcePlans as never ); await flushPromises ()
cabinetDialog ( wrapper ). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledTimes ( 2 )
for ( const args of vi . mocked ( convertLastMileContainerToInboundPlans ). mock . calls ) expect ( args ). toEqual ([
{ sourceContainerBookingId : bookingId , snapshotVersion : 9 , snapshotHash : 'a' . repeat ( 64 ) }, 'original-container-command' ,
])
})
2026-09-08 11:50:32 +08:00
it ( 'renders the all-cabinet empty state and unit' , async () => {
vi . mocked ( listContainerPlanProgress ). mockResolvedValueOnce ({ records : [], page : 1 , size : 10 , total : 0 , pages : 0 })
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-08 11:50:32 +08:00
expect ( wrapper . text ()). toContain ( '暂无货柜数据' )
expect ( wrapper . text ()). not . toContain ( '已全部生成计划的柜不再展示' )
})
2026-09-02 14:39:34 +08:00
it ( 'uses independent cabinet pagination and opens the complete long-id detail in a dialog' , async () => {
2026-08-28 13:54:44 +08:00
vi . mocked ( listContainerPlanProgress ). mockResolvedValue ({ records : [ summary ], page : 1 , size : 10 , total : 21 , pages : 3 })
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-02 14:39:34 +08:00
const pagination = wrapper . findComponent ({ name : 'ElPagination' })
pagination . vm . $emit ( 'current-change' , 2 )
2026-08-28 13:54:44 +08:00
await flushPromises ()
expect ( listContainerPlanProgress ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 2 , size : 10 }))
2026-09-02 15:15:47 +08:00
await wrapper . get ( '.container-workbench-card__detail' ). trigger ( 'click' )
2026-08-28 13:54:44 +08:00
await flushPromises ()
expect ( getContainerPlanProgress ). toHaveBeenCalledWith ( bookingId )
2026-09-02 14:39:34 +08:00
expect ( document . body . textContent ). toContain ( '柜子详情 · CONT-15' )
2026-08-28 13:54:44 +08:00
expect ( document . body . textContent ). toContain ( 'LMO-1' )
expect ( document . body . textContent ). toContain ( 'IN-1' )
expect ( document . body . textContent ). toContain ( 'LIP-1' )
2026-09-07 20:45:31 +08:00
expect ( document . body . textContent ). toContain ( '已生成入库计划' )
expect ( document . body . textContent ). toContain ( '待收货' )
expect ( document . body . textContent ). not . toContain ( 'INBOUND_PLANNED' )
expect ( document . body . textContent ). not . toContain ( 'PENDING_RECEIVING' )
2026-09-02 14:39:34 +08:00
expect ( document . body . querySelector ( '.el-dialog' )). not . toBeNull ()
expect ( document . body . querySelector ( '.el-drawer' )). toBeNull ()
2026-08-28 13:54:44 +08:00
})
2026-09-02 15:15:47 +08:00
it ( 'starts cabinet plan generation directly from the workbench card' , async () => {
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-10-19 17:56:31 +08:00
await button ( wrapper , '入库' ) ! . trigger ( 'click' )
2026-09-02 15:15:47 +08:00
await flushPromises ()
2026-09-11 15:39:47 +08:00
expect ( document . body . textContent ). toContain ( '头程已有 4 个托盘码,海外仓补充 1 个,最终生成 5 个托盘' )
expect ( document . body . textContent ). toContain ( '头程已有 4 托 / 海外仓补充 1 托' )
2026-09-09 00:44:07 +08:00
const confirm = [... document . body . querySelectorAll ( 'button' )]. find (( item ) => item . textContent ? . includes ( '确认生成' )) as HTMLButtonElement
2026-09-02 15:15:47 +08:00
expect ( confirm . disabled ). toBe ( false )
confirm . click ()
await flushPromises ()
2026-09-09 00:44:07 +08:00
expect ( convertLastMileContainerToInboundPlans ). toHaveBeenCalledWith ({
sourceContainerBookingId : bookingId ,
snapshotVersion : 9 ,
snapshotHash : 'a' . repeat ( 64 ),
}, expect . stringContaining ( 'container-inbound-plan-' ))
2026-09-02 15:15:47 +08:00
expect ( getContainerPlanProgress ). not . toHaveBeenCalled ()
})
2026-09-07 16:36:21 +08:00
it ( 'keeps an invalid historical cabinet readable while blocking plan generation' , async () => {
const invalid = {
... summary ,
overseasWarehouseId : null ,
overseasWarehouseCode : null ,
overseasWarehouseName : null ,
blockingReason : '柜内接柜仓业务快照不一致' ,
}
vi . mocked ( listContainerPlanProgress ). mockResolvedValueOnce ({ records : [ invalid ], page : 1 , size : 10 , total : 1 , pages : 1 } as never )
vi . mocked ( getContainerPlanProgress ). mockResolvedValueOnce ({ summary : invalid , orders : [] } as never )
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-07 16:36:21 +08:00
expect ( wrapper . text ()). toContain ( '柜内接柜仓业务快照不一致' )
2026-10-19 17:56:31 +08:00
expect (( button ( wrapper , '入库' ) ! . element as HTMLButtonElement ). disabled ). toBe ( true )
2026-09-07 16:36:21 +08:00
await wrapper . get ( '.container-workbench-card__detail' ). trigger ( 'click' )
await flushPromises ()
expect ( getContainerPlanProgress ). toHaveBeenCalledWith ( bookingId )
expect ( document . body . textContent ). toContain ( '柜子详情 · CONT-15' )
expect ( convertLastMileContainerToInboundPlans ). not . toHaveBeenCalled ()
})
2026-09-07 15:07:39 +08:00
it ( 'shows the receiving warehouse and changes it from ACTIVE OVERSEAS master data with versioning' , async () => {
2026-09-07 20:25:14 +08:00
vi . mocked ( listAllPlatformWarehouses ). mockResolvedValueOnce ([ currentReceivingWarehouse , receivingWarehouse ] as never )
2026-09-07 15:07:39 +08:00
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-07 15:07:39 +08:00
expect ( wrapper . text ()). toContain ( '海外接柜仓' )
expect ( wrapper . text ()). toContain ( 'LAX01 · 洛杉矶接柜仓' )
2026-09-07 20:25:14 +08:00
expect ( wrapper . get ( '.container-workbench-card__warehouse strong' ). attributes ( 'title' )). toBe ( 'LAX01 · 洛杉矶接柜仓' )
expect ( listAllPlatformWarehouses ). not . toHaveBeenCalled ()
2026-09-07 15:07:39 +08:00
await button ( wrapper , '修改接柜仓' ) ! . trigger ( 'click' )
await flushPromises ()
2026-09-07 20:25:14 +08:00
const receivingDialog = wrapper . findAllComponents ( AppDialog ). find (( item ) => item . props ( 'title' ) === '修改海外接柜仓' ) !
expect ( receivingDialog . props ( 'width' )). toBe ( 'min(600px, calc(100vw - 28px))' )
2026-09-07 15:07:39 +08:00
expect ( listAllPlatformWarehouses ). toHaveBeenLastCalledWith ({ warehouseType : 'OVERSEAS' , status : 'ACTIVE' })
2026-09-07 20:25:14 +08:00
expect ( listAllPlatformWarehouses ). toHaveBeenCalledTimes ( 1 )
2026-09-07 15:07:39 +08:00
const warehouseSelect = wrapper . findAllComponents ({ name : 'ElSelect' }). find (( item ) => item . props ( 'placeholder' ) === '请选择 ACTIVE 海外接柜仓' ) !
2026-09-07 20:25:14 +08:00
expect ( warehouseSelect . props ( 'modelValue' )). toBe ( '' )
expect ( wrapper . text ()). not . toContain ( '9007199254740995' )
const warehouseOptions = wrapper . findAllComponents ({ name : 'ElOption' })
expect ( warehouseOptions . find (( item ) => item . props ( 'value' ) === summary . overseasWarehouseId ) ? . props ( 'disabled' )). toBe ( true )
const confirm = [... document . body . querySelectorAll ( 'button' )]. find (( item ) => item . textContent ? . includes ( '确认修改' )) as HTMLButtonElement
warehouseSelect . vm . $emit ( 'update:modelValue' , summary . overseasWarehouseId )
await wrapper . vm . $nextTick ()
expect ( confirm . disabled ). toBe ( true )
expect ( wrapper . text ()). toContain ( '新的海外接柜仓必须与当前仓不同' )
2026-09-07 15:07:39 +08:00
warehouseSelect . vm . $emit ( 'update:modelValue' , '9007199254740996' )
await wrapper . vm . $nextTick ()
2026-09-07 20:25:14 +08:00
expect ( confirm . disabled ). toBe ( false )
2026-09-07 15:07:39 +08:00
confirm . click ()
await flushPromises ()
expect ( changeContainerReceivingWarehouse ). toHaveBeenCalledWith ( bookingId , {
overseasWarehouseId : '9007199254740996' , expectedVersion : 7 , remark : undefined ,
}, expect . stringMatching ( /^container-receiving-warehouse-/ ))
2026-09-07 20:25:14 +08:00
expect ( listContainerPlanProgress ). toHaveBeenCalledTimes ( 2 )
})
it ( 'keeps the receiving-warehouse confirmation disabled while candidates are loading' , async () => {
let resolveCandidates !: ( value : typeof receivingWarehouse []) => void
vi . mocked ( listAllPlatformWarehouses ). mockReturnValueOnce ( new Promise (( resolve ) => { resolveCandidates = resolve }) as never )
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-07 20:25:14 +08:00
await button ( wrapper , '修改接柜仓' ) ! . trigger ( 'click' )
await wrapper . vm . $nextTick ()
const confirm = [... document . body . querySelectorAll ( 'button' )]. find (( item ) => item . textContent ? . includes ( '确认修改' )) as HTMLButtonElement
expect ( confirm . disabled ). toBe ( true )
expect ( listAllPlatformWarehouses ). toHaveBeenCalledTimes ( 1 )
resolveCandidates ([ receivingWarehouse ])
await flushPromises ()
expect ( confirm . disabled ). toBe ( true )
2026-09-07 15:07:39 +08:00
})
2026-09-08 11:45:30 +08:00
it ( 'keeps a disabled receiving-warehouse action discoverable when the server locks the container' , async () => {
2026-09-07 15:07:39 +08:00
vi . mocked ( listContainerPlanProgress ). mockResolvedValueOnce ({
records : [{ ... summary , canChangeReceivingWarehouse : false , receivingWarehouseLockReason : '海外仓已开始拆柜收货' }],
page : 1 , size : 10 , total : 1 , pages : 1 ,
})
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-07 15:07:39 +08:00
expect ( wrapper . text ()). toContain ( '海外仓已开始拆柜收货' )
2026-09-08 11:45:30 +08:00
const changeButton = button ( wrapper , '修改接柜仓' ) !
expect ( changeButton . exists ()). toBe ( true )
expect (( changeButton . element as HTMLButtonElement ). disabled ). toBe ( true )
expect ( changeButton . attributes ( 'title' )). toBe ( '海外仓已开始拆柜收货' )
expect ( changeContainerReceivingWarehouse ). not . toHaveBeenCalled ()
})
it ( 'keeps a disabled receiving-warehouse action discoverable without edit permission' , async () => {
canAccess . mockImplementation (( permissionCode : string ) => permissionCode !== 'platform:overseas-warehouse:last-mile-order:edit' )
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-08 11:45:30 +08:00
const changeButton = button ( wrapper , '修改接柜仓' ) !
expect ( changeButton . exists ()). toBe ( true )
expect (( changeButton . element as HTMLButtonElement ). disabled ). toBe ( true )
expect ( changeButton . attributes ( 'title' )). toBe ( '暂无修改接柜仓权限,请联系管理员开通尾程订单编辑权限' )
await changeButton . trigger ( 'click' )
expect ( listAllPlatformWarehouses ). not . toHaveBeenCalled ()
2026-09-07 15:07:39 +08:00
expect ( changeContainerReceivingWarehouse ). not . toHaveBeenCalled ()
})
it ( 'keeps conflict metadata visible and refreshes the stale container summary' , async () => {
vi . mocked ( listAllPlatformWarehouses ). mockResolvedValueOnce ([ receivingWarehouse ] as never )
vi . mocked ( changeContainerReceivingWarehouse ). mockRejectedValueOnce ( new ApiResponseError (
'version conflict' , 'A0409' , 200 , undefined , 'request-receiving' , 'trace-receiving' ,
))
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-07 15:07:39 +08:00
await button ( wrapper , '修改接柜仓' ) ! . trigger ( 'click' )
await flushPromises ()
const warehouseSelect = wrapper . findAllComponents ({ name : 'ElSelect' }). find (( item ) => item . props ( 'placeholder' ) === '请选择 ACTIVE 海外接柜仓' ) !
warehouseSelect . vm . $emit ( 'update:modelValue' , '9007199254740996' )
await wrapper . vm . $nextTick ()
const confirm = [... document . body . querySelectorAll ( 'button' )]. find (( item ) => item . textContent ? . includes ( '确认修改' )) as HTMLButtonElement
confirm . click ()
await flushPromises ()
expect ( wrapper . text ()). toContain ( '接柜仓信息已被其他操作更新,请刷新后重试' )
expect ( wrapper . text ()). toContain ( 'request-receiving' )
expect ( wrapper . text ()). toContain ( 'trace-receiving' )
expect ( listContainerPlanProgress ). toHaveBeenCalledTimes ( 2 )
})
it ( 'renders an explicit empty state when there are no ACTIVE OVERSEAS receiving warehouses' , async () => {
vi . mocked ( listAllPlatformWarehouses ). mockResolvedValueOnce ([])
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-07 15:07:39 +08:00
await button ( wrapper , '修改接柜仓' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( document . body . textContent ). toContain ( '暂无可用的 ACTIVE 海外接柜仓' )
expect ( changeContainerReceivingWarehouse ). not . toHaveBeenCalled ()
})
it ( 'keeps receiving-warehouse candidate failure metadata visible without an FBA fallback' , async () => {
vi . mocked ( listAllPlatformWarehouses ). mockRejectedValueOnce ( new ApiResponseError (
'receiving warehouse candidate failed' , 'A0503' , 200 , undefined , 'request-receiving-options' , 'trace-receiving-options' ,
))
const wrapper = await mountView ()
2026-09-10 15:40:55 +08:00
await openHiddenContainerWorkbench ( wrapper )
2026-09-07 15:07:39 +08:00
await button ( wrapper , '修改接柜仓' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( document . body . textContent ). toContain ( 'request-receiving-options' )
expect ( document . body . textContent ). toContain ( 'trace-receiving-options' )
expect ( document . body . textContent ). not . toContain ( '请选择目的 FBA 仓' )
expect ( changeContainerReceivingWarehouse ). not . toHaveBeenCalled ()
})
2026-09-07 20:25:14 +08:00
it ( 'keeps dense cabinet fields and actions on one line with ellipsis at 1920px' , () => {
const source = readFileSync ( resolve ( process . cwd (), 'src/modules/overseas-warehouse/pages/LastMileOrdersView.vue' ), 'utf8' )
expect ( source ). toContain ( '.container-detail-summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }' )
2026-10-20 23:34:11 +08:00
expect ( source ). toContain ( '.container-workbench-card__warehouse strong { min-width: 0; overflow: hidden; color: var(--workbench-text); font-size: var(--admin-font-section); text-overflow: ellipsis; white-space: nowrap; }' )
2026-09-14 20:03:27 +08:00
expect ( source ). toContain ( '.container-workbench-card__actions { display: flex; justify-content: flex-end; padding-top: 2px; white-space: nowrap; }' )
2026-09-14 20:21:02 +08:00
expect ( source ). toContain ( "{ key: 'actions', label: '操作', width: 330, fixed: 'left'" )
2026-09-07 20:25:14 +08:00
})
2026-08-28 13:54:44 +08:00
it ( 'loads only active FBA master data and submits exact string fields for a cross-customer transfer' , async () => {
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-08-28 13:54:44 +08:00
await selectRows ( wrapper , [ selectedRow (), selectedRow ({ id : '2092873463568596995' , customerId : '2092618717861048322' })])
await button ( wrapper , '转仓' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( listAllPlatformWarehouses ). toHaveBeenCalledWith ({ warehouseType : 'FBA' , status : 'ACTIVE' })
const warehouseSelect = wrapper . findAllComponents ({ name : 'ElSelect' }). find (( item ) => item . props ( 'placeholder' ) === '请选择目的 FBA 仓' ) !
expect ( warehouseSelect ). toBeDefined ()
const option = wrapper . findAllComponents ({ name : 'ElOption' }). find (( item ) => item . props ( 'value' ) === '9007199254740993' ) !
expect ( option . props ( 'label' )). toBe ( 'ONT8 · Ontario FBA · CA / Ontario' )
warehouseSelect . vm . $emit ( 'update:modelValue' , '9007199254740993' )
await wrapper . vm . $nextTick ()
const confirm = [... document . body . querySelectorAll ( 'button' )]. find (( item ) => item . textContent ? . includes ( '确认转仓' )) as HTMLButtonElement
expect ( confirm . disabled ). toBe ( false )
confirm . click ()
await flushPromises ()
expect ( transferLastMileOrders ). toHaveBeenCalledWith ({
orderIds : [ '2092873463568596994' , '2092873463568596995' ],
warehouseId : '9007199254740993' , warehouseCode : 'ONT8' , warehouseName : 'Ontario FBA' , remark : undefined ,
})
expect ( vi . mocked ( transferLastMileOrders ). mock . calls [ 0 ] ? .[ 0 ]. warehouseId ). toBeTypeOf ( 'string' )
expect ( wrapper . html ()). not . toContain ( '请输入目的仓 ID' )
})
it ( 'clears all selected warehouse facts when the FBA candidate is cleared' , async () => {
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-08-28 13:54:44 +08:00
await selectRows ( wrapper , [ selectedRow ()])
await button ( wrapper , '转仓' ) ! . trigger ( 'click' )
await flushPromises ()
const warehouseSelect = wrapper . findAllComponents ({ name : 'ElSelect' }). find (( item ) => item . props ( 'placeholder' ) === '请选择目的 FBA 仓' ) !
warehouseSelect . vm . $emit ( 'update:modelValue' , '9007199254740993' )
await wrapper . vm . $nextTick ()
warehouseSelect . vm . $emit ( 'update:modelValue' , '' )
await wrapper . vm . $nextTick ()
const confirm = [... document . body . querySelectorAll ( 'button' )]. find (( item ) => item . textContent ? . includes ( '确认转仓' )) as HTMLButtonElement
expect ( confirm . disabled ). toBe ( true )
expect ( transferLastMileOrders ). not . toHaveBeenCalled ()
})
it ( 'renders an explicit empty state when there are no active FBA candidates' , async () => {
vi . mocked ( listAllPlatformWarehouses ). mockResolvedValueOnce ([])
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-08-28 13:54:44 +08:00
await selectRows ( wrapper , [ selectedRow ()])
await button ( wrapper , '转仓' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( document . body . textContent ). toContain ( '暂无可用 FBA 仓候选' )
expect ( transferLastMileOrders ). not . toHaveBeenCalled ()
})
it ( 'keeps candidate load request metadata visible and prevents free-form fallback' , async () => {
vi . mocked ( listAllPlatformWarehouses ). mockRejectedValueOnce ( new ApiResponseError (
'FBA candidate failed' , 'A0503' , 200 , undefined , 'request-fba' , 'trace-fba' ,
))
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-08-28 13:54:44 +08:00
await selectRows ( wrapper , [ selectedRow ()])
await button ( wrapper , '转仓' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( document . body . textContent ). toContain ( 'A0503' )
expect ( document . body . textContent ). toContain ( 'request-fba' )
expect ( document . body . textContent ). toContain ( 'trace-fba' )
expect ( wrapper . html ()). not . toContain ( '请输入目的仓 ID' )
expect ( wrapper . html ()). not . toContain ( '请输入目的仓代码' )
expect ( wrapper . html ()). not . toContain ( '请输入目的仓名称' )
expect ( transferLastMileOrders ). not . toHaveBeenCalled ()
})
2026-09-30 14:27:41 +08:00
it ( 'keeps the return packing entry and excludes return orders from manual inbound generation' , async () => {
const wrapper = await mountView ()
2026-10-09 18:03:34 +08:00
await selectRows ( wrapper , [ selectedRow ({ orderTypeCode : 'RETURN_RELABEL' , currentStatusCode : 'INBOUND_COMPLETED' , sourceOrderNo : 'RETURN-1' })])
2026-09-30 14:27:41 +08:00
const actions = wrapper . get ( '.data-table__business-actions' )
2026-10-19 17:56:31 +08:00
expect ( actions . text ()). not . toContain ( '入库' )
2026-09-30 14:27:41 +08:00
const packing = actions . findAll ( 'button' ). find ( action => action . text (). includes ( '创建退货合箱计划(1) ' )) !
await packing . trigger ( 'click' )
await flushPromises ()
2026-10-09 18:03:34 +08:00
expect ( routerPush ). toHaveBeenCalledWith ({ name : 'overseas-return-inventory' , query : expect.objectContaining ({ packing : '1' , returnNos : 'RETURN-1' }) })
2026-09-30 14:27:41 +08:00
})
2026-10-20 10:11:31 +08:00
it ( 'keeps manual-plan drafts through native fullscreen with plain frozen source facts' , async () => {
2026-10-22 23:10:09 +08:00
const wrapper = await mountView ()
await openArrivalPlan ( wrapper )
2026-10-20 10:11:31 +08:00
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '入库计划 ·' )) !
2026-10-22 23:10:09 +08:00
expect ( dialog . props ( 'width' )). toBe ( 'min(1160px, calc(100vw - 48px))' )
expect ( dialog . findComponent ({ name : 'ElDialog' }). props ( 'top' )). toBe ( '6vh' )
expect ( dialog . get ( '[aria-label="入库来源"] h3' ). text ()). toContain ( '来源摘要' )
expect ( dialog . get ( '[aria-label="入库计划登记"] h3' ). text ()). toBe ( '计划设置' )
const sourceRows = dialog . findAll ( '.inbound-plan-source tbody tr' )
expect ( sourceRows [ 0 ]. get ( 'td' ). attributes ( 'colspan' )). toBe ( '3' )
expect ( sourceRows [ 1 ]. findAll ( 'th' ). map ( cell => cell . text ())). toEqual ([ '计划入库仓' , '入仓码' ])
expect ( dialog . props ( 'confirmText' )). toBe ( '保存并生成入库单' )
2026-10-20 10:11:31 +08:00
expect ( dialog . props ( 'title' )). toContain ( 'LMO-260827-0001' )
expect ( dialog . get ( '[data-tone="source"]' ). find ( 'input' ). exists ()). toBe ( false )
await dialog . get ( 'textarea' ). setValue ( '全屏后保留' )
await dialog . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' )
expect ( dialog . findComponent ({ name : 'ElDialog' }). props ( 'fullscreen' )). toBe ( true )
expect (( dialog . get ( 'textarea' ). element as HTMLTextAreaElement ). value ). toBe ( '全屏后保留' )
})
it ( 'locks manual-plan form edits and preserves a failed draft and request context' , async () => {
let reject !: ( reason : unknown ) => void
vi . mocked ( createManualInboundPlan ). mockReturnValue ( new Promise (( _ , no ) => { reject = no }))
2026-10-22 23:10:09 +08:00
const wrapper = await mountView ()
await openArrivalPlan ( wrapper )
2026-10-20 10:11:31 +08:00
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '入库计划 ·' )) !
await dialog . get ( 'textarea' ). setValue ( '保留入库备注' )
await dialog . findAll ( 'button' ). find ( item => item . text () === '保存并生成入库单' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( dialog . findComponent ({ name : 'ElDialog' }). props ()). toMatchObject ({ showClose : false , closeOnPressEscape : false })
expect ( dialog . get ( 'textarea' ). attributes ( 'disabled' )). toBeDefined ()
reject ( new ApiResponseError ( '入库失败' , 'TEST_REJECTED' , 409 , null , 'plan-test-request' ))
await flushPromises ()
expect (( dialog . get ( 'textarea' ). element as HTMLTextAreaElement ). value ). toBe ( '保留入库备注' )
expect ( dialog . props ( 'modelValue' )). toBe ( true )
expect ( document . body . textContent ). toContain ( 'plan-test-request' )
})
it . each ([ false , true ])( 'keeps hold/release flags, reason and zero version for detained=%s' , async detained => {
const wrapper = await mountView ()
const row = selectedRow ({ detained , currentStatusCode : 'INBOUND_COMPLETED' , version : 0 })
const vm = wrapper . vm as unknown as { openHold ( row : object ) : void }
vm . openHold ( row ); await flushPromises ()
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( detained ? '解除暂扣 ·' : '暂扣订单 ·' )) !
await dialog . get ( 'textarea' ). setValue ( ' 原因 ' )
dialog . findAllComponents ({ name : 'ElRadioGroup' })[ 1 ] ! . vm . $emit ( 'update:modelValue' , false )
await dialog . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' )
await dialog . findAll ( 'button' ). find ( item => item . text () === '保存' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( updateLastMileOrderHold ). toHaveBeenCalledWith ( row . id , { held : ! detained , chargeable : false , reason : '原因' , expectedVersion : 0 }, expect . stringContaining ( 'last-mile-hold-' ))
})
2026-11-03 11:43:50 +08:00
it . each ([ 'RELABEL_APPLICATION' , 'WAREHOUSE_TRANSFER' , 'LOCAL_LOOSEBOARD' , 'RETURN_RELABEL' , 'CARGO_DISCARD' ])( 'does not offer manual transport nodes for %s' , async orderType => {
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [{ id : 'business-tail' , customerId , orderNo : 'BUSINESS-1' , orderType , currentStatus : 'PENDING_ASSIGNMENT' }], page : 1 , size : 10 , total : 1 , pages : 1 })
const wrapper = await mountView ()
expect ( wrapper . get ( '.el-table__body-wrapper .row-actions' ). text ()). not . toContain ( '修改节点' )
await ( wrapper . vm as unknown as { openManualNodes ( row : object ) : Promise < void > }). openManualNodes ( selectedRow ({ orderTypeCode : orderType }))
expect ( getLastMileOrderManualNodes ). not . toHaveBeenCalled ()
})
it ( 'provides the same return receipt statuses as the list labels and submits their exact codes' , async () => {
const wrapper = await mountView ()
const toolbar = wrapper . findComponent ( QueryToolbar )
const status = ( toolbar . props ( 'fields' ) as Array < { key : string ; options? : Array < { label : string ; value : string } > } > ). find ( field => field . key === 'status' ) !
for ( const [ value , label ] of [[ 'PENDING_REVIEW' , '待审核' ], [ 'PENDING_RECEIPT' , '待签收入库' ], [ 'PARTIALLY_RECEIVED' , '部分入库' ], [ 'INBOUND_COMPLETED' , '全部入库' ]]) {
expect ( status . options ). toContainEqual ({ value , label })
toolbar . vm . $emit ( 'search' , { ... toolbar . props ( 'modelValue' ), status : value })
await flushPromises ()
expect ( listLastMileOrders ). toHaveBeenLastCalledWith ( expect . objectContaining ({ currentStatus : value }))
}
})
2026-10-26 15:51:07 +08:00
it ( 'edits only customs nodes while preserving hidden node facts and zero version' , async () => {
vi . mocked ( getLastMileOrderManualNodes ). mockResolvedValue ({ orderId : '2092873463568596994' , manualOverrideEnabled : true , nodes : [{ nodeCode : 'ACTUAL_DEPARTURE' , nodeAt : '2026-10-19T08:00:00' }], version : 0 })
2026-10-20 10:11:31 +08:00
const wrapper = await mountView ()
const row = selectedRow ()
await ( wrapper . vm as unknown as { openManualNodes ( row : object ) : Promise < void > }). openManualNodes ( row )
await flushPromises ()
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '修改节点 ·' )) !
2026-10-26 15:51:07 +08:00
expect ( dialog . findAll ( '.manual-nodes-editor tbody tr' ). map ( row => row . find ( 'th' ). text ())). toEqual ([ '国内报关完成' , '国外清关完成' ])
2026-10-20 10:11:31 +08:00
const dates = dialog . findAllComponents ({ name : 'ElDatePicker' })
2026-10-26 15:51:07 +08:00
expect ( dates ). toHaveLength ( 2 )
2026-10-20 10:11:31 +08:00
dates [ 0 ] ! . vm . $emit ( 'update:modelValue' , '2026-10-20T08:00:00' )
await dialog . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' )
await dialog . findAll ( 'button' ). find ( item => item . text () === '保存' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( updateLastMileOrderManualNodes ). toHaveBeenCalledWith ( row . id , 0 , [
{ nodeCode : 'DOMESTIC_CUSTOMS_COMPLETED' , nodeAt : '2026-10-20T08:00:00' },
2026-10-26 15:51:07 +08:00
{ nodeCode : 'ESTIMATED_DEPARTURE' , nodeAt : null }, { nodeCode : 'ACTUAL_DEPARTURE' , nodeAt : '2026-10-19T08:00:00' },
2026-10-20 10:11:31 +08:00
{ nodeCode : 'ESTIMATED_ARRIVAL' , nodeAt : null }, { nodeCode : 'OVERSEAS_CUSTOMS_COMPLETED' , nodeAt : null },
{ nodeCode : 'ACTUAL_ARRIVAL_WAITING_UNLOAD' , nodeAt : null }, { nodeCode : 'PICKUP_APPOINTED' , nodeAt : null }, { nodeCode : 'UNLOADED' , nodeAt : null },
], expect . stringContaining ( 'last-mile-manual-nodes-' ))
})
it ( 'keeps every destination group in the main preview body through fullscreen' , async () => {
const original = await getContainerInboundPlanPreview ( bookingId )
const group = original . groups [ 0 ] !
vi . mocked ( getContainerInboundPlanPreview ). mockResolvedValue ({ ... original , totalOrderCount : 13 , readyOrderCount : 13 , pendingOrderCount : 13 , pendingPalletCount : 13 , sourcePalletCount : 13 , supplementPalletCount : 0 ,
groups : Array.from ({ length : 13 }, ( _ , index ) => ({ ... group , destinationWarehouseCode : 'FBA-' + index , orderCount : 1 , pendingOrderCount : 1 , pendingPalletCount : 1 , sourcePalletCount : 1 , supplementPalletCount : 0 ,
orders : [{ ... group . orders [ 0 ] ! , orderId : 'order-' + index , businessOrderNo : '业务单-' + index , sourcePalletCount : 1 , targetPalletCount : 1 , supplementPalletCount : 0 }] })),
})
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-10-20 10:11:31 +08:00
await openContainerDialog ( wrapper ); await chooseContainer ( wrapper )
const dialog = cabinetDialog ( wrapper )
expect ( dialog . findAll ( '[aria-label="目的地分组明细"]' )). toHaveLength ( 13 )
expect ( dialog . findAll ( '.el-table__body-wrapper tr' )). toHaveLength ( 13 )
expect ( dialog . text ()). toContain ( '业务单-12' )
expect ( dialog . find ( '.container-preview__scroll' ). exists ()). toBe ( false )
await dialog . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' )
expect ( dialog . findComponent ({ name : 'ElDialog' }). props ( 'fullscreen' )). toBe ( true )
})
it ( 'keeps transfer drafts locked during submission and editable after rejection' , async () => {
let reject !: ( reason : unknown ) => void
vi . mocked ( transferLastMileOrders ). mockReturnValueOnce ( new Promise (( _ , no ) => { reject = no }))
2026-10-26 15:51:07 +08:00
const wrapper = await openAfterDeparture ()
2026-10-20 10:11:31 +08:00
await selectRows ( wrapper , [ selectedRow ()])
await button ( wrapper , '转仓' ) ! . trigger ( 'click' ); await flushPromises ()
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '转仓 ·' )) !
dialog . findComponent ({ name : 'ElSelect' }). vm . $emit ( 'update:modelValue' , fbaWarehouse . id )
await dialog . get ( 'textarea' ). setValue ( '保留转仓备注' )
await dialog . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' )
await dialog . findAll ( 'button' ). find ( item => item . text () === '确认转仓' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( dialog . get ( 'textarea' ). attributes ( 'disabled' )). toBeDefined ()
reject ( new ApiResponseError ( '转仓失败' , 'TEST_REJECTED' , 409 , null , 'transfer-test-request' )); await flushPromises ()
expect (( dialog . get ( 'textarea' ). element as HTMLTextAreaElement ). value ). toBe ( '保留转仓备注' )
expect ( dialog . props ( 'modelValue' )). toBe ( true )
})
it ( 'keeps rework selection and reason through fullscreen without issuing a command' , async () => {
const wrapper = await mountView ()
;( wrapper . vm as unknown as { reworkRelabel ( row : object ) : void }). reworkRelabel ( selectedRow ({ relabelOrderNo : 'RL-TEST-1' , relabelStatusCode : 'CUSTOMER_CONFIRM_PENDING' }))
await flushPromises ()
const dialog = wrapper . findAllComponents ( AppDialog ). find ( item => item . props ( 'title' ). startsWith ( '换标退回重做 ·' )) !
expect ( dialog ). toBeDefined ()
await dialog . get ( 'textarea' ). setValue ( '保留重做原因' )
await dialog . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' )
expect (( dialog . get ( 'textarea' ). element as HTMLTextAreaElement ). value ). toBe ( '保留重做原因' )
expect ( dialog . findComponent ({ name : 'ElSelect' }). props ( 'modelValue' )). toBe ( 'RL-TEST-1' )
})
2026-08-27 15:56:13 +08:00
})
2026-10-10 14:58:15 +08:00
describe ( 'tail-leg pallet merge toolbar' , () => {
beforeEach (() => {
canAccess . mockImplementation (() => true )
vi . mocked ( listLastMileOrders ). mockResolvedValue ({ records : [], page : 1 , size : 10 , total : 0 , pages : 0 })
vi . mocked ( updateLastMileOrderPalletMerge ). mockReset (). mockResolvedValue ( null )
})
it ( 'submits the selected group and cancellation with the customer scope' , async () => {
vi . spyOn ( ElMessageBox , 'confirm' ). mockResolvedValue ( 'confirm' as Awaited < ReturnType < typeof ElMessageBox.confirm >>)
const wrapper = await mountView ()
const row = { id : '2092873463568596994' , customerId , currentStatusCode : 'PENDING_ASSIGNMENT' , palletMergeFlag : '' }
expect ( wrapper . findAll ( 'button' ). find ( b => b . text () === '合托' ) ! . attributes ( 'disabled' )). toBeDefined ()
await selectRows ( wrapper , [ row ])
await wrapper . findAll ( 'button' ). find ( b => b . text () === '合托' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( updateLastMileOrderPalletMerge ). toHaveBeenLastCalledWith ([ row . id ], customerId , true )
await selectRows ( wrapper , [{ ... row , palletMergeFlag : 'REQUIRED' }])
await wrapper . findAll ( 'button' ). find ( b => b . text () === '取消合托' ) ! . trigger ( 'click' )
await flushPromises ()
expect ( updateLastMileOrderPalletMerge ). toHaveBeenLastCalledWith ([ row . id ], customerId , false )
})
it ( 'blocks completed and mixed-customer selections and hides unauthorized actions' , async () => {
const wrapper = await mountView ()
for ( const rows of [
[{ id : '1' , customerId , currentStatusCode : 'INBOUND_COMPLETED' , palletMergeFlag : 'REQUIRED' }],
[{ id : '1' , customerId , palletMergeFlag : 'MERGED' }],
[{ id : '1' , customerId }, { id : '2' , customerId : 'other' }],
]) {
await selectRows ( wrapper , rows )
expect ( wrapper . findAll ( 'button' ). find ( b => b . text () === '合托' ) ! . attributes ( 'disabled' )). toBeDefined ()
expect ( wrapper . findAll ( 'button' ). find ( b => b . text () === '取消合托' ) ! . attributes ( 'disabled' )). toBeDefined ()
}
canAccess . mockImplementation (() => false )
const denied = await mountView ()
expect ( denied . findAll ( 'button' ). some ( b => b . text () === '合托' || b . text () === '取消合托' )). toBe ( false )
})
})