2026-10-20 10:11:31 +08:00
import ElementPlus from 'element-plus'
2026-10-21 07:45:36 +08:00
import { DOMWrapper , enableAutoUnmount , flushPromises , mount , type VueWrapper } from '@vue/test-utils'
2026-10-20 10:11:31 +08:00
import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
import DispatchWorkflowDialog from './DispatchWorkflowDialog.vue'
import DispatchArrangeFields from './DispatchArrangeFields.vue'
import DispatchEvidenceFiles from './DispatchEvidenceFiles.vue'
import DispatchResourceFields from './DispatchResourceFields.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { ApiResponseError } from '@/shared/api/http'
import * as api from '../api/dispatches'
2026-10-21 13:22:42 +08:00
import { command , detail , destination , loadingPreview , resources } from '../dispatchTestFixtures'
import type { ArrangementPreview , CargoLine , DispatchAction , DispatchActions , DispatchEvidenceFile , PreviewGroup , UpdateDispatch } from '../dispatchContracts'
2026-10-20 10:11:31 +08:00
const identity = vi . hoisted (() => ({ id : '' }))
const can = vi . hoisted (() => vi . fn (( _code : string ) => true ))
vi . mock ( '@/core/auth/auth' , () => ({ useAuthStore : () => ({ user : identity }) }))
vi . mock ( '@/core/access/accessPreview' , () => ({ useAccessPreviewStore : () => ({ can }) }))
vi . mock ( '@/modules/overseas-master-data' , () => ({ listVehicles : vi.fn (), listDrivers : vi.fn (), overseasMasterDataPermissions : { vehicleList : 'vehicle:list' , driverList : 'driver:list' } }))
vi . mock ( '../api/dispatches' , () => ({ getDispatch : vi.fn (), getDispatchItems : vi.fn (), getDispatchRoute : vi.fn (), reorderDispatchRoute : vi.fn (), removeDispatchItem : vi.fn (), getDispatchLoading : vi.fn (), getDispatchArrangement : vi.fn (), getDispatchCommand : vi.fn (), listDispatches : vi.fn (), listDispatchCandidates : vi.fn (), completeDispatchLoading : vi.fn (), departDispatch : vi.fn (), reviewDispatch : vi.fn (), reviewDispatchLoading : vi.fn (), returnDispatchToReview : vi.fn (), cancelDispatch : vi.fn (), updateDispatch : vi.fn (), addDispatchCargo : vi.fn (), submitDispatchReview : vi.fn (), transferDispatchToDelivery : vi.fn (), getDispatchEvidence : vi.fn (), getDispatchEvidenceContent : vi.fn (), uploadDispatchEvidence : vi.fn () }))
enableAutoUnmount ( afterEach )
afterEach (() => vi . unstubAllGlobals ())
const gate = { allowed : true , blockCode : null , blockReason : null }
const actions : DispatchActions = { EDIT : gate , ADD_CARGO : gate , REMOVE_CARGO : gate , CANCEL : gate , SUBMIT_REVIEW : gate , DISPATCH_REVIEW : gate , LOADING_REVIEW : gate , RETURN_TO_REVIEW : gate , LOADING_COMPLETE : gate , ARRANGE_DELIVERY : gate , DEPART : gate }
const evidence = ( fileId = '301' ) : DispatchEvidenceFile => ({ fileId , fileName : fileId === '301' ? '旧附件.png' : '新附件.png' , contentType : 'image/png' , sizeBytes : 3 })
function cargo ( key : string , customerId : string ) : CargoLine { return { dispatchItemId : 'item-' + key , customerId , customerName : '客户' + customerId , orderId : null , orderNo : 'ORDER-' + key , sourceKey : key , sourceStage : 'RECEIVED' , sourceRegion : null , sourceInboundId : null , inboundCode : 'IN-' + key , planPalletId : null , warehousePalletId : 'pallet-' + key , palletNo : 'PALLET-' + key , pickId : null , attributionId : null , inventoryPositionId : null , receiptId : null , returnReceiptId : null , returnCartonId : null , cartonMark : 'BOX-' + key , relabelNo : null , destination : destination (), packages : 2 , productQuantity : 4 , grossWeightKg : 10 , volumeCbm : 1 , batchAllocations : [], sourceOrigin : null } }
function arrangement () : ArrangementPreview {
const group = ( key : string , customerId : string ) : PreviewGroup => ({ groupKey : key , customerId , customerName : '客户' + customerId , destination : destination (), groupCargoHash : 'cargo-' + key , cargo : [ cargo ( key , customerId )], externalFreightHash : 'external-' + key , externalQuotes : [], feeCoverageCargoHash : 'coverage-' + key , feeSources : [], requiredServices : [ 'DELIVERY_FEE' ], blockReasons : [] })
const first = group ( 'A' , 'C1' ), second = group ( 'B' , 'C2' )
first . feeSources = [{ financeFeeId : 'original-fee' , customerId : 'C1' , currency : 'USD' , amount : 100 , sourceVersion : 0 , sourceHash : 'original-fee-hash' , status : 'ACTIVE' , services : [ 'DELIVERY_FEE' ], sourceKeys : [ 'A' ] }]
second . externalQuotes = [{ customerId : 'C2' , groupKey : 'B' , bookingNo : 'BOOK-2' , inboundCode : 'IN-B' , currency : 'USD' , amount : 200 , sourceHash : 'quote-hash' }]
return { dispatchNo : 'PC/1' , recordVersion : 0 , status : 'ACTIVE' , stage : 'AFTER_LOADING' , cargoHash : 'whole-cargo' , loadingCompletionHash : 'loaded' , arrange : gate , groups : [ first , second ], arrangement : null }
}
beforeEach (() => {
vi . resetAllMocks (); sessionStorage . clear (); identity . id = crypto . randomUUID (); can . mockReturnValue ( true )
vi . stubGlobal ( 'ResizeObserver' , class { observe() { } unobserve() { } disconnect() { } })
URL . createObjectURL = vi . fn (). mockReturnValue ( 'blob:owned-dispatch' ); URL . revokeObjectURL = vi . fn ()
vi . mocked ( api . getDispatch ). mockResolvedValue ( detail ({ recordVersion : 1 , actions }))
vi . mocked ( api . getDispatchCommand ). mockRejectedValue ( new ApiResponseError ( 'not found' , 'A0404' , 404 ))
vi . mocked ( api . updateDispatch ). mockImplementation ( async ( _ , body ) => command ( 'EDIT' , body . idempotencyKey ))
vi . mocked ( api . reviewDispatch ). mockImplementation ( async ( _ , body ) => command ( 'DISPATCH_REVIEW' , body . idempotencyKey ))
vi . mocked ( api . reviewDispatchLoading ). mockImplementation ( async ( _ , body ) => command ( 'LOADING_REVIEW' , body . idempotencyKey ))
vi . mocked ( api . cancelDispatch ). mockImplementation ( async ( _ , body ) => command ( 'CANCEL' , body . idempotencyKey ))
vi . mocked ( api . returnDispatchToReview ). mockImplementation ( async ( _ , body ) => command ( 'RETURN_TO_REVIEW' , body . idempotencyKey ))
vi . mocked ( api . submitDispatchReview ). mockImplementation ( async ( _ , body ) => command ( 'SUBMIT_REVIEW' , body . idempotencyKey ))
vi . mocked ( api . departDispatch ). mockImplementation ( async ( _ , body ) => command ( 'DEPART' , body . idempotencyKey ))
vi . mocked ( api . reorderDispatchRoute ). mockImplementation ( async ( _ , body ) => command ( 'EDIT' , body . idempotencyKey ))
vi . mocked ( api . completeDispatchLoading ). mockImplementation ( async ( _ , body ) => command ( 'LOADING_COMPLETE' , body . idempotencyKey ))
vi . mocked ( api . getDispatchLoading ). mockResolvedValue ( loadingPreview ())
vi . mocked ( api . getDispatchRoute ). mockResolvedValue ({ dispatchNo : 'PC/1' , recordVersion : 0 , edit : gate , stops : [{ index : 0 , name : '起运仓' , address : '起点地址' , destinationKey : null }, { index : 1 , name : '目的仓甲' , address : '甲地址' , destinationKey : 'A' }, { index : 2 , name : '外接卸货点' , address : '乙地址' , destinationKey : 'B' }] })
vi . mocked ( api . getDispatchArrangement ). mockResolvedValue ( arrangement ())
vi . mocked ( api . getDispatchEvidence ). mockImplementation ( async ( _ , __ , id ) => evidence ( id ))
vi . mocked ( api . getDispatchEvidenceContent ). mockResolvedValue ( new Blob ([ 'png' ], { type : 'image/png' }))
vi . mocked ( api . uploadDispatchEvidence ). mockResolvedValue ( evidence ( '302' ))
})
async function render ( action : DispatchAction , operation ?: 'ROUTE' , files = false ) {
const initial = detail ({ actions }); if ( files ) initial . vehicleInfo . vehicle ! . evidenceFileIds = [ '301' ]
vi . mocked ( api . getDispatch ). mockResolvedValueOnce ( initial )
const wrapper = mount ( DispatchWorkflowDialog , { attachTo : document.body , props : { modelValue : true , dispatchNo : 'PC/1' , action , operation }, global : { plugins : [ ElementPlus ] } }); await flushPromises ()
return { wrapper , dialog : wrapper.findComponent ( AppDialog ) }
}
function field ( wrapper : Pick < VueWrapper , 'findAllComponents' >, label : string ) { return wrapper . findAllComponents ({ name : 'ElFormItem' }). find ( item => item . props ( 'label' ) === label ) ! }
describe ( 'dispatch workflow native layout' , () => {
it ( 'submits all edited resources, dates and zero version through the continuous fullscreen form' , async () => {
const { dialog } = await render ( 'EDIT' )
expect ( dialog . props ( 'title' )). toBe ( '维护车辆信息 · PC/1' )
expect ( dialog . getComponent ( DispatchResourceFields ). props ( 'continuous' )). toBe ( true )
await field ( dialog , '车牌号' ). get ( 'input' ). setValue ( 'CA-NEW' )
field ( dialog , '实际托位容量' ). getComponent ({ name : 'ElInputNumber' }). vm . $emit ( 'update:modelValue' , 22 )
await field ( dialog , '司机姓名' ). get ( 'input' ). setValue ( '新司机' )
field ( dialog , '计划发车时间' ). getComponent ({ name : 'ElDatePicker' }). vm . $emit ( 'update:modelValue' , '2026-11-01T10:00:00Z' )
2026-10-20 14:52:06 +08:00
await dialog . get ( 'textarea[aria-label="本次操作备注"]' ). setValue ( '维护备注' )
2026-10-20 10:11:31 +08:00
await dialog . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' ); await flushPromises ()
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
const { vehicleStatus : _status , ... vehicle } = resources (). vehicle
expect ( api . updateDispatch ). toHaveBeenCalledExactlyOnceWith ( 'PC/1' , { recordVersion : 0 , idempotencyKey : expect.any ( String ), vehicle : { ... vehicle , plate : 'CA-NEW' , capacity : { ... vehicle . capacity , palletSpaces : 22 } }, driver : { ... resources (). driver , name : '新司机' }, plannedDepartureAt : '2026-11-01T10:00:00Z' , estimatedArrivalAt : null , remark : '维护备注' })
})
it ( 'retains the locally submitted draft after close and reopen and retries exactly the same failed request' , async () => {
vi . mocked ( api . updateDispatch ). mockRejectedValueOnce ( new ApiResponseError ( '结果未确认' , 'A0503' , 503 , null , 'workflow-request' )). mockImplementationOnce ( async ( _ , body ) => command ( 'EDIT' , body . idempotencyKey ))
const { wrapper , dialog } = await render ( 'EDIT' )
await field ( dialog , '车牌号' ). get ( 'input' ). setValue ( 'CA-PRESERVED' )
2026-10-20 14:52:06 +08:00
await dialog . get ( 'textarea[aria-label="本次操作备注"]' ). setValue ( '保留本次输入' )
2026-10-20 10:11:31 +08:00
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
const original = JSON . parse ( JSON . stringify ( vi . mocked ( api . updateDispatch ). mock . calls [ 0 ] ! [ 1 ])) as UpdateDispatch
expect ( dialog . text ()). toContain ( 'workflow-request' )
expect ( field ( dialog , '车牌号' ). get ( 'input' ). attributes ( 'disabled' )). toBeDefined ()
await wrapper . setProps ({ dispatchNo : 'PC/2' , action : 'CANCEL' }); await flushPromises ()
expect ( dialog . props ( 'title' )). toBe ( '维护车辆信息 · PC/1' )
await wrapper . setProps ({ modelValue : false }); await wrapper . setProps ({ modelValue : true }); await flushPromises ()
expect (( field ( dialog , '车牌号' ). get ( 'input' ). element as HTMLInputElement ). value ). toBe ( 'CA-PRESERVED' )
2026-10-20 14:52:06 +08:00
expect (( dialog . get ( 'textarea[aria-label="本次操作备注"]' ). element as HTMLTextAreaElement ). value ). toBe ( '保留本次输入' )
2026-10-20 10:11:31 +08:00
dialog . findComponent ( LoadErrorState ). vm . $emit ( 'retry' ); await flushPromises ()
expect ( vi . mocked ( api . updateDispatch ). mock . calls [ 1 ] ! [ 1 ]). toEqual ( original )
})
it . each ([ 'DISPATCH_REVIEW' , 'LOADING_REVIEW' ] as const )( 'keeps %s facts read-only and requires a rejection reason' , async action => {
const { dialog } = await render ( action )
expect ( dialog . get ( '[aria-label="本车审核依据"]' ). findAll ( 'input' )). toHaveLength ( 0 )
expect ( dialog . text ()). toContain ( '审核通过不代替后续现场扫码' )
field ( dialog , '审核结果' ). getComponent ({ name : 'ElRadioGroup' }). vm . $emit ( 'update:modelValue' , 'REJECTED' ); await flushPromises ()
const review = action === 'DISPATCH_REVIEW' ? api.reviewDispatch : api.reviewDispatchLoading
dialog . vm . $emit ( 'confirm' ); await flushPromises (); expect ( review ). not . toHaveBeenCalled ()
await dialog . get ( 'textarea' ). setValue ( ' 证据不完整 ' ); dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( review ). toHaveBeenCalledExactlyOnceWith ( 'PC/1' , { recordVersion : 0 , idempotencyKey : expect.any ( String ), decision : 'REJECTED' , reason : '证据不完整' })
expect ( dialog . findAll ( 'input[type="file"]' )). toHaveLength ( 0 )
})
it . each ([ 'CANCEL' , 'RETURN_TO_REVIEW' , 'SUBMIT_REVIEW' , 'DEPART' ] as const )( 'preserves the %s confirmation branch and its exact reason or remark' , async action => {
const { dialog } = await render ( action )
const needsReason = action === 'CANCEL' || action === 'RETURN_TO_REVIEW'
const send = action === 'CANCEL' ? api.cancelDispatch : action === 'RETURN_TO_REVIEW' ? api.returnDispatchToReview : action === 'DEPART' ? api.departDispatch : api.submitDispatchReview
if ( needsReason ) { dialog . vm . $emit ( 'confirm' ); await flushPromises (); expect ( send ). not . toHaveBeenCalled () }
await dialog . get ( 'textarea' ). setValue ( needsReason ? ' 现场原因 ' : '本次交接' )
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( send ). toHaveBeenCalledExactlyOnceWith ( 'PC/1' , { recordVersion : 0 , idempotencyKey : expect.any ( String ), ...( needsReason ? { reason : '现场原因' } : { remark : '本次交接' }) })
})
it ( 'reorders only real unload stops and preserves the external stop and zero route version' , async () => {
const { dialog } = await render ( 'EDIT' , 'ROUTE' )
const rows = dialog . get ( '[aria-label="本车卸货顺序"]' ). findAll ( 'tbody tr' ). filter ( row => row . findAll ( 'button' ). length )
expect ( rows ). toHaveLength ( 2 )
expect ( rows [ 0 ] ! . findAll ( 'button' )[ 0 ] ! . attributes ( 'disabled' )). toBeDefined ()
await rows [ 1 ] ! . findAll ( 'button' ). find ( button => button . text () === '上移' ) ! . trigger ( 'click' ); await flushPromises ()
await dialog . get ( 'textarea' ). setValue ( ' 顺序调整 ' )
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . reorderDispatchRoute ). toHaveBeenCalledExactlyOnceWith ( 'PC/1' , { recordVersion : 0 , idempotencyKey : expect.any ( String ), destinationKeys : [ 'B' , 'A' ], reason : '顺序调整' })
})
2026-10-21 13:22:42 +08:00
it . each ([ 'ADD_CARGO' , 'REMOVE_CARGO' ] as const )( 'replaces the old %s editor with the smart dispatch entry notice' , async action => {
const { dialog } = await render ( action )
expect ( dialog . text ()). toContain ( '货物安排统一在智能排车中修改' )
expect ( dialog . props ( 'confirmDisabled' )). toBe ( true )
expect ( dialog . findComponent ({ name : 'ElForm' }). exists ()). toBe ( false )
2026-10-20 10:11:31 +08:00
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
2026-10-21 13:22:42 +08:00
expect ( api . addDispatchCargo ). not . toHaveBeenCalled ()
expect ( api . removeDispatchItem ). not . toHaveBeenCalled ()
expect ( api . getDispatchItems ). not . toHaveBeenCalled ()
expect ( api . listDispatchCandidates ). not . toHaveBeenCalled ()
2026-10-20 10:11:31 +08:00
})
it ( 'keeps references separate from real loading measurements and submits the exact manifest' , async () => {
const { dialog } = await render ( 'LOADING_COMPLETE' )
const numbers = dialog . get ( '[aria-label="真实实载登记"]' ). findAllComponents ({ name : 'ElInputNumber' })
expect ( numbers [ 1 ] ! . props ( 'modelValue' )). toBeNull ()
expect ( dialog . get ( '[aria-label="实载与参考依据"]' ). text ()). toContain ( '仅供参考' )
dialog . vm . $emit ( 'confirm' ); await flushPromises (); expect ( api . completeDispatchLoading ). not . toHaveBeenCalled ()
numbers [ 0 ] ! . vm . $emit ( 'update:modelValue' , 1 ); numbers [ 1 ] ! . vm . $emit ( 'update:modelValue' , 120 ); numbers [ 2 ] ! . vm . $emit ( 'update:modelValue' , 1.2 ); await flushPromises ()
await dialog . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' ); await flushPromises ()
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . completeDispatchLoading ). toHaveBeenCalledExactlyOnceWith ( 'PC/1' , { recordVersion : 0 , idempotencyKey : expect.any ( String ), manifestHash : 'manifest' , measurements : [{ warehousePalletId : '21' , palletSpaces : 1 , weightKg : 120 , volumeCbm : 1.2 , deliveryIndex : 1 }], remark : '真实备注' })
})
it ( 'keeps grouped delivery fields, explicit old-fee coverage and only actual uncovered charges' , async () => {
const { dialog } = await render ( 'ARRANGE_DELIVERY' )
const sections = dialog . findAll ( '[data-tone="work"]' ). filter ( section => section . attributes ( 'aria-label' ) ? . startsWith ( '派送分组登记' ))
expect ( sections ). toHaveLength ( 2 )
const groups = dialog . getComponent ( DispatchArrangeFields ). props ( 'modelValue' )
await dialog . get ( '[aria-label="派送分组依据 1"] input[type="checkbox"]' ). setValue ( true ); await flushPromises ()
for ( let index = 0 ; index < 2 ; index ++ ) {
const section = sections [ index ] !
for ( const [ label , value ] of [[ '派送方式' , 'TRUCK_PALLET' ], [ '提货方式' , 'ONE_PICKUP_ONE_DROP' ], [ '装车类型' , index ? 'PALLET' : 'FLOOR' ]] as const ) field ( section , label ). getComponent ({ name : 'ElSelect' }). vm . $emit ( 'update:modelValue' , value )
field ( section , '预计派送时间' ). getComponent ({ name : 'ElDatePicker' }). vm . $emit ( 'update:modelValue' , '2026-11-02T10:00:00Z' )
}
field ( sections [ 0 ] ! , '预约账号' ). getComponent ({ name : 'ElSelect' }). vm . $emit ( 'update:modelValue' , 'BUY_APPOINTMENT' ); await flushPromises ()
dialog . vm . $emit ( 'confirm' ); await flushPromises (); expect ( api . transferDispatchToDelivery ). not . toHaveBeenCalled ()
await field ( sections [ 0 ] ! , '卖家姓名' ). get ( 'input' ). setValue ( '卖家甲' )
field ( sections [ 0 ] ! , '装车费(未覆盖部分)' ). getComponent ({ name : 'ElInputNumber' }). vm . $emit ( 'update:modelValue' , 25 )
field ( sections [ 0 ] ! , '装车费(未覆盖部分)' ). getComponent ({ name : 'ElSelect' }). vm . $emit ( 'update:modelValue' , 'CNY' )
field ( sections [ 0 ] ! , '买约费(未覆盖部分)' ). getComponent ({ name : 'ElInputNumber' }). vm . $emit ( 'update:modelValue' , 40 ); await flushPromises ()
expect ( groups [ 0 ] ! . feeCoverage ). toEqual ([{ financeFeeId : 'original-fee' , sourceVersion : 0 , sourceHash : 'original-fee-hash' , services : [ 'DELIVERY_FEE' ] }])
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . transferDispatchToDelivery ). toHaveBeenCalledExactlyOnceWith ( 'PC/1' , expect . objectContaining ({ recordVersion : 0 , cargoHash : 'whole-cargo' , loadingCompletionHash : 'loaded' , groups : [ expect . objectContaining ({ groupKey : 'A' , sellerName : '卖家甲' , feeCoverage : groups [ 0 ] ! . feeCoverage , fees : [ expect . objectContaining ({ feeType : 'LOADING_FEE' , amount : 25 , currency : 'CNY' }), expect . objectContaining ({ feeType : 'APPOINTMENT_FEE' , amount : 40 , currency : 'USD' })] }), expect . objectContaining ({ groupKey : 'B' , fees : [] })] }))
})
it ( 'previews owned images fullscreen and locks the parent while a real file reference upload is pending' , async () => {
let resolve !: ( value : DispatchEvidenceFile ) => void
vi . mocked ( api . uploadDispatchEvidence ). mockReturnValue ( new Promise ( done => { resolve = done }))
const { dialog } = await render ( 'EDIT' , undefined , true )
const files = dialog . findAllComponents ( DispatchEvidenceFiles )[ 0 ] !
await files . findAll ( 'button' ). find ( button => button . text () === '预览' ) ! . trigger ( 'click' ); await flushPromises ()
const image = files . findComponent ( AppDialog )
expect ( image . props ( 'modelValue' )). toBe ( true )
2026-10-21 07:45:36 +08:00
const preview = new DOMWrapper ( document . querySelector ( '[aria-label="本车附件预览信息"]' ) ! . closest ( '.el-dialog' ) ! )
expect ( preview . get ( 'img' ). attributes ( 'src' )). toBe ( 'blob:owned-dispatch' )
await preview . get ( 'button[aria-label="展开全屏"]' ). trigger ( 'click' ); await flushPromises ()
expect ( preview . classes ()). toContain ( 'is-fullscreen' )
await preview . findAll ( 'button' ). find ( button => button . text () === '关闭' ) ! . trigger ( 'click' ); await flushPromises ()
2026-10-20 10:11:31 +08:00
expect ( URL . revokeObjectURL ). toHaveBeenCalledWith ( 'blob:owned-dispatch' )
const input = files . get ( 'input[type="file"]' ), file = new File ([ 'png' ], 'new.png' , { type : 'image/png' })
Object . defineProperty ( input . element , 'files' , { configurable : true , value : [ file ] }); await input . trigger ( 'change' ); await flushPromises ()
expect ( dialog . props ( 'confirmLoading' )). toBe ( true )
expect ( dialog . findComponent ({ name : 'ElDialog' }). props ()). toMatchObject ({ showClose : false , closeOnPressEscape : false })
expect ( field ( dialog , '车牌号' ). get ( 'input' ). attributes ( 'disabled' )). toBeDefined ()
dialog . vm . $emit ( 'confirm' ); await flushPromises (); expect ( api . updateDispatch ). not . toHaveBeenCalled ()
resolve ( evidence ( '302' )); await flushPromises ()
expect ( dialog . props ( 'confirmLoading' )). toBe ( false )
expect ( files . props ( 'modelValue' )). toEqual ([ '301' , '302' ])
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . updateDispatch ). toHaveBeenCalledWith ( 'PC/1' , expect . objectContaining ({ vehicle : expect.objectContaining ({ evidenceFileIds : [ '301' , '302' ] }) }))
})
it ( 'keeps server gates and write permissions independent from readable facts' , async () => {
can . mockReturnValue ( false )
const { dialog } = await render ( 'EDIT' )
expect ( dialog . props ( 'confirmDisabled' )). toBe ( true )
expect ( field ( dialog , '车牌号' ). get ( 'input' ). attributes ( 'disabled' )). toBeDefined ()
dialog . vm . $emit ( 'confirm' ); await flushPromises ()
expect ( api . updateDispatch ). not . toHaveBeenCalled ()
})
})