2026-08-26 12:04:50 +08:00
import ElementPlus , { ElMessageBox } from 'element-plus'
2026-08-26 11:10:48 +08:00
import { flushPromises , mount , type VueWrapper } from '@vue/test-utils'
import { beforeEach , describe , expect , it , vi } from 'vitest'
import ContainerDetailView from './ContainerDetailView.vue'
2026-08-27 15:02:41 +08:00
import { completeContainerClearance , downloadContainerClearance , downloadContainerDocument , getContainer , getContainerClearance , getContainerOverview , uploadContainerClearance , uploadContainerDocument } from '../api/platformContainerLoading'
2026-08-26 13:06:00 +08:00
import { completeLoadingDeclaration , downloadLoadingDeclaration , getLoadingDeclaration , uploadLoadingDeclaration } from '@/modules/loading-lists'
2026-08-26 12:04:50 +08:00
import { confirmContainerOutboundDispatch , getContainerOutboundDispatch } from '@/modules/outbound-dispatch/api/platformOutboundDispatch'
import type { ContainerOutboundDispatchDetail } from '@/modules/outbound-dispatch/types'
2026-08-26 12:46:46 +08:00
import { ApiResponseError } from '@/shared/api/http'
2026-08-18 17:40:40 +08:00
2026-08-26 12:04:50 +08:00
const { accessCan , routerPush , carrierMounts } = vi . hoisted (() => ({ accessCan : vi.fn (), routerPush : vi.fn (), carrierMounts : { value : 0 } }))
2026-08-26 11:10:48 +08:00
vi . mock ( 'vue-router' , () => ({ useRoute : () => ({ params : { containerId : '2092438843630391297' }, query : {} }), useRouter : () => ({ push : routerPush }) }))
vi . mock ( '@/core/access/accessPreview' , () => ({ useAccessPreviewStore : () => ({ can : accessCan }) }))
2026-08-26 12:04:50 +08:00
vi . mock ( '@/modules/port-schedules' , () => ({ CarrierScheduleQueryPanel : { template : '<div data-test="carrier-panel" />' , mounted : () => { carrierMounts . value += 1 } } }))
2026-08-26 13:06:00 +08:00
vi . mock ( '@/modules/loading-lists' , () => ({ uploadLoadingDeclaration : vi.fn (), completeLoadingDeclaration : vi.fn (), downloadLoadingDeclaration : vi.fn (), getLoadingDeclaration : vi.fn () }))
2026-08-26 12:04:50 +08:00
vi . mock ( '@/modules/outbound-dispatch/api/platformOutboundDispatch' , () => ({ getContainerOutboundDispatch : vi.fn (), confirmContainerOutboundDispatch : vi.fn () }))
2026-08-26 11:10:48 +08:00
vi . mock ( '../api/platformContainerLoading' , () => ({
getContainer : vi.fn (), getContainerOverview : vi.fn (), getContainerClearance : vi.fn (),
uploadContainerClearance : vi.fn (), completeContainerClearance : vi.fn (), uploadContainerDocument : vi.fn (),
downloadContainerDocument : vi.fn (), downloadContainerPdaPhoto : vi.fn (), markContainerException : vi.fn (), cancelContainerException : vi.fn (),
2026-08-26 13:06:00 +08:00
downloadContainerClearance : vi.fn (),
2026-08-26 11:10:48 +08:00
}))
const detail = { id : '2092438843630391297' , planId : '2092438843630391401' , planNo : 'LP-260825-0181' , bookingNo : 'CB-1' , cabinetNo : 'CAB-1' , containerNo : 'C11111111' , warehouseId : '193000000000200002' , warehouseName : '东莞仓' , containerType : '45HC' , planStatus : 'COMPLETED' , version : 8 } as never
const overview = {
container : { id : '2092438843630391297' , bookingNo : 'CB-1' , cabinetNo : 'CAB-1' , containerNo : 'C11111111' , status : 'COMPLETED' , warehouseId : '193000000000200002' , warehouseName : '东莞仓' , voyageNo : null , vesselName : null , version : 8 },
loadingPlanId : '2092438843630391401' , pda : null ,
documents : [{ id : '2092438843630391501' , documentType : 'CUSTOMS_DECLARATION' , fileId : '2092438843630391502' , fileName : 'old.pdf' , fileHash : null , uploadedBy : '1' , uploadedByName : '操作员' , uploadedAt : '2026-08-26T10:00:00' , version : 5 , completed : false }],
exception : { active : false , reason : null , markedBy : null , markedByName : null , markedAt : null }, exceptionHistory : [], timeline : [],
}
2026-08-26 12:04:50 +08:00
const outboundDetail = {
aggregate : {
containerBookingId : '2092438843630391297' , cabinetNo : 'CAB-1' , containerNo : 'C11111111' ,
2026-08-26 12:36:01 +08:00
loadingPlanId : '2092438843630391401' , loadingPlanStatus : 'COMPLETED' , truckAppointmentStatus : 'ARRIVED' , dispatchStatus : 'READY_FOR_DISPATCH' , version : 4 ,
2026-08-26 12:04:50 +08:00
inbounds : [{ orderId : '11' , orderNo : 'O-1' , inboundId : '2092438843630391501' , inboundCode : 'IN-1' , tenantId : '22' , inboundStatus : 'LOADED' }],
gate : { passed : true , checkedAt : '2026-08-26T12:00:00' , inbounds : [{ inboundId : '2092438843630391501' , inboundCode : 'IN-1' , gate : { passed : true , checkedAt : '2026-08-26T12:00:00' , items : [{ code : 'CUSTOMS_CLEARANCE' , name : '报关协同完成' , passed : true , actual : 'COMPLETED' , requirement : 'COMPLETED' }] } }] },
},
timeline : [],
} as ContainerOutboundDispatchDetail
2026-08-26 11:10:48 +08:00
2026-08-26 12:04:50 +08:00
function mountView ( customs = true , outbound = true ) {
accessCan . mockImplementation (( permission : string ) => permission === 'platform:firstleg:container:edit'
|| ( customs && permission === 'platform:firstleg:customs:manage' )
2026-08-26 13:00:51 +08:00
|| permission === 'platform:firstleg:loading-list:list'
2026-08-26 12:04:50 +08:00
|| ( outbound && [ 'platform:firstleg:outbound-dispatch:view' , 'platform:firstleg:outbound-dispatch:confirm' ]. includes ( permission )))
2026-08-26 11:10:48 +08:00
return mount ( ContainerDetailView , { global : { plugins : [ ElementPlus ] } })
}
2026-08-26 13:00:51 +08:00
function documentArticle ( wrapper : VueWrapper , label : string ) { return wrapper . findAll ( '.document-table tbody tr' ). find (( item ) => item . text (). includes ( label )) ! }
2026-08-26 12:46:46 +08:00
function documentButton ( wrapper : VueWrapper , label : string , buttonText : string ) { return documentArticle ( wrapper , label ). findAll ( 'button' ). find (( button ) => button . text (). includes ( buttonText )) ! }
2026-08-27 15:02:41 +08:00
function deferred < T >() {
let resolve !: ( value : T ) => void
const promise = new Promise < T >(( resolvePromise ) => { resolve = resolvePromise })
return { promise , resolve }
}
type ClearanceVm = { clearanceOpen : boolean ; clearanceFile : File | null ; clearanceError : { code? : string ; requestId? : string } | null ; documentRefreshWarning : unknown ; selectClearanceFile : ( file : { raw : File }) => void ; uploadClearance : () => Promise < void >; completeClearance : () => Promise < void > }
function clearanceVm ( wrapper : VueWrapper ) { return wrapper . vm as unknown as ClearanceVm }
2026-08-28 09:19:24 +08:00
type DocumentVm = { documentOpen : boolean ; documentFile : File | null ; documentError : { code? : string ; requestId? : string } | null ; documentUploadKey : number ; documentRefreshWarning : { code? : string } | null ; selectDocumentFile : ( file : { raw : File }) => void ; saveDocument : () => Promise < void > }
function documentVm ( wrapper : VueWrapper ) { return wrapper . vm as unknown as DocumentVm }
2026-08-18 17:40:40 +08:00
describe ( 'ContainerDetailView' , () => {
2026-08-26 11:10:48 +08:00
beforeEach (() => {
vi . clearAllMocks ()
2026-08-26 13:00:51 +08:00
Object . defineProperty ( URL , 'createObjectURL' , { configurable : true , value : vi.fn (() => 'blob:document' ) })
Object . defineProperty ( URL , 'revokeObjectURL' , { configurable : true , value : vi.fn () })
2026-08-26 12:04:50 +08:00
carrierMounts . value = 0
2026-08-26 11:10:48 +08:00
vi . mocked ( getContainer ). mockResolvedValue ( detail )
vi . mocked ( getContainerOverview ). mockResolvedValue ( overview as never )
vi . mocked ( getContainerClearance ). mockResolvedValue ( null )
2026-08-26 13:00:51 +08:00
vi . mocked ( uploadLoadingDeclaration ). mockResolvedValue ({ id : '2092438843630391501' , documentType : 'CUSTOMS_DECLARATION' , fileId : '2092438843630391502' , fileName : 'customs-new.pdf' , fileHash : 'hash' , uploadedBy : '1' , uploadedByName : '操作员' , uploadedAt : '2026-08-26T12:00:00' , completedBy : null , completedByName : null , completedAt : null , version : 6 } as never )
2026-08-26 12:46:46 +08:00
vi . mocked ( completeLoadingDeclaration ). mockResolvedValue ({} as never )
2026-08-26 13:00:51 +08:00
vi . mocked ( downloadLoadingDeclaration ). mockResolvedValue ( new Blob ([ 'customs' ]))
2026-08-26 13:06:00 +08:00
vi . mocked ( downloadContainerClearance ). mockResolvedValue ( new Blob ([ 'clearance' ]))
vi . mocked ( getLoadingDeclaration ). mockResolvedValue ( null )
2026-08-26 11:10:48 +08:00
vi . mocked ( uploadContainerDocument ). mockResolvedValue ({} as never )
vi . mocked ( uploadContainerClearance ). mockResolvedValue ({} as never )
2026-08-26 12:04:50 +08:00
vi . mocked ( getContainerOutboundDispatch ). mockResolvedValue ( outboundDetail )
vi . mocked ( confirmContainerOutboundDispatch ). mockResolvedValue ({ ... outboundDetail , aggregate : { ... outboundDetail . aggregate , dispatchStatus : 'WAITING_FOR_LOADING' , version : 5 } } as never )
2026-08-18 17:40:40 +08:00
})
2026-08-26 11:10:48 +08:00
it ( 'opens domestic export customs upload and sends stable plan, warehouse, version and reason' , async () => {
const wrapper = mountView (); await flushPromises ()
2026-08-26 13:00:51 +08:00
await documentButton ( wrapper , '国内出口报关附件' , '更新附件' ). trigger ( 'click' ); await flushPromises ()
2026-08-26 11:10:48 +08:00
expect ( wrapper . text ()). toContain ( '国内出口报关附件' )
const file = new File ([ 'customs' ], 'customs.pdf' , { type : 'application/pdf' })
;( wrapper . vm as unknown as { selectDocumentFile : ( file : { raw : File }) => void }). selectDocumentFile ({ raw : file })
await ( wrapper . vm as unknown as { saveDocument : () => Promise < void > }). saveDocument ()
expect ( uploadLoadingDeclaration ). toHaveBeenCalledWith ( '2092438843630391401' , '193000000000200002' , file , 5 , '上传国内出口报关附件' )
expect ( uploadContainerDocument ). not . toHaveBeenCalled (); expect ( uploadContainerClearance ). not . toHaveBeenCalled ()
wrapper . unmount ()
2026-08-18 17:40:40 +08:00
})
2026-08-19 16:45:51 +08:00
2026-08-26 11:10:48 +08:00
it ( 'shows a readable error when the container has no loading plan' , async () => {
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ({ ... overview , loadingPlanId : null } as never )
const wrapper = mountView (); await flushPromises ()
2026-08-26 13:00:51 +08:00
await documentButton ( wrapper , '国内出口报关附件' , '更新附件' ). trigger ( 'click' ); await flushPromises ()
2026-08-26 11:10:48 +08:00
expect ( wrapper . text ()). toContain ( '当前货柜没有装柜清单,无法上传国内出口报关附件' )
expect ( uploadLoadingDeclaration ). not . toHaveBeenCalled (); wrapper . unmount ()
2026-08-19 16:45:51 +08:00
})
2026-08-20 18:02:52 +08:00
2026-08-26 11:10:48 +08:00
it ( 'hides customs and clearance upload without customs manage permission while keeping DO and PU' , async () => {
const wrapper = mountView ( false ); await flushPromises ()
2026-08-26 13:00:51 +08:00
expect ( documentArticle ( wrapper , '国内出口报关附件' ). findAll ( 'button' ). map (( button ) => button . text ())). toEqual ([ '下载' ])
2026-08-26 11:10:48 +08:00
expect ( documentArticle ( wrapper , '目的港清关资料' ). find ( 'button' ). exists ()). toBe ( false )
expect ( documentArticle ( wrapper , 'DO 资料' ). find ( 'button' ). exists ()). toBe ( true )
expect ( documentArticle ( wrapper , 'PU 资料' ). find ( 'button' ). exists ()). toBe ( true )
wrapper . unmount ()
})
it ( 'keeps destination clearance on its independent container clearance API' , async () => {
const wrapper = mountView (); await flushPromises ()
await documentArticle ( wrapper , '目的港清关资料' ). find ( 'button' ). trigger ( 'click' ); await flushPromises ()
expect ( getContainerClearance ). toHaveBeenCalledWith ( '2092438843630391297' )
expect ( wrapper . text ()). toContain ( '目的港清关资料' ); expect ( uploadLoadingDeclaration ). not . toHaveBeenCalled ()
wrapper . unmount ()
2026-08-20 18:02:52 +08:00
})
2026-08-26 12:04:50 +08:00
2026-08-26 13:00:51 +08:00
it ( 'downloads customs through the dedicated loading-list API and keeps DO PU isolated' , async () => {
const click = vi . spyOn ( HTMLAnchorElement . prototype , 'click' ). mockImplementation (() => {})
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '国内出口报关附件' , '下载' ). trigger ( 'click' ); await flushPromises ()
expect ( downloadLoadingDeclaration ). toHaveBeenCalledWith ( '2092438843630391401' , '193000000000200002' , '2092438843630391502' )
expect ( downloadContainerDocument ). not . toHaveBeenCalled ()
expect ( URL . createObjectURL ). toHaveBeenCalledWith ( expect . any ( Blob ))
expect ( click ). toHaveBeenCalledOnce ()
expect ( URL . revokeObjectURL ). toHaveBeenCalled ()
wrapper . unmount ()
})
it ( 'keeps customs download after completion while hiding update and completion actions' , async () => {
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ({ ... overview , documents : overview.documents.map (( item ) => ({ ... item , completed : true })) } as never )
const wrapper = mountView (); await flushPromises ()
const row = documentArticle ( wrapper , '国内出口报关附件' )
expect ( row . findAll ( 'button' ). map (( button ) => button . text ())). toEqual ([ '下载' ])
expect ( row . text ()). toContain ( '已完成' )
wrapper . unmount ()
})
it ( 'keeps the optimistic customs upload when overview refresh fails and shows a readable warning' , async () => {
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ( overview as never ). mockRejectedValueOnce ( new Error ( 'overview unavailable' ))
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '国内出口报关附件' , '更新附件' ). trigger ( 'click' ); await flushPromises ()
const file = new File ([ 'customs-new' ], 'customs-new.pdf' , { type : 'application/pdf' })
;( wrapper . vm as unknown as { selectDocumentFile : ( file : { raw : File }) => void }). selectDocumentFile ({ raw : file })
await ( wrapper . vm as unknown as { saveDocument : () => Promise < void > }). saveDocument (); await flushPromises ()
const row = documentArticle ( wrapper , '国内出口报关附件' )
expect ( row . text ()). toContain ( 'customs-new.pdf' )
expect ( row . text ()). toContain ( '操作员' )
expect ( row . text ()). toContain ( '2026-08-26 12:00' )
expect ( wrapper . text ()). toContain ( '附件已上传,但最新资料刷新失败;当前已保留本次上传结果' )
expect ( uploadLoadingDeclaration ). toHaveBeenCalledWith ( '2092438843630391401' , '193000000000200002' , file , 5 , '上传国内出口报关附件' )
wrapper . unmount ()
})
2026-08-26 13:06:00 +08:00
it ( 'refreshes a hidden customs version after A0409 and preserves error evidence without auto retry' , async () => {
const hidden = { id : '2092438843630392501' , documentType : 'CUSTOMS_DECLARATION' , fileId : '2092438843630392502' , fileName : 'server-customs.pdf' , fileHash : 'server-hash' , uploadedBy : '2' , uploadedByName : '服务端操作员' , uploadedAt : '2026-08-26T12:30:00' , completedBy : null , completedByName : null , completedAt : null , version : 7 }
const withoutCustoms = { ... overview , documents : [] }
vi . mocked ( getContainerOverview ). mockResolvedValue ( withoutCustoms as never )
vi . mocked ( getLoadingDeclaration ). mockResolvedValueOnce ( hidden as never )
vi . mocked ( uploadLoadingDeclaration ). mockRejectedValueOnce ( new ApiResponseError ( '版本冲突' , 'A0409' , undefined , undefined , 'request-upload-409' , 'trace-upload-409' ))
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '国内出口报关附件' , '上传附件' ). trigger ( 'click' ); await flushPromises ()
const file = new File ([ 'next' ], 'next.pdf' , { type : 'application/pdf' })
;( wrapper . vm as unknown as { selectDocumentFile : ( file : { raw : File }) => void }). selectDocumentFile ({ raw : file })
await ( wrapper . vm as unknown as { saveDocument : () => Promise < void > }). saveDocument (); await flushPromises ()
expect ( uploadLoadingDeclaration ). toHaveBeenCalledOnce ()
expect ( uploadLoadingDeclaration ). toHaveBeenCalledWith ( '2092438843630391401' , '193000000000200002' , file , 0 , '上传国内出口报关附件' )
expect ( getLoadingDeclaration ). toHaveBeenCalledWith ( '2092438843630391401' , '193000000000200002' )
expect ( documentArticle ( wrapper , '国内出口报关附件' ). text ()). toContain ( 'server-customs.pdf' )
expect ( documentArticle ( wrapper , '国内出口报关附件' ). text ()). toContain ( '服务端操作员' )
expect ( wrapper . text ()). toContain ( 'A0409' )
expect ( wrapper . text ()). toContain ( 'request-upload-409' )
wrapper . unmount ()
})
2026-08-27 15:02:41 +08:00
it ( 'closes immediately after a valid destination upload response, clears dialog state and refreshes after optimistic table update' , async () => {
2026-08-26 13:06:00 +08:00
const existing = { id : '301' , documentType : 'DESTINATION_CLEARANCE' , fileId : '302' , fileName : 'clearance-v1.pdf' , fileHash : null , uploadedBy : '3' , uploadedByName : '清关员' , uploadedAt : '2026-08-26T11:00:00' , completedBy : null , completedByName : null , completedAt : null , version : 1 }
const updated = { ... existing , fileId : '303' , fileName : 'clearance-v2.pdf' , uploadedAt : '2026-08-26T13:00:00' , version : 2 }
2026-08-27 15:02:41 +08:00
const refresh = deferred < typeof overview >()
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ( overview as never ). mockReturnValueOnce ( refresh . promise as never )
2026-08-26 13:06:00 +08:00
vi . mocked ( getContainerClearance ). mockResolvedValueOnce ( existing as never )
vi . mocked ( uploadContainerClearance ). mockResolvedValueOnce ( updated as never )
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '目的港清关资料' , '上传附件' ). trigger ( 'click' ); await flushPromises ()
const file = new File ([ 'clearance' ], 'clearance-v2.pdf' , { type : 'application/pdf' })
2026-08-27 15:02:41 +08:00
clearanceVm ( wrapper ). selectClearanceFile ({ raw : file })
const request = clearanceVm ( wrapper ). uploadClearance (); await flushPromises ()
2026-08-26 13:06:00 +08:00
expect ( uploadContainerClearance ). toHaveBeenCalledWith ( '2092438843630391297' , file , 1 , '上传目的港清关资料' )
2026-08-27 15:02:41 +08:00
expect ( clearanceVm ( wrapper ). clearanceOpen ). toBe ( false )
expect ( clearanceVm ( wrapper ). clearanceFile ). toBeNull ()
expect ( clearanceVm ( wrapper ). clearanceError ). toBeNull ()
2026-08-26 13:06:00 +08:00
expect ( documentArticle ( wrapper , '目的港清关资料' ). text ()). toContain ( 'clearance-v2.pdf' )
2026-08-27 15:02:41 +08:00
refresh . resolve ( overview )
await request ; await flushPromises ()
expect ( clearanceVm ( wrapper ). documentRefreshWarning ). toBeNull ()
2026-08-26 13:06:00 +08:00
wrapper . unmount ()
2026-08-28 09:19:24 +08:00
})
it . each ([
[ 'DELIVERY_ORDER' , 'DO 资料' ],
[ 'PICKUP_ORDER' , 'PU 资料' ],
] as const )( 'optimistically replaces %s, closes before overview refresh and preserves the returned file over a stale overview' , async ( documentType , label ) => {
const existing = { id : `old- ${ documentType } ` , documentType , fileId : `old-file- ${ documentType } ` , fileName : `old- ${ documentType } .pdf` , fileHash : null , uploadedBy : '1' , uploadedByName : '旧操作员' , uploadedAt : '2026-08-26T10:00:00' , version : 1 , completed : false }
const initial = { ... overview , documents : [... overview . documents , existing ] }
const uploaded = { id : `new- ${ documentType } ` , documentType , fileId : `new-file- ${ documentType } ` , fileName : `new- ${ documentType } .pdf` , fileHash : 'hash' , uploadedBy : '2' , uploadedByName : '新操作员' , uploadedAt : '2026-08-27T13:00:00' , completedBy : null , completedByName : null , completedAt : null , version : 2 }
const refresh = deferred < typeof initial >()
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ( initial as never ). mockReturnValueOnce ( refresh . promise as never )
vi . mocked ( uploadContainerDocument ). mockResolvedValueOnce ( uploaded as never )
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , label , '更新附件' ). trigger ( 'click' ); await flushPromises ()
const file = new File ([ documentType ], `new- ${ documentType } .pdf` , { type : 'application/pdf' })
documentVm ( wrapper ). selectDocumentFile ({ raw : file })
const request = documentVm ( wrapper ). saveDocument (); await flushPromises ()
expect ( uploadContainerDocument ). toHaveBeenCalledWith ( '2092438843630391297' , documentType , file , 1 , `上传 ${ label } ` )
expect ( documentVm ( wrapper ). documentOpen ). toBe ( false )
expect ( documentVm ( wrapper ). documentFile ). toBeNull ()
expect ( documentArticle ( wrapper , label ). text ()). toContain ( `new- ${ documentType } .pdf` )
expect ( documentArticle ( wrapper , label ). text ()). toContain ( '新操作员' )
refresh . resolve ( initial ); await request ; await flushPromises ()
expect ( documentArticle ( wrapper , label ). text ()). toContain ( `new- ${ documentType } .pdf` )
expect ( documentVm ( wrapper ). documentRefreshWarning ). toMatchObject ({ code : 'DOCUMENT_OVERVIEW_STALE' })
wrapper . unmount ()
})
it ( 'rebuilds the upload picker on reopen and keeps DO failures open with the selected file and API evidence' , async () => {
const existing = { id : 'do-old' , documentType : 'DELIVERY_ORDER' , fileId : 'do-file-old' , fileName : 'old-do.pdf' , fileHash : null , uploadedBy : '1' , uploadedByName : '旧操作员' , uploadedAt : '2026-08-26T10:00:00' , version : 1 , completed : false }
vi . mocked ( getContainerOverview ). mockResolvedValue ({ ... overview , documents : [... overview . documents , existing ] } as never )
vi . mocked ( uploadContainerDocument )
. mockRejectedValueOnce ( new ApiResponseError ( '版本冲突' , 'A0409' , undefined , undefined , 'request-do-409' , 'trace-do-409' ))
. mockResolvedValueOnce ( null )
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , 'DO 资料' , '更新附件' ). trigger ( 'click' ); await flushPromises ()
const firstKey = documentVm ( wrapper ). documentUploadKey
const file = new File ([ 'do' ], 'next-do.pdf' , { type : 'application/pdf' })
documentVm ( wrapper ). selectDocumentFile ({ raw : file })
await documentVm ( wrapper ). saveDocument (); await flushPromises ()
expect ( documentVm ( wrapper ). documentOpen ). toBe ( true )
expect ( documentVm ( wrapper ). documentFile ). toBe ( file )
expect ( documentVm ( wrapper ). documentError ). toMatchObject ({ code : 'A0409' , requestId : 'request-do-409' })
expect ( wrapper . text ()). toContain ( 'trace-do-409' )
documentVm ( wrapper ). documentOpen = false ; await flushPromises ()
await documentButton ( wrapper , 'PU 资料' , '上传附件' ). trigger ( 'click' ); await flushPromises ()
expect ( documentVm ( wrapper ). documentUploadKey ). toBeGreaterThan ( firstKey )
expect ( documentVm ( wrapper ). documentFile ). toBeNull ()
expect ( documentVm ( wrapper ). documentError ). toBeNull ()
expect ( wrapper . text ()). not . toContain ( 'request-do-409' )
const puFile = new File ([ 'pu' ], 'next-pu.pdf' , { type : 'application/pdf' })
documentVm ( wrapper ). selectDocumentFile ({ raw : puFile })
await documentVm ( wrapper ). saveDocument (); await flushPromises ()
expect ( documentVm ( wrapper ). documentOpen ). toBe ( true )
expect ( documentVm ( wrapper ). documentFile ). toBe ( puFile )
expect ( uploadContainerDocument ). toHaveBeenCalledTimes ( 2 )
wrapper . unmount ()
2026-08-26 13:06:00 +08:00
})
it ( 'downloads destination clearance through its dedicated endpoint and keeps completed rows download-only' , async () => {
const completed = { id : '351' , documentType : 'DESTINATION_CLEARANCE' , fileId : '2092438843630393502' , fileName : 'destination-clearance.pdf' , fileHash : null , uploadedBy : '3' , uploadedByName : '清关员' , uploadedAt : '2026-08-26T11:00:00' , version : 2 , completed : true }
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ({ ... overview , documents : [... overview . documents , completed ] } as never )
const click = vi . spyOn ( HTMLAnchorElement . prototype , 'click' ). mockImplementation (() => {})
const wrapper = mountView (); await flushPromises ()
const row = documentArticle ( wrapper , '目的港清关资料' )
expect ( row . findAll ( 'button' ). map (( button ) => button . text ())). toEqual ([ '下载' ])
await documentButton ( wrapper , '目的港清关资料' , '下载' ). trigger ( 'click' ); await flushPromises ()
expect ( downloadContainerClearance ). toHaveBeenCalledWith ( '2092438843630391297' , '2092438843630393502' )
expect ( downloadLoadingDeclaration ). not . toHaveBeenCalled ()
expect ( downloadContainerDocument ). not . toHaveBeenCalled ()
expect ( click ). toHaveBeenCalledOnce ()
wrapper . unmount ()
})
it ( 'refreshes destination clearance after A0409 and preserves the server file and error metadata' , async () => {
const existing = { id : '401' , documentType : 'DESTINATION_CLEARANCE' , fileId : '402' , fileName : 'clearance-v1.pdf' , fileHash : null , uploadedBy : '3' , uploadedByName : '清关员' , uploadedAt : '2026-08-26T11:00:00' , completedBy : null , completedByName : null , completedAt : null , version : 1 }
const server = { ... existing , fileId : '403' , fileName : 'server-clearance-v2.pdf' , uploadedByName : '服务端清关员' , version : 2 }
2026-08-27 15:02:41 +08:00
vi . mocked ( getContainerClearance ). mockResolvedValueOnce ( existing as never ). mockResolvedValueOnce ( server as never ). mockResolvedValueOnce ( server as never )
2026-08-26 13:06:00 +08:00
vi . mocked ( uploadContainerClearance ). mockRejectedValueOnce ( new ApiResponseError ( '版本冲突' , 'A0409' , undefined , undefined , 'request-clearance-409' , 'trace-clearance-409' ))
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '目的港清关资料' , '上传附件' ). trigger ( 'click' ); await flushPromises ()
const file = new File ([ 'clearance' ], 'clearance-next.pdf' , { type : 'application/pdf' })
2026-08-27 15:02:41 +08:00
clearanceVm ( wrapper ). selectClearanceFile ({ raw : file })
await clearanceVm ( wrapper ). uploadClearance (); await flushPromises ()
2026-08-26 13:06:00 +08:00
expect ( uploadContainerClearance ). toHaveBeenCalledOnce ()
2026-08-27 15:02:41 +08:00
expect ( clearanceVm ( wrapper ). clearanceOpen ). toBe ( true )
expect ( clearanceVm ( wrapper ). clearanceFile ). toBe ( file )
expect ( clearanceVm ( wrapper ). clearanceError ). toMatchObject ({ code : 'A0409' , requestId : 'request-clearance-409' })
2026-08-26 13:06:00 +08:00
expect ( documentArticle ( wrapper , '目的港清关资料' ). text ()). toContain ( 'server-clearance-v2.pdf' )
expect ( wrapper . text ()). toContain ( 'A0409' )
expect ( wrapper . text ()). toContain ( 'request-clearance-409' )
2026-08-27 15:02:41 +08:00
clearanceVm ( wrapper ). clearanceOpen = false ; await flushPromises ()
await documentButton ( wrapper , '目的港清关资料' , '更新附件' ). trigger ( 'click' ); await flushPromises ()
expect ( clearanceVm ( wrapper ). clearanceOpen ). toBe ( true )
expect ( clearanceVm ( wrapper ). clearanceFile ). toBeNull ()
expect ( clearanceVm ( wrapper ). clearanceError ). toBeNull ()
expect ( wrapper . text ()). not . toContain ( 'request-clearance-409' )
wrapper . unmount ()
})
it ( 'keeps the destination dialog and selected file for network and empty-response upload failures' , async () => {
const existing = { id : '501' , documentType : 'DESTINATION_CLEARANCE' , fileId : '502' , fileName : 'clearance-v1.pdf' , fileHash : null , uploadedBy : '3' , uploadedByName : '清关员' , uploadedAt : '2026-08-26T11:00:00' , completedBy : null , completedByName : null , completedAt : null , version : 1 }
vi . mocked ( getContainerClearance ). mockResolvedValueOnce ( existing as never )
vi . mocked ( uploadContainerClearance )
. mockRejectedValueOnce ( new ApiResponseError ( '网络请求失败' , 'NETWORK_ERROR' , undefined , undefined , 'request-network' , 'trace-network' ))
. mockResolvedValueOnce ( null )
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '目的港清关资料' , '上传附件' ). trigger ( 'click' ); await flushPromises ()
const file = new File ([ 'clearance' ], 'clearance-next.pdf' , { type : 'application/pdf' })
clearanceVm ( wrapper ). selectClearanceFile ({ raw : file })
await clearanceVm ( wrapper ). uploadClearance (); await flushPromises ()
expect ( clearanceVm ( wrapper ). clearanceOpen ). toBe ( true )
expect ( clearanceVm ( wrapper ). clearanceFile ). toBe ( file )
expect ( wrapper . text ()). toContain ( 'request-network' )
await clearanceVm ( wrapper ). uploadClearance (); await flushPromises ()
expect ( clearanceVm ( wrapper ). clearanceOpen ). toBe ( true )
expect ( clearanceVm ( wrapper ). clearanceFile ). toBe ( file )
expect ( uploadContainerClearance ). toHaveBeenCalledTimes ( 2 )
wrapper . unmount ()
})
it ( 'closes after successful destination clearance completion but keeps the dialog open on completion failure' , async () => {
const existing = { id : '601' , documentType : 'DESTINATION_CLEARANCE' , fileId : '602' , fileName : 'clearance.pdf' , fileHash : null , uploadedBy : '3' , uploadedByName : '清关员' , uploadedAt : '2026-08-26T11:00:00' , completedBy : null , completedByName : null , completedAt : null , version : 1 }
const completed = { ... existing , completedBy : '4' , completedByName : '复核员' , completedAt : '2026-08-27T12:00:00' , version : 2 }
const refresh = deferred < typeof overview >()
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ( overview as never ). mockReturnValueOnce ( refresh . promise as never )
vi . mocked ( getContainerClearance ). mockResolvedValueOnce ( existing as never )
vi . mocked ( completeContainerClearance ). mockResolvedValueOnce ( completed as never )
let wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '目的港清关资料' , '上传附件' ). trigger ( 'click' ); await flushPromises ()
const request = clearanceVm ( wrapper ). completeClearance (); await flushPromises ()
expect ( clearanceVm ( wrapper ). clearanceOpen ). toBe ( false )
expect ( clearanceVm ( wrapper ). clearanceError ). toBeNull ()
expect ( documentArticle ( wrapper , '目的港清关资料' ). text ()). toContain ( '已完成' )
refresh . resolve ( overview ); await request ; await flushPromises ()
wrapper . unmount ()
vi . mocked ( getContainerOverview ). mockResolvedValue ( overview as never )
vi . mocked ( getContainerClearance ). mockResolvedValueOnce ( existing as never )
vi . mocked ( completeContainerClearance ). mockRejectedValueOnce ( new ApiResponseError ( '完成失败' , 'A0409' , undefined , undefined , 'request-complete-409' , 'trace-complete-409' ))
wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '目的港清关资料' , '上传附件' ). trigger ( 'click' ); await flushPromises ()
await clearanceVm ( wrapper ). completeClearance (); await flushPromises ()
expect ( clearanceVm ( wrapper ). clearanceOpen ). toBe ( true )
expect ( clearanceVm ( wrapper ). clearanceError ). toMatchObject ({ code : 'A0409' , requestId : 'request-complete-409' })
expect ( wrapper . text ()). toContain ( 'trace-complete-409' )
2026-08-26 13:06:00 +08:00
wrapper . unmount ()
})
2026-08-26 12:46:46 +08:00
it ( 'keeps customs attachment upload and completion as two explicit steps with exact payload' , async () => {
const completedOverview = { ... overview , documents : overview.documents.map (( item ) => ({ ... item , completed : true })) }
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ( overview as never ). mockResolvedValue ( completedOverview as never )
vi . spyOn ( ElMessageBox , 'confirm' ). mockResolvedValueOnce ({ action : 'confirm' } as never )
const wrapper = mountView (); await flushPromises ()
const customs = documentArticle ( wrapper , '国内出口报关附件' )
expect ( customs . text ()). toContain ( '附件已上传,待确认报关完成' )
expect ( customs . text ()). toContain ( '更新附件' )
await documentButton ( wrapper , '国内出口报关附件' , '确认报关完成' ). trigger ( 'click' ); await flushPromises ()
expect ( completeLoadingDeclaration ). toHaveBeenCalledOnce ()
expect ( completeLoadingDeclaration ). toHaveBeenCalledWith ( '2092438843630391401' , '193000000000200002' , 5 , '货柜详情确认国内出口报关完成' )
expect ( uploadLoadingDeclaration ). not . toHaveBeenCalled ()
expect ( getContainerOverview ). toHaveBeenCalledTimes ( 2 )
expect ( getContainerOutboundDispatch ). toHaveBeenCalledTimes ( 2 )
expect ( documentArticle ( wrapper , '国内出口报关附件' ). text ()). toContain ( '已确认报关完成' )
expect ( documentButton ( wrapper , '国内出口报关附件' , '确认报关完成' )). toBeUndefined ()
wrapper . unmount ()
})
it ( 'shows customs completion only for an existing unfinished attachment with manage permission' , async () => {
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ({ ... overview , documents : [] } as never )
let wrapper = mountView (); await flushPromises ()
expect ( documentArticle ( wrapper , '国内出口报关附件' ). text ()). toContain ( '第一步上传附件,第二步确认报关完成' )
expect ( documentButton ( wrapper , '国内出口报关附件' , '确认报关完成' )). toBeUndefined ()
wrapper . unmount ()
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ({ ... overview , documents : overview.documents.map (( item ) => ({ ... item , completed : true })) } as never )
wrapper = mountView (); await flushPromises ()
expect ( documentButton ( wrapper , '国内出口报关附件' , '确认报关完成' )). toBeUndefined ()
wrapper . unmount ()
wrapper = mountView ( false ); await flushPromises ()
expect ( documentButton ( wrapper , '国内出口报关附件' , '确认报关完成' )). toBeUndefined ()
wrapper . unmount ()
})
it ( 'shows a readable completion error without a loading plan and does not call the API' , async () => {
vi . mocked ( getContainerOverview ). mockResolvedValueOnce ({ ... overview , loadingPlanId : null } as never )
const wrapper = mountView (); await flushPromises ()
await documentButton ( wrapper , '国内出口报关附件' , '确认报关完成' ). trigger ( 'click' ); await flushPromises ()
expect ( wrapper . text ()). toContain ( '当前货柜没有装柜清单,无法确认报关完成' )
expect ( completeLoadingDeclaration ). not . toHaveBeenCalled ()
expect ( ElMessageBox . confirm ). not . toHaveBeenCalled ()
wrapper . unmount ()
})
it ( 'prevents repeated customs completion and preserves A0409 evidence after refreshing facts' , async () => {
let releasePrompt !: () => void
vi . spyOn ( ElMessageBox , 'confirm' ). mockReturnValueOnce ( new Promise < void >(( resolve ) => { releasePrompt = resolve }) as never )
const wrapper = mountView (); await flushPromises ()
const button = documentButton ( wrapper , '国内出口报关附件' , '确认报关完成' )
await button . trigger ( 'click' ); await button . trigger ( 'click' )
expect ( ElMessageBox . confirm ). toHaveBeenCalledOnce ()
releasePrompt (); await flushPromises ()
vi . mocked ( completeLoadingDeclaration ). mockRejectedValueOnce ( new ApiResponseError ( '数据已变更' , 'A0409' , undefined , undefined , 'request-customs-409' , 'trace-customs-409' ))
vi . mocked ( ElMessageBox . confirm ). mockResolvedValueOnce ({ action : 'confirm' } as never )
await documentButton ( wrapper , '国内出口报关附件' , '确认报关完成' ). trigger ( 'click' ); await flushPromises ()
expect ( completeLoadingDeclaration ). toHaveBeenCalledTimes ( 2 )
expect ( wrapper . text ()). toContain ( 'A0409' )
expect ( wrapper . text ()). toContain ( 'request-customs-409' )
expect ( getContainerOverview ). toHaveBeenCalledTimes ( 3 )
expect ( getContainerOutboundDispatch ). toHaveBeenCalledTimes ( 3 )
wrapper . unmount ()
})
2026-08-26 12:04:50 +08:00
it ( 'loads and displays the whole-container gate without querying an inbound id' , async () => {
const wrapper = mountView (); await flushPromises ()
expect ( getContainerOutboundDispatch ). toHaveBeenCalledOnce ()
expect ( getContainerOutboundDispatch ). toHaveBeenCalledWith ( '2092438843630391297' )
expect ( wrapper . text ()). toContain ( '国内出仓门禁' )
2026-08-26 12:36:01 +08:00
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '总门禁 1' )
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '已通过 1' )
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '阻断 0' )
expect ( wrapper . find ( '.gate-list' ). text ()). not . toContain ( '报关协同完成' )
await wrapper . find ( '.gate-summary button' ). trigger ( 'click' )
expect ( wrapper . find ( '.gate-list' ). text ()). toContain ( '报关协同完成' )
expect ( wrapper . find ( '.gate-list' ). text ()). toContain ( '覆盖 1 个入库码' )
expect ( wrapper . find ( '.gate-list' ). text ()). toContain ( 'IN-1' )
2026-08-26 12:04:50 +08:00
expect ( wrapper . text ()). toContain ( '确认离仓' )
wrapper . unmount ()
})
it ( 'confirms the physical container once with version and no inboundId then refreshes the carrier panel' , async () => {
vi . spyOn ( ElMessageBox , 'confirm' ). mockResolvedValueOnce ({ action : 'confirm' } as never )
const wrapper = mountView (); await flushPromises ()
await wrapper . find ( '.outbound-panel__actions button' ). trigger ( 'click' ); await flushPromises ()
expect ( confirmContainerOutboundDispatch ). toHaveBeenCalledOnce ()
expect ( confirmContainerOutboundDispatch ). toHaveBeenCalledWith ( '2092438843630391297' , { expectedVersion : 4 , remark : '货柜详情确认国内出仓' })
expect ( vi . mocked ( confirmContainerOutboundDispatch ). mock . calls [ 0 ] ? .[ 1 ]). not . toHaveProperty ( 'inboundId' )
expect ( getContainerOverview ). toHaveBeenCalledTimes ( 2 )
expect ( carrierMounts . value ). toBe ( 2 )
expect ( wrapper . text ()). toContain ( '已确认离仓' )
wrapper . unmount ()
})
it ( 'prevents a repeated confirmation wave while the first prompt is open' , async () => {
vi . spyOn ( ElMessageBox , 'confirm' ). mockReturnValueOnce ( new Promise (() => {}) as never )
const wrapper = mountView (); await flushPromises ()
const button = wrapper . find ( '.outbound-panel__actions button' )
await button . trigger ( 'click' )
await button . trigger ( 'click' )
expect ( ElMessageBox . confirm ). toHaveBeenCalledOnce ()
expect ( confirmContainerOutboundDispatch ). not . toHaveBeenCalled ()
wrapper . unmount ()
})
it ( 'hides the outbound gate and never calls its API without view permission' , async () => {
const wrapper = mountView ( true , false ); await flushPromises ()
expect ( wrapper . text ()). not . toContain ( '国内出仓门禁' )
expect ( getContainerOutboundDispatch ). not . toHaveBeenCalled ()
wrapper . unmount ()
})
it ( 'shows gate failure reasons and does not expose confirm when any gate is blocked' , async () => {
vi . mocked ( getContainerOutboundDispatch ). mockResolvedValueOnce ({
... outboundDetail ,
aggregate : {
... outboundDetail . aggregate ,
2026-08-26 12:46:46 +08:00
gate : { passed : false , checkedAt : '2026-08-26T12:00:00' , inbounds : [{ inboundId : '2092438843630391501' , inboundCode : 'IN-1' , gate : { passed : false , checkedAt : '2026-08-26T12:00:00' , items : [{ code : 'CUSTOMS_CLEARANCE' , name : '报关协同完成' , passed : false , actual : 'MISSING' , requirement : 'COMPLETED' }] } }] },
2026-08-26 12:04:50 +08:00
},
} as never )
const wrapper = mountView (); await flushPromises ()
2026-08-26 12:46:46 +08:00
expect ( wrapper . text ()). toContain ( '报关协同完成' )
expect ( wrapper . text ()). toContain ( 'MISSING' )
expect ( wrapper . text ()). toContain ( 'COMPLETED' )
2026-08-26 12:04:50 +08:00
expect ( wrapper . find ( '.outbound-panel__actions button' ). exists ()). toBe ( false )
wrapper . unmount ()
})
2026-08-26 12:36:01 +08:00
it ( 'deduplicates identical gates across inbounds and shows compact coverage counts' , async () => {
const blocked = { code : 'CUSTOMS_CLEARANCE' , name : '报关协同完成' , passed : false , actual : 'MISSING' , requirement : 'COMPLETED' }
2026-08-26 12:46:46 +08:00
const passed = { code : 'LOADING_PLAN_COMPLETED' , name : '柜次装柜计划完成' , passed : true , actual : 'COMPLETED' , requirement : 'COMPLETED' }
2026-08-26 12:36:01 +08:00
vi . mocked ( getContainerOutboundDispatch ). mockResolvedValueOnce ({
... outboundDetail ,
aggregate : {
... outboundDetail . aggregate ,
gate : {
passed : false ,
checkedAt : '2026-08-26T12:00:00' ,
inbounds : [
{ inboundId : '101' , inboundCode : 'IN-101' , gate : { passed : false , checkedAt : '2026-08-26T12:00:00' , items : [ blocked , passed ] } },
{ inboundId : '102' , inboundCode : 'IN-102' , gate : { passed : false , checkedAt : '2026-08-26T12:00:00' , items : [ blocked , passed ] } },
],
},
},
} as never )
const wrapper = mountView (); await flushPromises ()
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '总门禁 2' )
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '已通过 1' )
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '阻断 1' )
expect ( wrapper . findAll ( '.gate-row' )). toHaveLength ( 1 )
expect ( wrapper . find ( '.gate-row' ). text ()). toContain ( '影响 2 个入库码' )
expect ( wrapper . find ( '.gate-row' ). text ()). toContain ( 'IN-101' )
expect ( wrapper . find ( '.gate-row' ). text ()). toContain ( 'IN-102' )
await wrapper . find ( '.gate-summary button' ). trigger ( 'click' )
expect ( wrapper . findAll ( '.gate-row' )). toHaveLength ( 2 )
expect ( wrapper . findAll ( '.gate-row' )[ 1 ] ? . text ()). toContain ( '覆盖 2 个入库码' )
wrapper . unmount ()
})
it ( 'keeps different facts for the same gate code separate and labels the affected inbound' , async () => {
vi . mocked ( getContainerOutboundDispatch ). mockResolvedValueOnce ({
... outboundDetail ,
aggregate : {
... outboundDetail . aggregate ,
gate : {
passed : false ,
checkedAt : '2026-08-26T12:00:00' ,
inbounds : [
{ inboundId : '201' , inboundCode : 'IN-MISSING' , gate : { passed : false , checkedAt : '2026-08-26T12:00:00' , items : [{ code : 'CUSTOMS_CLEARANCE' , name : '报关协同完成' , passed : false , actual : 'MISSING' , requirement : 'COMPLETED' }] } },
{ inboundId : '202' , inboundCode : 'IN-PENDING' , gate : { passed : false , checkedAt : '2026-08-26T12:00:00' , items : [{ code : 'CUSTOMS_CLEARANCE' , name : '报关协同完成' , passed : false , actual : 'PENDING' , requirement : 'COMPLETED' }] } },
{ inboundId : '203' , inboundCode : 'IN-DONE' , gate : { passed : true , checkedAt : '2026-08-26T12:00:00' , items : [{ code : 'CUSTOMS_CLEARANCE' , name : '报关协同完成' , passed : true , actual : 'COMPLETED' , requirement : 'COMPLETED' }] } },
],
},
},
} as never )
const wrapper = mountView (); await flushPromises ()
expect ( wrapper . findAll ( '.gate-row' )). toHaveLength ( 2 )
expect ( wrapper . find ( '.gate-list' ). text ()). toContain ( 'MISSING' )
expect ( wrapper . find ( '.gate-list' ). text ()). toContain ( 'IN-MISSING' )
expect ( wrapper . find ( '.gate-list' ). text ()). toContain ( 'PENDING' )
expect ( wrapper . find ( '.gate-list' ). text ()). toContain ( 'IN-PENDING' )
await wrapper . find ( '.gate-summary button' ). trigger ( 'click' )
expect ( wrapper . findAll ( '.gate-row' )). toHaveLength ( 3 )
expect ( wrapper . find ( '.gate-list' ). text ()). toContain ( 'IN-DONE' )
wrapper . unmount ()
})
2026-08-26 12:46:46 +08:00
it ( 'keeps truck and sailing schedule as reference only and never counts legacy reference gates as blocking' , async () => {
2026-08-26 12:36:01 +08:00
vi . mocked ( getContainerOutboundDispatch ). mockResolvedValueOnce ({
... outboundDetail ,
aggregate : {
... outboundDetail . aggregate ,
truckAppointmentStatus : 'ARRIVED' ,
gate : {
passed : false ,
checkedAt : '2026-08-26T12:00:00' ,
inbounds : [{ inboundId : '301' , inboundCode : 'IN-301' , gate : { passed : false , checkedAt : '2026-08-26T12:00:00' , items : [
{ code : 'TRUCK_DEPARTED' , name : '关联拖车已离仓' , passed : false , actual : 'ARRIVED' , requirement : 'DEPARTED或COMPLETED' },
2026-08-26 12:46:46 +08:00
{ code : 'SAILING_SCHEDULE' , name : '船期有效且未过期' , passed : false , actual : 'EXPIRED' , requirement : 'VALID' },
2026-08-26 12:36:01 +08:00
{ code : 'CUSTOMS_CLEARANCE' , name : '报关协同完成' , passed : true , actual : 'COMPLETED' , requirement : 'COMPLETED' },
] } }],
},
},
} as never )
const wrapper = mountView (); await flushPromises ()
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '总门禁 1' )
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '阻断 0' )
expect ( wrapper . find ( '.gate-summary' ). text ()). toContain ( '拖车状态(参考):已到仓' )
expect ( wrapper . text ()). not . toContain ( '关联拖车已离仓' )
2026-08-26 12:46:46 +08:00
expect ( wrapper . text ()). not . toContain ( '船期有效且未过期' )
2026-08-26 12:36:01 +08:00
expect ( wrapper . find ( '.outbound-panel__actions button' ). text ()). toContain ( '确认离仓' )
wrapper . unmount ()
})
2026-08-18 17:40:40 +08:00
})