2026-10-03 04:35:55 +08:00
import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
import { flushPromises , shallowMount , type VueWrapper } from '@vue/test-utils'
2026-10-03 23:44:35 +08:00
import { defineComponent , h , KeepAlive , ref } from 'vue'
2026-10-03 04:35:55 +08:00
import { ElMessage } from 'element-plus'
import { ApiResponseError } from '@/shared/api/http'
import * as api from '../api/drivers'
import type { DriverDetail } from '../driverTypes'
import DriversView from './DriversView.vue'
import DriverDetailView from './DriverDetailView.vue'
import DriverEditorDialog from '../components/DriverEditorDialog.vue'
import DriverLicensePreview from '../components/DriverLicensePreview.vue'
const security = vi . hoisted (() => ({ allowWrite : true }))
2026-10-04 01:02:28 +08:00
const routing = vi . hoisted (() => ({ route : null as null | { name : string ; params : { driverId? : string | string [] } } }))
2026-10-03 04:35:55 +08:00
vi . mock ( '@/core/access/accessPreview' , () => ({ useAccessPreviewStore : () => ({ can : ( code : string ) => code . endsWith ( ':list' ) || security . allowWrite }) }))
2026-10-04 01:02:28 +08:00
vi . mock ( 'vue-router' , async () => {
const { reactive } = await import ( 'vue' )
routing . route = reactive ({ name : 'overseas-master-data-driver-detail' , params : { driverId : '9223372036854775701' } })
return { useRouter : () => ({ push : vi.fn () }), useRoute : () => routing . route }
})
2026-10-03 04:35:55 +08:00
vi . mock ( 'element-plus' , () => ({ ElMessage : { success : vi.fn (), warning : vi.fn (), error : vi.fn () }, ElMessageBox : { confirm : vi.fn (). mockResolvedValue ( undefined ), prompt : vi.fn (). mockResolvedValue ({ value : '暂停' }) } }))
vi . mock ( '../api/drivers' , () => ({ listDrivers : vi.fn (), getDriver : vi.fn (), listDriverHistory : vi.fn (), listDriverWarehouses : vi.fn (), createDriver : vi.fn (), updateDriver : vi.fn (), changeDriverStatus : vi.fn (), reviewDriver : vi.fn (), submitDriver : vi.fn (), getDriverQualification : vi.fn (), uploadDriverLicense : vi.fn (), downloadDriverLicense : vi.fn () }))
const driver : DriverDetail = {
id : '9223372036854775701' , driverNo : 'DR-TEST' , details : { name : '测试司机' , phone : '123' , ownership : 'OWNED' , region : 'OVERSEAS' , licenseNumber : 'LIC-1' , licenseType : 'Class A' , licenseIssuedOn : null , licenseExpiresOn : '2026-10-22' , licenseZone : 'America/Los_Angeles' , remark : null , profile : { warehouseId : '92' , vehicleModels : '53尺拖车' , languagesAndArea : '中文 / 洛杉矶' , medicalExpiresOn : null , trainingExpiresOn : null , insuranceExpiresOn : null } },
active : true , reviewStatus : 'PENDING' , licenseFileId : '9223372036854775801' , recordVersion : 2 , updatedBy : 'operator' , updatedAt : '2026-10-02T12:00:00' , qualification : { canDispatch : false , expiringSoon : true , reason : 'PENDING_REVIEW' , licenseExpiresOn : '2026-10-22' }, licenseState : 'EXPIRING' , warehouseCode : 'WH-US' , warehouseName : '洛杉矶仓' ,
}
const layout = { template : '<div><slot name="header"/><slot/></div>' }
const button = { props : [ 'disabled' , 'loading' ], template : '<button :disabled="disabled || loading"><slot/></button>' }
const dialog = { name : 'AppDialog' , props : [ 'modelValue' ], emits : [ 'confirm' , 'update:modelValue' ], template : '<div v-if="modelValue" data-test="dialog"><slot/><button data-test="confirm" @click="$emit(\'confirm\')">保存</button></div>' }
const input = { name : 'ElInput' , props : [ 'modelValue' ], emits : [ 'update:modelValue' ], template : '<input :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />' }
2026-10-07 12:24:21 +08:00
const table = { name : 'DataTable' , props : [ 'data' , 'page' , 'pageSize' , 'total' , 'loading' ], emits : [ 'update:page' , 'update:pageSize' ], template : '<div><slot name="toolbar-start"/><slot name="toolbar-actions"/><slot name="filters"/><slot name="notices"/><div v-for="row in data" :key="row.id || row.recordVersion"><span>{{row.driverNo}}</span><slot name="actions" :row="row"/><slot name="file" :row="row"/></div></div>' }
2026-10-03 04:35:55 +08:00
const stubs = { AdminPage : layout , ListPageLayout : layout , DetailPageLayout : layout , FormSection : layout , DataTable : table , QueryToolbar : true , RowActions : true , StatusBadge : true , LoadErrorState : true , AppDialog : dialog , DriverEditorDialog : true , DriverLicensePreview : true , ElButton : button , ElForm : layout , ElFormItem : layout , ElInput : input , ElSelect : { ... input , name : 'ElSelect' }, ElOption : true , ElDatePicker : { ... input , name : 'ElDatePicker' } }
const wrappers : VueWrapper [] = []
function mount ( component : object , props : Record < string , unknown > = {}) : VueWrapper {
const wrapper = shallowMount ( component , { props , global : { stubs : { ... stubs , DriverEditorDialog : component === DriverEditorDialog ? false : true , DriverLicensePreview : component === DriverLicensePreview ? false : true }, directives : { loading : () => undefined } } })
wrappers . push ( wrapper )
return wrapper
}
function findButton ( wrapper : VueWrapper , text : string ) { const found = wrapper . findAll ( 'button' ). find ( value => value . text (). includes ( text )); expect ( found ). toBeDefined (); return found ! }
2026-10-04 01:02:28 +08:00
function cachedDetails() {
const showing = ref < string | null >( driver . id )
const host = defineComponent ({ setup : () => () => h ( KeepAlive , null , { default : () => showing . value ? h ( DriverDetailView , { key : showing.value }) : h ( 'div' , '司机列表' ) }) })
const wrapper = shallowMount ( host , { global : { stubs : { ... stubs , KeepAlive : false , DriverDetailView : false }, directives : { loading : () => undefined } } })
wrappers . push ( wrapper )
return { wrapper , show ( id : string | null ) {
routing . route ! . name = id ? 'overseas-master-data-driver-detail' : 'overseas-master-data-drivers'
routing . route ! . params = id ? { driverId : id } : {}
showing . value = id
} }
}
2026-10-03 04:35:55 +08:00
beforeEach (() => {
vi . clearAllMocks (); security . allowWrite = true
2026-10-04 01:02:28 +08:00
routing . route ! . name = 'overseas-master-data-driver-detail'
routing . route ! . params = { driverId : driver.id }
2026-10-03 04:35:55 +08:00
vi . mocked ( api . listDrivers ). mockResolvedValue ({ records : [ structuredClone ( driver )], page : 1 , size : 20 , total : 24 , pages : 2 , summary : { total : 24 , owned : 20 , dispatched : 4 , expiringSoon : 3 , expired : 1 } })
vi . mocked ( api . getDriver ). mockResolvedValue ( structuredClone ( driver ))
vi . mocked ( api . listDriverWarehouses ). mockResolvedValue ([{ id : '92' , code : 'WH-US' , name : '洛杉矶仓' , region : 'OVERSEAS' }])
vi . mocked ( api . listDriverHistory ). mockResolvedValue ({ records : [], page : 1 , size : 20 , total : 0 , pages : 0 })
vi . stubGlobal ( 'URL' , Object . assign ( URL , { createObjectURL : vi.fn (). mockReturnValue ( 'blob:test-license' ), revokeObjectURL : vi.fn () }))
})
afterEach (() => { wrappers . splice ( 0 ). forEach ( wrapper => wrapper . unmount ()); vi . unstubAllGlobals () })
describe ( 'driver page workflows' , () => {
2026-10-05 18:39:25 +08:00
it . each ([ 'success' , 'failure' ] as const )( 'keeps B after cached A detail arrives with late %s' , async outcome => {
let resolveA !: ( value : DriverDetail ) => void , rejectA !: ( reason : Error ) => void
const pendingA = new Promise < DriverDetail >(( resolve , reject ) => { resolveA = resolve ; rejectA = reject })
const other = { ... structuredClone ( driver ), id : '9223372036854775702' , driverNo : 'DR-OTHER' , details : { ... driver . details , name : '另一司机' } }
vi . mocked ( api . getDriver ). mockImplementation ( id => id === driver . id ? pendingA : Promise.resolve ( other ))
const { wrapper , show } = cachedDetails (); await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledExactlyOnceWith ( driver . id )
show ( null ); await flushPromises (); show ( other . id ); await flushPromises ()
expect ( wrapper . text ()). toContain ( '另一司机' )
const before = wrapper . text ()
if ( outcome === 'success' ) resolveA ({ ... structuredClone ( driver ), details : { ... driver . details , name : '迟到A司机' } })
else rejectA ( new Error ( '迟到A详情失败' ))
await flushPromises ()
expect ( wrapper . text ()). toBe ( before )
expect ( wrapper . findComponent ({ name : 'LoadErrorState' }). exists ()). toBe ( false )
const fresh = { ... structuredClone ( driver ), details : { ... driver . details , name : '新请求A司机' } }
vi . mocked ( api . getDriver ). mockResolvedValueOnce ( fresh )
show ( null ); await flushPromises (); show ( driver . id ); await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 3 )
expect ( api . getDriver ). toHaveBeenLastCalledWith ( driver . id )
expect ( wrapper . text ()). toContain ( '新请求A司机' )
expect ( wrapper . text ()). not . toContain ( '迟到A' )
expect ( wrapper . findComponent ({ name : 'LoadErrorState' }). exists ()). toBe ( false )
})
it . each ([ 'success' , 'failure' ] as const )( 'keeps B history after cached A history arrives with late %s' , async outcome => {
type HistoryPage = Awaited < ReturnType < typeof api.listDriverHistory >>
let resolveA !: ( value : HistoryPage ) => void , rejectA !: ( reason : Error ) => void
const pendingA = new Promise < HistoryPage >(( resolve , reject ) => { resolveA = resolve ; rejectA = reject })
const other = { ... structuredClone ( driver ), id : '9223372036854775702' , driverNo : 'DR-OTHER' , details : { ... driver . details , name : '另一司机' } }
const history = ( snapshot : DriverDetail , total : number ) : HistoryPage => ({ records : [{ recordVersion : snapshot.recordVersion , action : 'CREATE' , reason : null , operatorId : 'test-operator' , occurredAt : '2026-10-02T12:00:00' , snapshot }], page : 1 , size : 20 , total , pages : 1 })
const bHistory = history ( other , 15 ), aHistory = history ( driver , 3 )
vi . mocked ( api . getDriver ). mockImplementation ( async id => structuredClone ( id === driver . id ? driver : other ))
vi . mocked ( api . listDriverHistory ). mockImplementation ( id => id === driver . id ? pendingA : Promise.resolve ( bHistory ))
const { wrapper , show } = cachedDetails (); await flushPromises ()
await findButton ( wrapper , '查看操作历史' ). trigger ( 'click' ); await flushPromises ()
expect ( api . listDriverHistory ). toHaveBeenCalledExactlyOnceWith ( driver . id , 1 , 20 )
show ( null ); await flushPromises (); show ( other . id ); await flushPromises ()
await findButton ( wrapper , '查看操作历史' ). trigger ( 'click' ); await flushPromises ()
const before = wrapper . text (), tableB = wrapper . findComponent ({ name : 'DataTable' })
expect ( tableB . props ( 'data' )). toEqual ( bHistory . records ); expect ( tableB . props ( 'total' )). toBe ( 15 ); expect ( tableB . props ( 'loading' )). toBe ( false )
if ( outcome === 'success' ) resolveA ( aHistory )
else rejectA ( new Error ( '迟到A历史失败' ))
await flushPromises ()
expect ( wrapper . text ()). toBe ( before )
expect ( tableB . props ( 'data' )). toEqual ( bHistory . records ); expect ( tableB . props ( 'total' )). toBe ( 15 ); expect ( tableB . props ( 'loading' )). toBe ( false )
expect ( wrapper . findComponent ({ name : 'LoadErrorState' }). exists ()). toBe ( false )
show ( null ); await flushPromises (); show ( driver . id ); await flushPromises ()
expect ( wrapper . findComponent ({ name : 'DataTable' }). exists ()). toBe ( false )
expect ( api . listDriverHistory ). toHaveBeenCalledTimes ( 2 )
vi . mocked ( api . listDriverHistory ). mockResolvedValueOnce ( aHistory )
await findButton ( wrapper , '查看操作历史' ). trigger ( 'click' ); await flushPromises ()
expect ( api . listDriverHistory ). toHaveBeenCalledTimes ( 3 )
expect ( api . listDriverHistory ). toHaveBeenLastCalledWith ( driver . id , 1 , 20 )
expect ( wrapper . findComponent ({ name : 'DataTable' }). props ( 'data' )). toEqual ( aHistory . records )
expect ( wrapper . findComponent ({ name : 'DataTable' }). props ( 'total' )). toBe ( 3 )
expect ( wrapper . findComponent ({ name : 'LoadErrorState' }). exists ()). toBe ( false )
})
2026-10-04 01:02:28 +08:00
it ( 'does not fetch an absent driver on return to list and loads the valid driver on reentry' , async () => {
const { wrapper , show } = cachedDetails (); await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledExactlyOnceWith ( driver . id )
show ( null )
await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 1 )
const next = { ... structuredClone ( driver ), recordVersion : 3 , details : { ... driver . details , name : '当前司机' } }
vi . mocked ( api . getDriver ). mockResolvedValueOnce ( next )
show ( next . id )
await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 2 )
expect ( api . getDriver ). toHaveBeenLastCalledWith ( next . id )
expect ( wrapper . text ()). toContain ( '当前司机' )
})
it ( 'loads only the active driver instance when two parameter-keyed detail pages are cached' , async () => {
const other = { ... structuredClone ( driver ), id : '9223372036854775702' , driverNo : 'DR-OTHER' , details : { ... driver . details , name : '另一司机' } }
vi . mocked ( api . getDriver ). mockImplementation ( async id => structuredClone ( id === driver . id ? driver : other ))
const { wrapper , show } = cachedDetails (); await flushPromises ()
const first = wrapper . findComponent ( DriverDetailView )
expect ( api . getDriver ). toHaveBeenCalledExactlyOnceWith ( driver . id )
show ( other . id ); await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 2 )
expect ( api . getDriver ). toHaveBeenLastCalledWith ( other . id )
expect ( first . text ()). not . toContain ( '另一司机' )
expect ( wrapper . text ()). toContain ( '另一司机' )
show ( driver . id ); await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 3 )
expect ( api . getDriver ). toHaveBeenLastCalledWith ( driver . id )
expect ( wrapper . text ()). toContain ( '测试司机' )
show ( null ); await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 3 )
show ( other . id ); await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 4 )
expect ( api . getDriver ). toHaveBeenLastCalledWith ( other . id )
})
it ( 'invalidates a pending detail response when the route leaves even before params are cleared' , async () => {
let finish !: ( value : DriverDetail ) => void
vi . mocked ( api . getDriver ). mockReturnValueOnce ( new Promise ( resolve => { finish = resolve }))
const wrapper = mount ( DriverDetailView ); await flushPromises ()
routing . route ! . name = 'overseas-master-data-drivers'
await flushPromises ()
finish ({ ... structuredClone ( driver ), details : { ... driver . details , name : '离页迟到司机' } })
await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 1 )
expect ( wrapper . text ()). not . toContain ( '离页迟到司机' )
})
it . each ([{ label : 'missing' , id : undefined }, { label : 'empty' , id : '' }, { label : 'array' , id : [ driver . id ] }])( 'does not fabricate a detail ID from $label route params' , async ({ id }) => {
routing . route ! . params = { driverId : id }
mount ( DriverDetailView ); await flushPromises ()
expect ( api . getDriver ). not . toHaveBeenCalled ()
})
2026-10-03 23:44:35 +08:00
it ( 'refreshes a cached list once on return and retains applied filters and pagination' , async () => {
const showingList = ref ( true )
const host = defineComponent ({ setup : () => () => h ( KeepAlive , null , { default : () => showingList . value ? h ( DriversView ) : h ( 'div' , '司机详情' ) }) })
const wrapper = shallowMount ( host , { global : { stubs : { ... stubs , KeepAlive : false , DriversView : false }, directives : { loading : () => undefined } } })
wrappers . push ( wrapper )
await flushPromises ()
expect ( api . listDrivers ). toHaveBeenCalledTimes ( 1 )
const list = wrapper . findComponent ( DriversView )
const toolbar = list . findComponent ({ name : 'QueryToolbar' })
toolbar . vm . $emit ( 'update:modelValue' , { keyword : 'DR-TEST' , licenseState : 'EXPIRING' })
toolbar . vm . $emit ( 'search' ); await flushPromises ()
list . findComponent ({ name : 'DataTable' }). vm . $emit ( 'update:page' , 2 ); await flushPromises ()
expect ( api . listDrivers ). toHaveBeenCalledTimes ( 3 )
showingList . value = false ; await flushPromises ()
const refreshed = { ... structuredClone ( driver ), recordVersion : 3 , reviewStatus : 'APPROVED' as const }
vi . mocked ( api . listDrivers ). mockResolvedValueOnce ({ records : [ refreshed ], page : 2 , size : 20 , total : 1 , pages : 1 , summary : { total : 1 , owned : 1 , dispatched : 0 , expiringSoon : 1 , expired : 0 } })
showingList . value = true ; await flushPromises ()
expect ( api . listDrivers ). toHaveBeenCalledTimes ( 4 )
expect ( api . listDrivers ). toHaveBeenLastCalledWith ({ keyword : 'DR-TEST' , licenseState : 'EXPIRING' , region : undefined , ownership : undefined , active : undefined , reviewStatus : undefined , page : 2 , size : 20 })
expect ( wrapper . findComponent ({ name : 'DataTable' }). props ( 'data' )). toEqual ([ refreshed ])
expect ( wrapper . text ()). toContain ( '司机总数1' )
expect ( api . getDriver ). not . toHaveBeenCalled ()
})
it ( 'ignores a list response that arrives while its cached page is deactivated' , async () => {
let resolveList !: ( value : Awaited < ReturnType < typeof api.listDrivers >>) => void
vi . mocked ( api . listDrivers ). mockReturnValueOnce ( new Promise ( resolve => { resolveList = resolve }))
const showingList = ref ( true )
const host = defineComponent ({ setup : () => () => h ( KeepAlive , null , { default : () => showingList . value ? h ( DriversView ) : h ( 'div' , '司机详情' ) }) })
const wrapper = shallowMount ( host , { global : { stubs : { ... stubs , KeepAlive : false , DriversView : false }, directives : { loading : () => undefined } } })
wrappers . push ( wrapper )
await flushPromises ()
const list = wrapper . findComponent ( DriversView )
showingList . value = false ; await flushPromises ()
resolveList ({ records : [ driver ], page : 1 , size : 20 , total : 24 , pages : 2 , summary : { total : 24 , owned : 20 , dispatched : 4 , expiringSoon : 3 , expired : 1 } }); await flushPromises ()
expect ( list . findComponent ({ name : 'DataTable' }). props ( 'data' )). toEqual ([])
showingList . value = true ; await flushPromises ()
expect ( api . listDrivers ). toHaveBeenCalledTimes ( 2 )
expect ( wrapper . findComponent ({ name : 'DataTable' }). props ( 'data' )). toEqual ([ driver ])
})
2026-10-03 04:35:55 +08:00
it ( 'loads one paged request with server summary and does not request each driver or file' , async () => {
const wrapper = mount ( DriversView )
await flushPromises ()
expect ( api . listDrivers ). toHaveBeenCalledTimes ( 1 )
expect ( wrapper . text ()). toContain ( '司机总数24' )
expect ( api . getDriver ). not . toHaveBeenCalled ()
expect ( api . downloadDriverLicense ). not . toHaveBeenCalled ()
wrapper . findComponent ({ name : 'DataTable' }). vm . $emit ( 'update:page' , 2 )
await flushPromises ()
expect ( api . listDrivers ). toHaveBeenCalledTimes ( 2 )
expect ( api . listDrivers ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 2 }))
await findButton ( wrapper , '即将到期' ). trigger ( 'click' )
await flushPromises ()
expect ( api . listDrivers ). toHaveBeenCalledTimes ( 3 )
expect ( api . listDrivers ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 1 , licenseState : 'EXPIRING' }))
})
it ( 'shows a retryable error instead of stale summary after list failure' , async () => {
vi . mocked ( api . listDrivers ). mockRejectedValueOnce ( new Error ( '服务不可用' ))
const wrapper = mount ( DriversView )
await flushPromises ()
const error = wrapper . findComponent ({ name : 'LoadErrorState' })
expect ( error . exists ()). toBe ( true )
expect ( wrapper . text ()). toContain ( '司机总数—' )
error . vm . $emit ( 'retry' ); await flushPromises ()
expect ( api . listDrivers ). toHaveBeenCalledTimes ( 2 )
})
it ( 'does not expose write actions to a read-only user' , async () => {
security . allowWrite = false
const list = mount ( DriversView ), detail = mount ( DriverDetailView )
await flushPromises ()
expect ( list . text ()). not . toContain ( '新增司机' )
expect ( detail . text ()). not . toContain ( '审核通过' )
expect ( detail . text ()). not . toContain ( '上传 / 替换驾驶证' )
expect ( detail . text ()). not . toContain ( '编辑档案' )
})
it ( 'loads detail once, requests history only on demand, and requires a rejection reason' , async () => {
const wrapper = mount ( DriverDetailView )
await flushPromises ()
expect ( api . getDriver ). toHaveBeenCalledTimes ( 1 )
expect ( api . listDriverHistory ). not . toHaveBeenCalled ()
expect ( wrapper . text ()). toContain ( '洛杉矶仓' )
expect ( wrapper . text ()). not . toContain ( driver . id )
await findButton ( wrapper , '打回' ). trigger ( 'click' )
expect ( api . reviewDriver ). not . toHaveBeenCalled ()
expect ( ElMessage . warning ). toHaveBeenCalledWith ( '请填写打回原因' )
await findButton ( wrapper , '查看操作历史' ). trigger ( 'click' ); await flushPromises ()
expect ( api . listDriverHistory ). toHaveBeenCalledTimes ( 1 )
})
it ( 'submits approval with current version and uses returned state without reloading detail' , async () => {
vi . mocked ( api . reviewDriver ). mockResolvedValue ({ ... structuredClone ( driver ), recordVersion : 3 , reviewStatus : 'APPROVED' })
const wrapper = mount ( DriverDetailView )
await flushPromises ()
await findButton ( wrapper , '审核通过' ). trigger ( 'click' ); await flushPromises ()
expect ( api . reviewDriver ). toHaveBeenCalledWith ( driver . id , 2 , 'APPROVED' , null )
expect ( api . getDriver ). toHaveBeenCalledTimes ( 1 )
expect ( wrapper . findAll ( 'button' ). some ( value => value . text () === '审核通过' )). toBe ( false )
})
it ( 'keeps the editor and version after a failed write' , async () => {
vi . mocked ( api . updateDriver ). mockRejectedValue ( new ApiResponseError ( '版本已变化,请刷新' , 'A0409' , 200 , null , 'req-driver-conflict' ))
const wrapper = mount ( DriverEditorDialog , { modelValue : false , driver })
await wrapper . setProps ({ modelValue : true }); await flushPromises ()
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . updateDriver ). toHaveBeenCalledWith ( driver . id , 2 , expect . objectContaining ({ name : driver.details.name }))
expect ( wrapper . findComponent ({ name : 'AppDialog' }). props ( 'modelValue' )). toBe ( true )
expect ( wrapper . findComponent ({ name : 'LoadErrorState' }). props ( 'message' )). toContain ( '版本已变化' )
})
it ( 'invalidates an old task preview after the planned end changes without inventing a new server qualification' , async () => {
vi . mocked ( api . getDriverQualification ). mockResolvedValue ({ ... driver . qualification , canDispatch : true , reason : 'ELIGIBLE' })
const wrapper = mount ( DriverDetailView )
await flushPromises ()
const picker = wrapper . findComponent ({ name : 'ElDatePicker' })
picker . vm . $emit ( 'update:modelValue' , new Date ( '2026-10-20T12:00:00Z' ))
picker . vm . $emit ( 'change' ); await flushPromises ()
await findButton ( wrapper , '校验本次任务资格' ). trigger ( 'click' ); await flushPromises ()
expect ( api . getDriverQualification ). toHaveBeenCalledWith ( driver . id , '2026-10-20T12:00:00.000Z' )
expect ( wrapper . text ()). toContain ( '本次任务可派车' )
picker . vm . $emit ( 'update:modelValue' , new Date ( '2026-11-20T12:00:00Z' ))
picker . vm . $emit ( 'change' ); await flushPromises ()
expect ( wrapper . text ()). not . toContain ( '本次任务可派车' )
expect ( wrapper . text ()). toContain ( '请重新校验' )
expect ( api . getDriverQualification ). toHaveBeenCalledTimes ( 1 )
})
2026-11-06 21:02:20 +08:00
it ( 'accepts image selection and clears it when a PDF is selected instead' , async () => {
vi . mocked ( api . updateDriver ). mockResolvedValue ( structuredClone ( driver ))
const wrapper = mount ( DriverEditorDialog , { modelValue : false , driver })
await wrapper . setProps ({ modelValue : true }); await flushPromises ()
const picker = wrapper . get ( 'input[type="file"]' )
expect ( picker . attributes ( 'accept' )). toBe ( 'image/jpeg,image/png' )
Object . defineProperty ( picker . element , 'files' , { value : [ new File ([ 'image' ], 'license.png' , { type : 'image/png' })], configurable : true })
await picker . trigger ( 'change' )
expect ( ElMessage . warning ). not . toHaveBeenCalled ()
Object . defineProperty ( picker . element , 'files' , { value : [ new File ([ '%PDF' ], 'license.pdf' , { type : 'application/pdf' })], configurable : true })
await picker . trigger ( 'change' )
expect ( ElMessage . warning ). toHaveBeenCalledWith ( '请选择非空 JPG 或 PNG,最大 10MiB' )
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . updateDriver ). toHaveBeenCalledTimes ( 1 )
expect ( api . uploadDriverLicense ). not . toHaveBeenCalled ()
})
2026-10-03 04:35:55 +08:00
it ( 'does not create a second draft when attachment upload fails after saving' , async () => {
vi . mocked ( api . createDriver ). mockResolvedValue ({ ... structuredClone ( driver ), recordVersion : 0 })
vi . mocked ( api . uploadDriverLicense ). mockRejectedValueOnce ( new Error ( '文件服务不可用' )). mockResolvedValueOnce ({ ... structuredClone ( driver ), recordVersion : 1 })
const wrapper = mount ( DriverEditorDialog , { modelValue : false , driver : null })
await wrapper . setProps ({ modelValue : true }); await flushPromises ()
const inputs = wrapper . findAllComponents ({ name : 'ElInput' })
inputs [ 0 ] ! . vm . $emit ( 'update:modelValue' , '新司机' )
inputs [ 1 ] ! . vm . $emit ( 'update:modelValue' , '123' )
const selects = wrapper . findAllComponents ({ name : 'ElSelect' })
selects . at ( - 1 ) ! . vm . $emit ( 'update:modelValue' , 'UTC' )
2026-11-06 21:02:20 +08:00
const file = new File ([ 'image' ], 'license.png' , { type : 'image/png' })
2026-10-03 04:35:55 +08:00
const picker = wrapper . get ( 'input[type="file"]' )
Object . defineProperty ( picker . element , 'files' , { value : [ file ] })
await picker . trigger ( 'change' )
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . createDriver ). toHaveBeenCalledTimes ( 1 )
expect ( api . uploadDriverLicense ). toHaveBeenCalledTimes ( 1 )
expect ( wrapper . findComponent ({ name : 'LoadErrorState' }). props ( 'message' )). toContain ( '档案已保存' )
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . createDriver ). toHaveBeenCalledTimes ( 1 )
expect ( api . updateDriver ). not . toHaveBeenCalled ()
expect ( api . uploadDriverLicense ). toHaveBeenCalledTimes ( 2 )
expect ( wrapper . emitted ( 'update:modelValue' ) ? . at ( - 1 )). toEqual ([ false ])
})
it ( 'keeps an old save outside the newly opened driver session and uploads only that driver file' , async () => {
let finishOld !: ( value : DriverDetail ) => void
vi . mocked ( api . updateDriver ). mockReturnValueOnce ( new Promise ( resolve => { finishOld = resolve }))
const other = { ... structuredClone ( driver ), id : '9223372036854775702' , driverNo : 'DR-OTHER' , details : { ... driver . details , name : '另一司机' } }
const wrapper = mount ( DriverEditorDialog , { modelValue : false , driver })
await wrapper . setProps ({ modelValue : true }); await flushPromises ()
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . updateDriver ). toHaveBeenCalledWith ( driver . id , 2 , expect . anything ())
await wrapper . setProps ({ modelValue : false }); await flushPromises ()
await wrapper . setProps ({ driver : other , modelValue : true }); await flushPromises ()
2026-11-06 21:02:20 +08:00
const file = new File ([ 'image' ], 'other-driver.jpg' , { type : 'image/jpeg' })
2026-10-03 04:35:55 +08:00
const picker = wrapper . get ( 'input[type="file"]' )
Object . defineProperty ( picker . element , 'files' , { value : [ file ], configurable : true })
await picker . trigger ( 'change' )
finishOld ({ ... structuredClone ( driver ), recordVersion : 3 }); await flushPromises ()
expect ( api . uploadDriverLicense ). not . toHaveBeenCalled ()
expect ( wrapper . emitted ( 'saved' )). toBeUndefined ()
expect ( wrapper . findComponent ({ name : 'AppDialog' }). props ( 'modelValue' )). toBe ( true )
expect ( wrapper . findAllComponents ({ name : 'ElInput' })[ 0 ] ! . props ( 'modelValue' )). toBe ( '另一司机' )
vi . mocked ( api . updateDriver ). mockResolvedValue ({ ... other , recordVersion : 3 })
vi . mocked ( api . uploadDriverLicense ). mockResolvedValue ({ ... other , recordVersion : 4 })
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . updateDriver ). toHaveBeenLastCalledWith ( other . id , 2 , expect . objectContaining ({ name : '另一司机' }))
expect ( api . uploadDriverLicense ). toHaveBeenCalledExactlyOnceWith ( other . id , 3 , file )
expect ( wrapper . emitted ( 'saved' ) ? .[ 0 ] ? .[ 0 ]). toEqual ( expect . objectContaining ({ id : other.id }))
})
it ( 'does not continue a pending save with an upload after the editor is unmounted' , async () => {
let finishOld !: ( value : DriverDetail ) => void
vi . mocked ( api . updateDriver ). mockReturnValueOnce ( new Promise ( resolve => { finishOld = resolve }))
const wrapper = mount ( DriverEditorDialog , { modelValue : false , driver })
await wrapper . setProps ({ modelValue : true }); await flushPromises ()
const picker = wrapper . get ( 'input[type="file"]' )
2026-11-06 21:02:20 +08:00
Object . defineProperty ( picker . element , 'files' , { value : [ new File ([ 'image' ], 'driver.jpg' , { type : 'image/jpeg' })] })
2026-10-03 04:35:55 +08:00
await picker . trigger ( 'change' )
wrapper . findComponent ({ name : 'AppDialog' }). vm . $emit ( 'confirm' ); await flushPromises ()
wrapper . unmount ()
finishOld ({ ... structuredClone ( driver ), recordVersion : 3 }); await flushPromises ()
expect ( api . uploadDriverLicense ). not . toHaveBeenCalled ()
expect ( ElMessage . success ). not . toHaveBeenCalled ()
})
it ( 'releases preview blobs when closed and ignores a download completed after close' , async () => {
vi . mocked ( api . downloadDriverLicense ). mockResolvedValueOnce ( new Blob ([ '%PDF' ], { type : 'application/pdf' }))
const wrapper = mount ( DriverLicensePreview , { modelValue : false , driverId : driver.id , fileId : driver.licenseFileId , driverName : driver.details.name })
await wrapper . setProps ({ modelValue : true }); await flushPromises ()
expect ( wrapper . find ( 'iframe' ). exists ()). toBe ( true )
await wrapper . setProps ({ modelValue : false })
expect ( URL . revokeObjectURL ). toHaveBeenCalledWith ( 'blob:test-license' )
let complete !: ( value : Blob ) => void
vi . mocked ( api . downloadDriverLicense ). mockReturnValueOnce ( new Promise ( resolve => { complete = resolve }))
await wrapper . setProps ({ modelValue : true })
await wrapper . setProps ({ modelValue : false })
complete ( new Blob ([ '%PDF' ], { type : 'application/pdf' })); await flushPromises ()
expect ( URL . createObjectURL ). toHaveBeenCalledTimes ( 1 )
})
})