2026-10-21 16:12:24 +08:00
import { mount , flushPromises } from '@vue/test-utils'
import ElementPlus from 'element-plus'
import { beforeEach , expect , it , vi } from 'vitest'
import TransportExecutionPanel from './TransportExecutionPanel.vue'
2026-10-21 18:11:48 +08:00
import { assignTransport , commandTransport , getTransport , uploadTransport , type TransportDetail } from '../api/transportTasks'
import { getDriver } from '@/modules/overseas-master-data'
import DispatchEvidenceFiles from './DispatchEvidenceFiles.vue'
2026-10-21 16:12:24 +08:00
vi . mock ( 'vue-router' , () => ({ onBeforeRouteLeave : vi.fn () }))
vi . mock ( '@/core/access/accessPreview' , () => ({ useAccessPreviewStore : () => ({ can : () => true }) }))
2026-10-21 18:11:48 +08:00
vi . mock ( '@/modules/overseas-master-data' , () => ({ listVehicles : vi.fn (), listDrivers : vi.fn (), getDriver : vi.fn (), downloadDriverLicense : vi.fn (), pickupStatusLabels : { ASSIGNED : '待取货' , PICKED : '已取货' , ARRIVED : '已到仓' } }))
2026-10-21 16:12:24 +08:00
vi . mock ( '../api/transportTasks' , () => ({ getTransport : vi.fn (), assignTransport : vi.fn (), transportOccupancy : vi.fn (), cancelTransport : vi.fn (), releaseTransport : vi.fn (), commandTransport : vi.fn (), uploadTransport : vi.fn (), downloadTransportEvidence : vi.fn (), transportLabels : { PICKUP : '到仓取货' , LOCAL_TRUCK : '本土卡车' } }))
const facts = ( arriving = false ) : TransportDetail => ({ dispatchNo : 'PC1' , taskType : 'PICKUP' , execution : { trip : { status : arriving ? 'IN_TRANSIT' : 'PLANNED' , recordVersion : 4 , warehouseName : '洛杉矶仓' , vehicleId : '1' , vehiclePlate : 'CA-1' , driverId : '2' , driverName : '司机甲' , driverPhone : '555' } }, pickups : [{ customerId : 'C1' , transferNo : 'WT1' , packageCount : 5 , actualPackages : arriving ? 5 : null , status : arriving ? 'PICKED' : 'ASSIGNED' , evidence : [] }], completionEvidence : [] }) as unknown as TransportDetail
const options = { global : { plugins : [ ElementPlus ] } }
beforeEach (() => {
vi . clearAllMocks (); vi . stubGlobal ( 'ResizeObserver' , class { observe() { } unobserve() { } disconnect() { } })
vi . mocked ( getTransport ). mockResolvedValue ( facts ())
2026-10-21 18:11:48 +08:00
vi . mocked ( getDriver ). mockResolvedValue ({ id : '2' , licenseFileId : '33' } as Awaited < ReturnType < typeof getDriver >>)
2026-10-21 16:12:24 +08:00
vi . mocked ( uploadTransport ). mockResolvedValue ({ fileId : '88' , fileName : '现场.jpg' , contentType : 'image/jpeg' , sizeBytes : 10 })
})
2026-10-21 18:11:48 +08:00
const photoOptions = { global : { plugins : [ ElementPlus ], stubs : { DispatchEvidenceFiles : true } } }
it ( 'refreshes the selected driver master license when reopening resource maintenance' , async () => {
const value = facts ()
value . resources = { vehicle : null , driver : { driverId : '2' , evidence : { photoFileIds : [ '22' ], fileIds : [ '30' ], masterLicenseFileId : '30' } } } as TransportDetail [ 'resources' ]
vi . mocked ( getTransport ). mockResolvedValue ( value )
const wrapper = mount ( TransportExecutionPanel , { ... photoOptions , props : { dispatchNo : 'PC1' , editResources : true } }); await flushPromises ()
const license = wrapper . findAllComponents ( DispatchEvidenceFiles )[ 2 ] !
expect ( license . props ( 'referenceIds' )). toEqual ([ '33' ])
expect ( license . props ( 'modelValue' )). toEqual ([])
wrapper . unmount ()
})
it ( 'requires three categories and sends photos with both current versions for review' , async () => {
const value = facts (); value . dispatchRecordVersion = 9
vi . mocked ( getTransport ). mockResolvedValue ( value )
vi . mocked ( assignTransport ). mockResolvedValue ( value )
const wrapper = mount ( TransportExecutionPanel , { ... photoOptions , props : { dispatchNo : 'PC1' , editResources : true } }); await flushPromises ()
const save = wrapper . findAll ( 'button' ). find ( button => button . text () === '保存车辆司机' ) !
expect ( save . attributes ( 'disabled' )). toBeDefined ()
const cards = wrapper . findAllComponents ( DispatchEvidenceFiles )
expect ( cards . map ( card => card . props ( 'label' ))). toEqual ([ '车辆图片' , '司机照片' , '驾驶证照片' ])
expect ( cards [ 2 ] ! . props ( 'referenceIds' )). toEqual ([ '33' ])
cards [ 0 ] ! . vm . $emit ( 'update:modelValue' , [ '21' ]); cards [ 1 ] ! . vm . $emit ( 'update:modelValue' , [ '22' ]); await flushPromises ()
expect ( save . attributes ( 'disabled' )). toBeUndefined ()
await save . trigger ( 'click' ); await flushPromises ()
expect ( assignTransport ). toHaveBeenCalledWith ( 'PC1' , expect . objectContaining ({ recordVersion : 4 , dispatchRecordVersion : 9 , vehicleId : '1' , driverId : '2' , masterLicenseFileId : '33' , reviewPhotos : { vehicleFileIds : [ '21' ], driverPhotoFileIds : [ '22' ], licenseFileIds : [] } }))
wrapper . unmount ()
})
it ( 'restores saved photos and retains uploads after a failed save' , async () => {
const value = facts (); value . dispatchRecordVersion = 9
value . resources = { vehicle : { vehicleId : '1' , evidenceFileIds : [ '21' ] }, driver : { driverId : '2' , evidence : { photoFileIds : [ '22' ], fileIds : [ '23' ] } } } as TransportDetail [ 'resources' ]
vi . mocked ( getTransport ). mockResolvedValue ( value ); vi . mocked ( assignTransport ). mockRejectedValue ( new Error ( '保存失败' ))
const wrapper = mount ( TransportExecutionPanel , { ... photoOptions , props : { dispatchNo : 'PC1' , editResources : true } }); await flushPromises ()
const cards = wrapper . findAllComponents ( DispatchEvidenceFiles )
expect ( cards . map ( card => card . props ( 'modelValue' ))). toEqual ([[ '21' ], [ '22' ], [ '23' ]])
expect ( getDriver ). not . toHaveBeenCalled ()
cards [ 0 ] ! . vm . $emit ( 'update:modelValue' , [ '24' ]); await flushPromises ()
await wrapper . findAll ( 'button' ). find ( button => button . text () === '保存车辆司机' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( cards [ 0 ] ! . props ( 'modelValue' )). toEqual ([ '24' ])
expect ( wrapper . emitted ( 'saved' )). toBeUndefined (); wrapper . unmount ()
})
it ( 'clears old photos when selecting another vehicle or driver and ignores late license responses' , async () => {
let resolve !: ( value : Awaited < ReturnType < typeof getDriver >>) => void
vi . mocked ( getDriver ). mockImplementationOnce (() => new Promise ( done => { resolve = done })). mockResolvedValue ({ id : '4' , licenseFileId : '44' } as Awaited < ReturnType < typeof getDriver >>)
const wrapper = mount ( TransportExecutionPanel , { ... photoOptions , props : { dispatchNo : 'PC1' , editResources : true } }); await flushPromises ()
const cards = wrapper . findAllComponents ( DispatchEvidenceFiles )
cards [ 0 ] ! . vm . $emit ( 'update:modelValue' , [ '21' ]); cards [ 1 ] ! . vm . $emit ( 'update:modelValue' , [ '22' ]); cards [ 2 ] ! . vm . $emit ( 'update:modelValue' , [ '23' ])
const selectors = wrapper . findAllComponents ({ name : 'ElSelect' })
selectors [ 0 ] ! . vm . $emit ( 'update:modelValue' , '3' ); selectors [ 1 ] ! . vm . $emit ( 'update:modelValue' , '4' ); await flushPromises ()
resolve ({ id : '2' , licenseFileId : '33' } as Awaited < ReturnType < typeof getDriver >>); await flushPromises ()
expect ( cards . map ( card => card . props ( 'modelValue' ))). toEqual ([[], [], []])
expect ( cards [ 2 ] ! . props ( 'referenceIds' )). toEqual ([ '44' ]); wrapper . unmount ()
})
2026-10-21 16:12:24 +08:00
it . each ([ false , true ])( 'confirms the %s photo stage with its own evidence and current transport version' , async arriving => {
vi . mocked ( getTransport ). mockResolvedValue ( facts ( arriving ))
const wrapper = mount ( TransportExecutionPanel , { ... options , props : { dispatchNo : 'PC1' , pickupApproved : true } }); await flushPromises ()
expect ( wrapper . text ()). toContain ( arriving ? '派送中' : '装货中' )
expect ( wrapper . findAll ( 'button' ). some ( button => button . text () === '确认发车' )). toBe ( false )
2026-10-21 17:28:04 +08:00
await wrapper . findAll ( 'button' ). find ( button => button . text () === ( arriving ? '上传到仓照片' : '上传装货照片' )) ! . trigger ( 'click' ); await flushPromises ()
if ( ! arriving ) {
const quantity = wrapper . get ( 'input[role="spinbutton"]' )
expect (( quantity . element as HTMLInputElement ). value ). toBe ( '' )
await quantity . setValue ( '3' ); await quantity . trigger ( 'change' ); await flushPromises ()
}
2026-10-21 16:12:24 +08:00
const input = wrapper . get ( 'input[type="file"]' )
Object . defineProperty ( input . element , 'files' , { value : [ new File ([ 'photo' ], '现场.jpg' , { type : 'image/jpeg' })], configurable : true })
await input . trigger ( 'change' ); await flushPromises ()
await wrapper . findAll ( 'button' ). find ( button => button . text () === ( arriving ? '确认到仓' : '确认取货' )) ! . trigger ( 'click' ); await flushPromises ()
2026-10-21 17:28:04 +08:00
expect ( commandTransport ). toHaveBeenCalledWith ( 'PC1' , `pickup/C1/WT1/ ${ arriving ? 'arrive' : 'picked' } ` , expect . objectContaining ({ recordVersion : 4 , evidenceFileIds : [ '88' ], ...( arriving ? { reason : '上传到仓照片,确认取货派送完成' } : { actualPackages : 3 }) }))
2026-10-21 17:56:57 +08:00
if ( arriving ) expect ( vi . mocked ( commandTransport ). mock . calls [ 0 ] ! [ 2 ]). not . toHaveProperty ( 'actualPackages' )
2026-10-21 16:12:24 +08:00
expect ( wrapper . emitted ( 'saved' )). toHaveLength ( 1 ); wrapper . unmount ()
})
it ( 'keeps photo confirmation unavailable until both reviews are approved' , async () => {
const wrapper = mount ( TransportExecutionPanel , { ... options , props : { dispatchNo : 'PC1' , pickupApproved : false } }); await flushPromises ()
2026-10-21 17:28:04 +08:00
expect ( wrapper . findAll ( 'button' ). some ( button => button . text () === '上传装货照片' )). toBe ( false )
2026-10-21 16:12:24 +08:00
expect ( commandTransport ). not . toHaveBeenCalled (); wrapper . unmount ()
})
2026-10-21 17:28:04 +08:00
it ( 'requires an explicitly entered pickup quantity even after a photo has uploaded' , async () => {
const wrapper = mount ( TransportExecutionPanel , { ... options , props : { dispatchNo : 'PC1' , pickupApproved : true } }); await flushPromises ()
await wrapper . findAll ( 'button' ). find ( button => button . text () === '上传装货照片' ) ! . trigger ( 'click' ); await flushPromises ()
const input = wrapper . get ( 'input[type="file"]' )
Object . defineProperty ( input . element , 'files' , { value : [ new File ([ 'photo' ], '现场.jpg' , { type : 'image/jpeg' })], configurable : true })
await input . trigger ( 'change' ); await flushPromises ()
const confirm = wrapper . findAll ( 'button' ). find ( button => button . text () === '确认取货' ) !
expect ( confirm . attributes ( 'disabled' )). toBeDefined ()
await confirm . trigger ( 'click' )
expect ( commandTransport ). not . toHaveBeenCalled ()
wrapper . unmount ()
})
2026-10-21 16:12:24 +08:00
it . each ([ false , true ])( 'renders read-only transport facts without resource editing or execution controls (%s)' , async arriving => {
const value = facts ( arriving )
value . execution . trip . vehicleId = null
value . execution . trip . driverId = null
vi . mocked ( getTransport ). mockResolvedValue ( value )
const wrapper = mount ( TransportExecutionPanel , { ... options , props : { dispatchNo : 'PC1' , pickupApproved : true , readonly : true } }); await flushPromises ()
expect ( wrapper . text ()). toContain ( 'WT1' )
expect ( wrapper . text ()). toContain ( '洛杉矶仓' )
expect ( wrapper . findAll ( 'input, select, textarea' )). toHaveLength ( 0 )
expect ( wrapper . findAll ( 'button' )). toHaveLength ( 0 )
expect ( wrapper . findAll ( 'th' ). map ( column => column . text ())). not . toContain ( '操作' )
expect ( commandTransport ). not . toHaveBeenCalled ()
wrapper . unmount ()
})
2026-10-21 17:28:04 +08:00
it ( 'keeps pickup addresses and contacts on their own transfer rows' , async () => {
const value = facts ()
value . pickups [ 0 ] = { ... value . pickups [ 0 ] ! , pickupAddress : '100 Test Pickup Avenue' , contactName : '取货联系人甲' , contactPhone : '0000000001' }
value . pickups . push ({ ... value . pickups [ 0 ] ! , transferNo : 'WT2' , customerId : 'C2' , pickupAddress : '200 Test Pickup Road' , contactName : '取货联系人乙' , contactPhone : '0000000002' })
vi . mocked ( getTransport ). mockResolvedValue ( value )
const wrapper = mount ( TransportExecutionPanel , { ... options , props : { dispatchNo : 'PC1' , readonly : true } }); await flushPromises ()
const rows = wrapper . get ( '[aria-label="取货明细"]' ). findAll ( 'tbody tr' )
expect ( rows [ 0 ] ! . text ()). toContain ( '100 Test Pickup Avenue' )
expect ( rows [ 0 ] ! . text ()). toContain ( '取货联系人甲' )
expect ( rows [ 0 ] ! . text ()). toContain ( '0000000001' )
expect ( rows [ 0 ] ! . text ()). not . toContain ( '200 Test Pickup Road' )
expect ( rows [ 1 ] ! . text ()). toContain ( '200 Test Pickup Road' )
expect ( rows [ 1 ] ! . text ()). toContain ( '取货联系人乙' )
expect ( rows [ 1 ] ! . text ()). toContain ( '0000000002' )
expect ( wrapper . findAll ( 'button' )). toHaveLength ( 0 )
wrapper . unmount ()
})