2026-10-09 22:05:51 +08:00
import { createPinia } from 'pinia'
2026-10-21 16:58:38 +08:00
import ElementPlus , { ElSelect , ElTag } from 'element-plus'
2026-10-19 17:56:31 +08:00
import { flushPromises , mount } from '@vue/test-utils'
import { beforeEach , expect , it , vi } from 'vitest'
2026-08-21 11:20:09 +08:00
import DispatchesView from './DispatchesView.vue'
2026-10-19 17:56:31 +08:00
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
2026-10-21 16:58:38 +08:00
import DataTable from '@/shared/components/DataTable.vue'
2026-10-19 17:56:31 +08:00
import RowActions from '@/shared/components/RowActions.vue'
import { getDispatch , getDispatchBolPdf , listDispatches } from '../api/dispatches'
2026-10-20 14:25:01 +08:00
import { command , detail , listRow , page } from '../dispatchTestFixtures'
2026-10-19 17:56:31 +08:00
import type { DispatchPage } from '../dispatchContracts'
import { useDispatchCommandRecovery } from '../dispatchCommandRecovery'
const identity = vi . hoisted (() => ({ id : '' }))
2026-10-08 14:11:54 +08:00
const push = vi . hoisted (() => vi . fn ())
2026-10-19 17:56:31 +08:00
const can = vi . hoisted (() => vi . fn (() => true ))
vi . mock ( 'vue-router' , () => ({ useRoute : () => ({ query : {} }), useRouter : () => ({ push }) }))
vi . mock ( '@/core/access/accessPreview' , () => ({ useAccessPreviewStore : () => ({ can }) }))
vi . mock ( '@/core/auth/auth' , () => ({ useAuthStore : () => ({ user : identity }) }))
vi . mock ( '@/modules/appointment-calendar' , () => ({ appointmentReturnTarget : () => null }))
vi . mock ( '@/modules/overseas-master-data' , () => ({ TransportRouteDialog : { template : '<div />' }, routeLocation : ( label : string ) => ({ label }) }))
vi . mock ( '../api/dispatches' , () => ({ listDispatches : vi.fn (), getDispatch : vi.fn (), getDispatchBolPdf : vi.fn () }))
2026-10-20 21:33:46 +08:00
const options = { global : { plugins : [ createPinia (), ElementPlus ], stubs : { SmartDispatchDialog : true , DispatchWorkflowDialog : true , TransportTaskCreateDialog : true } } }
2026-10-19 17:56:31 +08:00
beforeEach (() => { vi . clearAllMocks (); identity . id = crypto . randomUUID (); sessionStorage . clear (); can . mockReturnValue ( true ); vi . stubGlobal ( 'ResizeObserver' , class { observe() { } unobserve() { } disconnect() { } }); localStorage . clear (); vi . mocked ( listDispatches ). mockResolvedValue ( page ()); vi . mocked ( getDispatch ). mockResolvedValue ( detail ()) })
2026-10-21 14:30:33 +08:00
it ( 'never offers a second arrangement or departure after loading completion' , async () => {
const gate = { allowed : true , blockCode : null , blockReason : null }
vi . mocked ( listDispatches ). mockResolvedValueOnce ( page ([ listRow ({ stage : 'DELIVERING' , actions : { ARRANGE_DELIVERY : gate , DEPART : gate } })]))
2026-10-19 17:56:31 +08:00
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
2026-10-21 14:30:33 +08:00
expect ( wrapper . findAll ( '[role="tab"]' ). some ( tab => tab . text (). startsWith ( '装车后' ))). toBe ( false )
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
const keys = ( row . props ( 'actions' ) as Array < { key : string } > ). map ( action => action . key )
expect ( keys ). not . toContain ( 'ARRANGE_DELIVERY' ); expect ( keys ). not . toContain ( 'DEPART' )
row . vm . $emit ( 'action' , 'DEPART' ); await flushPromises ()
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ( 'modelValue' )). toBe ( false )
wrapper . unmount ()
})
it ( 'offers recovery of the original pending command independently of the filtered list rows' , async () => {
const pending = useDispatchCommandRecovery (() => identity . id ); pending . begin ( 'PC-OTHER' , 'LOADING_COMPLETE' , { recordVersion : 0 , idempotencyKey : 'original-key' })
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
expect ( wrapper . text ()). toContain ( 'PC-OTHER 的确认装车完成结果尚待核对' )
2026-10-19 17:56:31 +08:00
await wrapper . findAll ( 'button' ). find ( button => button . text () === '核对原请求' ) ! . trigger ( 'click' )
const dialog = wrapper . get ( 'dispatch-workflow-dialog-stub' )
expect ( dialog . attributes ( 'dispatchno' )). toBe ( 'PC-OTHER' ); expect ( dialog . attributes ( 'modelvalue' )). toBe ( 'true' ); wrapper . unmount ()
})
2026-10-21 13:22:42 +08:00
it ( 'centralizes cargo changes in smart dispatch and never exposes row add/remove actions' , async () => {
vi . mocked ( listDispatches ). mockResolvedValueOnce ( page ([ listRow ({ actions : { ADD_CARGO : { allowed : true , blockCode : null , blockReason : null }, REMOVE_CARGO : { allowed : true , blockCode : null , blockReason : null } } })]))
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
for ( const action of [ 'ADD_CARGO' , 'REMOVE_CARGO' ]) {
expect (( row . props ( 'actions' ) as Array < { key : string } > ). map ( item => item . key )). not . toContain ( action )
row . vm . $emit ( 'action' , action ); await flushPromises ()
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ( 'modelValue' )). toBe ( false )
}
await wrapper . findAll ( 'button' ). find ( button => button . text () === '智能排车' ) ! . trigger ( 'click' )
expect ( wrapper . getComponent ({ name : 'SmartDispatchDialog' }). props ( 'modelValue' )). toBe ( true )
wrapper . unmount ()
})
2026-10-21 14:30:33 +08:00
it ( 'keeps vehicle maintenance and detail visible while submitting review remains available in the menu' , async () => {
const allowed = { allowed : true , blockCode : null , blockReason : null }
vi . mocked ( listDispatches ). mockResolvedValue ( page ([ listRow ({ actions : { EDIT : allowed , SUBMIT_REVIEW : allowed , CANCEL : allowed } })]))
const wrapper = mount ( DispatchesView , { ... options , attachTo : document.body }); await flushPromises ()
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
expect ( row . findAll ( 'button' ). map ( button => button . text ())). toEqual ([ '维护车辆信息' , '详情' , '更多' ])
await row . get ( 'button[aria-label="更多操作 · PC/1"]' ). trigger ( 'click' ); await flushPromises ()
const submit = Array . from ( document . querySelectorAll < HTMLElement >( '.el-dropdown-menu__item' )). find ( item => item . textContent === '提交调度审核' ) !
expect ( submit ). toBeDefined ()
submit . click (); await flushPromises ()
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ()). toMatchObject ({ modelValue : true , action : 'SUBMIT_REVIEW' , dispatchNo : 'PC/1' })
wrapper . unmount ()
})
it . each ([[ 'DISPATCH_REVIEW' , 1 , '调度审核' ], [ 'LOADING_REVIEW' , 2 , '理货员审核' ]] as const )( 'prioritizes the current %s queue action with a short label' , async ( action , index , label ) => {
const allowed = { allowed : true , blockCode : null , blockReason : null }
2026-10-21 15:10:54 +08:00
vi . mocked ( listDispatches ). mockResolvedValue ( page ([ listRow ({ actions : { EDIT : allowed , CANCEL : allowed , SUBMIT_REVIEW : allowed , DISPATCH_REVIEW : allowed , LOADING_REVIEW : allowed } })]))
2026-10-21 14:30:33 +08:00
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
await wrapper . get ( '[aria-label="装车前审核分区"]' ). findAll ( 'button' )[ index ] ! . trigger ( 'click' ); await flushPromises ()
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
2026-10-21 15:10:54 +08:00
expect ( row . findAll ( 'button' ). map ( button => button . text ())). toEqual ([ '审核' , '详情' ])
expect (( row . props ( 'actions' ) as Array < { key : string } > ). map ( item => item . key )). toEqual ([ action , 'detail' ])
for ( const key of [ 'EDIT' , 'CANCEL' , 'SUBMIT_REVIEW' , 'route-order' , 'bol' ]) {
row . vm . $emit ( 'action' , key ); await flushPromises ()
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ( 'modelValue' )). toBe ( false )
}
expect ( getDispatchBolPdf ). not . toHaveBeenCalled ()
2026-10-21 14:30:33 +08:00
await row . get ( `button[aria-label=" ${ label } "]` ). trigger ( 'click' ); await flushPromises ()
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ()). toMatchObject ({ modelValue : true , action , dispatchNo : 'PC/1' })
wrapper . unmount ()
})
2026-10-21 13:22:42 +08:00
it . each ([ 'ADD_CARGO' , 'REMOVE_CARGO' ] as const )( 'recovers %s only inside smart dispatch' , async action => {
useDispatchCommandRecovery (() => identity . id ). begin ( 'PC-OTHER' , action , { recordVersion : 0 , idempotencyKey : 'cargo-key' }, action === 'REMOVE_CARGO' ? { itemId : '17' } : undefined )
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
await wrapper . findAll ( 'button' ). find ( button => button . text () === '核对原请求' ) ! . trigger ( 'click' )
expect ( wrapper . getComponent ({ name : 'SmartDispatchDialog' }). props ( 'modelValue' )). toBe ( true )
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ( 'modelValue' )). toBe ( false )
wrapper . unmount ()
})
2026-10-21 15:10:54 +08:00
it . each ([ 1 , 2 ])( 'keeps only details in audit tab %s without its review permission' , async index => {
can . mockReturnValue ( false )
const allowed = { allowed : true , blockCode : null , blockReason : null }
vi . mocked ( listDispatches ). mockResolvedValue ( page ([ listRow ({ actions : { EDIT : allowed , DISPATCH_REVIEW : allowed , LOADING_REVIEW : allowed } })]))
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
await wrapper . get ( '[aria-label="装车前审核分区"]' ). findAll ( 'button' )[ index ] ! . trigger ( 'click' ); await flushPromises ()
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
expect (( row . props ( 'actions' ) as Array < { key : string } > ). map ( item => item . key )). toEqual ([ 'detail' ])
wrapper . unmount ()
})
2026-10-21 14:30:33 +08:00
it ( 'shows four working phases and uses server stage counts instead of current page length' , async () => {
2026-10-19 17:56:31 +08:00
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
2026-10-20 23:34:11 +08:00
const tabs = wrapper . get ( '.data-table__toolbar-start .workbench-list-tabs[aria-label="排车执行阶段"]' ). findAll ( 'button' )
2026-10-20 23:49:47 +08:00
expect ( wrapper . find ( '.data-table__filters [aria-label="装车前审核分区"]' ). exists ()). toBe ( true )
2026-10-21 14:30:33 +08:00
expect ( tabs . map ( tab => tab . text ())). toEqual ([ '装车前 3' , '装车中 1' , '派送中 1' , '派送完成 1' ])
2026-10-19 17:56:31 +08:00
expect ( listDispatches ). toHaveBeenCalledWith ( expect . objectContaining ({ stage : 'BEFORE_LOADING' , status : 'ACTIVE' , beforeLoadingSection : 'VEHICLE_INFO' }))
2026-10-21 14:30:33 +08:00
await tabs [ 2 ] ! . trigger ( 'click' ); await flushPromises ()
2026-10-19 17:56:31 +08:00
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ stage : 'DELIVERING' })); expect ( wrapper . find ( '[aria-label="装车前审核分区"]' ). exists ()). toBe ( false ); wrapper . unmount ()
})
2026-10-21 16:58:38 +08:00
it . each ([ 0 , 1 , 2 ])( 'preserves section %s and its query after clearing the active dispatch filter' , async index => {
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const sectionValues = [ 'VEHICLE_INFO' , 'DISPATCH_REVIEW' , 'LOADING_REVIEW' ]
const sectionSelector = '[aria-label="装车前审核分区"]'
await wrapper . get ( sectionSelector ). findAll ( 'button' )[ index ] ! . trigger ( 'click' ); await flushPromises ()
const toolbar = wrapper . getComponent ( QueryToolbar )
await toolbar . findAll ( 'button' ). find ( button => button . text () === '高级筛选' ) ! . trigger ( 'click' )
toolbar . findAllComponents ( ElSelect ). find ( select => select . props ( 'placeholder' ) === '单据范围' ) ! . vm . $emit ( 'update:modelValue' , '' )
await flushPromises ()
expect ( wrapper . get ( sectionSelector ). findAll ( 'button' ). map ( button => button . text ())). toEqual ([ '车辆信息' , '调度审核' , '理货员审核' ])
expect ( wrapper . get ( sectionSelector ). findAll ( 'button' )[ index ] ! . attributes ( 'aria-selected' )). toBe ( 'true' )
await toolbar . findAll ( 'button' ). find ( button => button . text () === '查询' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ status : 'ACTIVE' , beforeLoadingSection : sectionValues [ index ] }))
const nextIndex = ( index + 1 ) % 3
await wrapper . get ( sectionSelector ). findAll ( 'button' )[ nextIndex ] ! . trigger ( 'click' ); await flushPromises ()
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ status : 'ACTIVE' , beforeLoadingSection : sectionValues [ nextIndex ] }))
await toolbar . findAll ( 'button' ). find ( button => button . text () === '重置' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( wrapper . get ( sectionSelector ). findAll ( 'button' )). toHaveLength ( 3 )
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ status : 'ACTIVE' , beforeLoadingSection : sectionValues [ nextIndex ] }))
await wrapper . get ( '[aria-label="排车执行阶段"]' ). findAll ( 'button' )[ 1 ] ! . trigger ( 'click' ); await flushPromises ()
expect ( wrapper . find ( sectionSelector ). exists ()). toBe ( false )
expect ( vi . mocked ( listDispatches ). mock . calls . at ( - 1 ) ? .[ 0 ]). not . toHaveProperty ( 'beforeLoadingSection' )
wrapper . unmount ()
})
it . each ([
[ 'WAIT_DEPARTURE' , 'BEFORE_LOADING' , '装车前' ],
[ 'DELIVERING' , 'DELIVERING' , '派送中' ],
[ 'DELIVERED' , 'DELIVERED' , '派送完成' ],
] as const )( 'queries %s through server pagination and selects the corresponding stage' , async ( vehicleStatus , stage , label ) => {
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
await wrapper . get ( '[aria-label="排车执行阶段"]' ). findAll ( 'button' )[ 3 ] ! . trigger ( 'click' ); await flushPromises ()
const table = wrapper . getComponent ( DataTable )
table . vm . $emit ( 'update:page' , 3 ); await flushPromises ()
const result = { ... page ([ listRow ({ stage })]), total : 21 , pages : 3 }
vi . mocked ( listDispatches ). mockResolvedValueOnce ( result )
const toolbar = wrapper . getComponent ( QueryToolbar )
const select = toolbar . findAllComponents ( ElSelect ). find ( item => item . props ( 'placeholder' ) === '车辆状态' ) !
select . vm . $emit ( 'update:modelValue' , vehicleStatus ); select . vm . $emit ( 'change' , vehicleStatus ); await flushPromises ()
await toolbar . findAll ( 'button' ). find ( button => button . text () === '查询' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 1 , size : 10 , stage , status : 'ACTIVE' }))
expect ( vi . mocked ( listDispatches ). mock . calls . at ( - 1 ) ? .[ 0 ]). not . toHaveProperty ( 'vehicleStatus' )
expect ( wrapper . get ( '[aria-label="排车执行阶段"] [aria-selected="true"]' ). text ()). toContain ( label )
expect ( table . props ( 'total' )). toBe ( 21 )
expect ( table . props ( 'data' )). toEqual ( result . records )
expect ( wrapper . find ( '[aria-label="装车前审核分区"]' ). exists ()). toBe ( stage === 'BEFORE_LOADING' )
wrapper . unmount ()
})
it ( 'keeps waiting vehicles in the current loading phase and synchronizes an active status when changing phases' , async () => {
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const tabs = wrapper . get ( '[aria-label="排车执行阶段"]' ). findAll ( 'button' )
await tabs [ 1 ] ! . trigger ( 'click' ); await flushPromises ()
const toolbar = wrapper . getComponent ( QueryToolbar )
toolbar . vm . $emit ( 'search' , { vehicleStatus : 'WAIT_DEPARTURE' , status : 'ACTIVE' }); await flushPromises ()
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ stage : 'LOADING' }))
expect ( vi . mocked ( listDispatches ). mock . calls . at ( - 1 ) ? .[ 0 ]). not . toHaveProperty ( 'beforeLoadingSection' )
await tabs [ 2 ] ! . trigger ( 'click' ); await flushPromises ()
expect ( toolbar . props ( 'modelValue' ). vehicleStatus ). toBe ( 'DELIVERING' )
await tabs [ 0 ] ! . trigger ( 'click' ); await flushPromises ()
expect ( toolbar . props ( 'modelValue' ). vehicleStatus ). toBe ( 'WAIT_DEPARTURE' )
wrapper . unmount ()
})
it ( 'shows trip progress independently of master vehicle status and leaves cancelled vehicles without progress' , async () => {
const records = [
listRow ({ dispatchNo : 'PC-WAIT' , stage : 'BEFORE_LOADING' }),
listRow ({ dispatchNo : 'PC-LOADING' , stage : 'LOADING' }),
listRow ({ dispatchNo : 'PC-DELIVERING' , stage : 'DELIVERING' }),
listRow ({ dispatchNo : 'PC-DELIVERED' , stage : 'DELIVERED' }),
listRow ({ dispatchNo : 'PC-CANCELLED' , status : 'CANCELLED' }),
]
for ( const row of records ) row . resources . vehicle ! . vehicleStatus = 'MAINTENANCE'
vi . mocked ( listDispatches ). mockResolvedValueOnce ( page ( records ))
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const toolbar = wrapper . getComponent ( QueryToolbar )
expect ( toolbar . props ( 'fields' ). map ( field => field . key )). toEqual ([ 'dispatchNo' , 'vehiclePlate' , 'vehicleStatus' ])
expect ( toolbar . props ( 'fields' ). find ( field => field . key === 'vehicleStatus' ) ? . options ? . map ( option => option . label )). toEqual ([ '待发车' , '配送中' , '已送达' ])
expect ( wrapper . getComponent ( DataTable ). props ( 'columns' ). some ( column => column . key === 'vehicleStatus' && column . label === '车辆状态' )). toBe ( true )
expect ( wrapper . findAllComponents ( ElTag ). map ( tag => tag . text ())). toEqual ([ '待发车' , '待发车' , '配送中' , '已送达' ])
expect ( wrapper . text ()). toContain ( '已取消' )
expect ( wrapper . text ()). not . toContain ( '维修中' )
wrapper . unmount ()
})
it . each ([ 'CANCELLED' , 'ALL' ])( 'retains advanced %s lookup and resets its hidden value' , async status => {
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const toolbar = wrapper . getComponent ( QueryToolbar )
toolbar . vm . $emit ( 'search' , { status : 'ACTIVE' , vehicleStatus : 'WAIT_DEPARTURE' }); await flushPromises ()
await toolbar . findAll ( 'button' ). find ( button => button . text () === '高级筛选' ) ! . trigger ( 'click' )
const select = toolbar . findAllComponents ( ElSelect ). find ( item => item . props ( 'placeholder' ) === '单据范围' ) !
select . vm . $emit ( 'update:modelValue' , status ); select . vm . $emit ( 'change' , status ); await flushPromises ()
await toolbar . findAll ( 'button' ). find ( button => button . text () === '查询' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( toolbar . props ( 'modelValue' ). vehicleStatus ). toBe ( '' )
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ status }))
expect ( vi . mocked ( listDispatches ). mock . calls . at ( - 1 ) ? .[ 0 ]). not . toHaveProperty ( 'beforeLoadingSection' )
await toolbar . findAll ( 'button' ). find ( button => button . text (). startsWith ( '收起筛选' )) ! . trigger ( 'click' )
expect ( toolbar . props ( 'fields' ). some ( field => field . key === 'status' )). toBe ( false )
expect ( toolbar . text ()). toContain ( status === 'CANCELLED' ? '高级筛选 · 已取消' : '高级筛选 · 全部单据' )
await toolbar . findAll ( 'button' ). find ( button => button . text () === '重置' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( toolbar . props ( 'modelValue' )). toMatchObject ({ status : 'ACTIVE' , vehicleStatus : '' })
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ page : 1 , status : 'ACTIVE' , beforeLoadingSection : 'VEHICLE_INFO' }))
wrapper . unmount ()
})
it ( 'returns to active dispatches when selecting vehicle progress from cancelled lookup' , async () => {
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const toolbar = wrapper . getComponent ( QueryToolbar )
toolbar . vm . $emit ( 'search' , { status : 'CANCELLED' , vehicleStatus : '' }); await flushPromises ()
const select = toolbar . findAllComponents ( ElSelect ). find ( item => item . props ( 'placeholder' ) === '车辆状态' ) !
select . vm . $emit ( 'update:modelValue' , 'DELIVERING' ); select . vm . $emit ( 'change' , 'DELIVERING' ); await flushPromises ()
await toolbar . findAll ( 'button' ). find ( button => button . text () === '查询' ) ! . trigger ( 'click' ); await flushPromises ()
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ status : 'ACTIVE' , stage : 'DELIVERING' }))
expect ( toolbar . props ( 'modelValue' )). toMatchObject ({ status : 'ACTIVE' , vehicleStatus : 'DELIVERING' })
wrapper . unmount ()
})
2026-10-21 14:30:33 +08:00
it ( 'queries the current review queue without offering historical result filters' , async () => {
2026-10-19 17:56:31 +08:00
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
await wrapper . get ( '[aria-label="装车前审核分区"]' ). findAll ( 'button' )[ 1 ] ! . trigger ( 'click' ); await flushPromises ()
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ beforeLoadingSection : 'DISPATCH_REVIEW' }))
2026-10-21 14:30:33 +08:00
expect ( wrapper . getComponent ( QueryToolbar ). props ( 'fields' ). some (( field : { key : string }) => field . key === 'reviewStatus' )). toBe ( false )
wrapper . getComponent ( QueryToolbar ). vm . $emit ( 'search' , { status : 'ACTIVE' , reviewStatus : 'REJECTED' , dispatchNo : '' , vehiclePlate : '' }); await flushPromises ()
expect ( vi . mocked ( listDispatches ). mock . calls . at ( - 1 ) ? .[ 0 ]). not . toHaveProperty ( 'reviewStatus' ); wrapper . unmount ()
})
it . each ([ 'dispatchReview' , 'loadingReview' ] as const )( 'shows %s rejection in the initial customer-service section' , async reviewKey => {
const row = listRow ()
row . reviews [ reviewKey ] = { ... row . reviews [ reviewKey ], status : 'REJECTED' , reason : '车辆证照需补齐' }
vi . mocked ( listDispatches ). mockResolvedValue ( page ([ row ]))
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
expect ( listDispatches ). toHaveBeenLastCalledWith ( expect . objectContaining ({ beforeLoadingSection : 'VEHICLE_INFO' }))
expect ( wrapper . text ()). toContain ( ` ${ reviewKey === 'dispatchReview' ? '调度审核' : '理货员审核' } 退回:车辆证照需补齐` )
wrapper . unmount ()
2026-10-19 17:56:31 +08:00
})
it ( 'discards a late page response after switching phase' , async () => {
let old !: ( value : DispatchPage ) => void
vi . mocked ( listDispatches ). mockImplementationOnce (() => new Promise ( resolve => { old = resolve }))
const wrapper = mount ( DispatchesView , options )
await wrapper . get ( '[aria-label="排车执行阶段"]' ). findAll ( 'button' )[ 1 ] ! . trigger ( 'click' ); await flushPromises ()
old ( page ([])); await flushPromises (); expect ( wrapper . text ()). toContain ( 'PC/1' ); wrapper . unmount ()
})
it ( 'gates every mutation with both its literal service capability and independent permission' , async () => {
vi . mocked ( listDispatches ). mockResolvedValue ( page ([ listRow ({ actions : { DEPART : { allowed : false , blockReason : '未安排' , blockCode : 'NO' }, EDIT : { allowed : true , blockCode : null , blockReason : null } } })]))
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const actions = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) ! . props ( 'actions' ) as Array < { key : string } >
expect ( actions . map ( row => row . key )). toContain ( 'EDIT' ); expect ( actions . map ( row => row . key )). not . toContain ( 'DEPART' )
can . mockReturnValue ( false ); wrapper . unmount ()
const readonly = mount ( DispatchesView , options ); await flushPromises (); expect (( readonly . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) ! . props ( 'actions' ) as Array < { key : string } > ). map ( row => row . key )). toEqual ([ 'detail' , 'bol' ]); readonly . unmount ()
})
it ( 'keeps server errors visible with the integrated query and retry context' , async () => {
vi . mocked ( listDispatches ). mockRejectedValueOnce ( new Error ( 'service unavailable' ))
const wrapper = mount ( DispatchesView , options ); await flushPromises (); expect ( wrapper . text ()). toContain ( '排车列表加载失败,请重试' ); expect ( wrapper . findComponent ( QueryToolbar ). exists ()). toBe ( true ); wrapper . unmount ()
})
it ( 'navigates by the business number without arranging a separate trip' , async () => {
const wrapper = mount ( DispatchesView , options ); await flushPromises (); wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) ! . vm . $emit ( 'action' , 'detail' ); await flushPromises ()
expect ( push ). toHaveBeenCalledWith ({ name : 'overseas-dispatch-detail' , params : { dispatchNo : 'PC/1' } }); expect ( wrapper . text ()). not . toContain ( '安排车次' ); wrapper . unmount ()
})
2026-10-20 21:33:46 +08:00
it . each ([ 'PICKUP' , 'LOCAL_TRUCK' ] as const )( 'opens %s through the existing dispatch detail and omits warehouse commands' , async taskType => {
vi . mocked ( listDispatches ). mockResolvedValueOnce ( page ([ listRow ({ taskType })]))
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
2026-10-21 15:53:19 +08:00
expect ( row . props ( 'actions' )). toEqual ([ expect . objectContaining ({ key : 'detail' , label : taskType === 'PICKUP' ? '详情' : '运输操作' })])
2026-10-20 21:33:46 +08:00
row . vm . $emit ( 'action' , 'detail' ); await flushPromises ()
expect ( push ). toHaveBeenCalledWith ({ name : 'overseas-dispatch-detail' , params : { dispatchNo : 'PC/1' } })
expect ( wrapper . text ()). toContain ( '安排到仓取货' ); expect ( wrapper . text ()). toContain ( '安排本土卡车' ); wrapper . unmount ()
})
2026-10-21 15:53:19 +08:00
it ( 'keeps pickup workflow actions but hides route ordering and BOL even when editing is allowed' , async () => {
const allowed = { allowed : true , blockCode : null , blockReason : null }
vi . mocked ( listDispatches ). mockResolvedValueOnce ( page ([ listRow ({ taskType : 'PICKUP' , actions : { EDIT : allowed , SUBMIT_REVIEW : allowed , CANCEL : allowed } })]))
const wrapper = mount ( DispatchesView , { global : { ... options . global , stubs : { ... options . global . stubs , TransportExecutionPanel : true , AppDialog : { template : '<div><slot /></div>' } } } }); await flushPromises ()
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
expect (( row . props ( 'actions' ) as Array < { key : string } > ). map ( item => item . key )). toEqual ([ 'EDIT' , 'SUBMIT_REVIEW' , 'CANCEL' , 'detail' ])
row . vm . $emit ( 'action' , 'EDIT' ); await flushPromises ()
expect ( wrapper . getComponent ({ name : 'TransportExecutionPanel' }). props ( 'dispatchNo' )). toBe ( 'PC/1' )
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ( 'modelValue' )). toBe ( false )
row . vm . $emit ( 'action' , 'SUBMIT_REVIEW' ); await flushPromises ()
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ()). toMatchObject ({ modelValue : true , action : 'SUBMIT_REVIEW' , dispatchNo : 'PC/1' })
wrapper . unmount ()
})
2026-10-19 17:56:31 +08:00
it ( 'reads one BOL on demand and preserves popup failure handling' , async () => {
vi . stubGlobal ( 'open' , vi . fn (). mockReturnValue ( null )); const wrapper = mount ( DispatchesView , options ); await flushPromises (); wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) ! . vm . $emit ( 'action' , 'bol' ); await flushPromises (); expect ( getDispatchBolPdf ). not . toHaveBeenCalled (); wrapper . unmount ()
2026-08-21 11:20:09 +08:00
})
2026-10-20 14:25:01 +08:00
it . each ([ 'saved' , 'rejected' ] as const )( 'refreshes list actions after workflow %s without retaining a stale submit button' , async event => {
vi . mocked ( listDispatches ). mockResolvedValueOnce ( page ([ listRow ({ actions : { SUBMIT_REVIEW : { allowed : true , blockCode : null , blockReason : null } } })]))
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
expect ( row . props ( 'actions' )). toContainEqual ( expect . objectContaining ({ key : 'SUBMIT_REVIEW' }))
row . vm . $emit ( 'action' , 'SUBMIT_REVIEW' ); await flushPromises ()
vi . mocked ( listDispatches ). mockResolvedValueOnce ( page ([ listRow ({ recordVersion : 1 , actions : { SUBMIT_REVIEW : { allowed : false , blockCode : 'ALREADY_SUBMITTED' , blockReason : '审核已提交' } } })]))
const dialog = wrapper . getComponent ({ name : 'DispatchWorkflowDialog' })
if ( event === 'rejected' ) dialog . vm . $emit ( 'rejected' , { message : '本轮审核已经提交' , code : 'A0409' , requestId : 'list-reject-request' })
else dialog . vm . $emit ( 'saved' )
dialog . vm . $emit ( 'update:modelValue' , false ); await flushPromises ()
expect ( listDispatches ). toHaveBeenCalledTimes ( 2 )
expect ( row . props ( 'actions' )). not . toContainEqual ( expect . objectContaining ({ key : 'SUBMIT_REVIEW' }))
if ( event === 'rejected' ) {
expect ( wrapper . text ()). toContain ( '本轮审核已经提交' )
expect ( wrapper . text ()). toContain ( 'A0409' )
expect ( wrapper . text ()). toContain ( 'list-reject-request' )
}
expect ( wrapper . text ()). not . toContain ( '结果尚待核对' )
wrapper . unmount ()
})
it ( 'describes a proven command awaiting its GET as completed instead of unknown' , async () => {
const recovery = useDispatchCommandRecovery (() => identity . id )
recovery . begin ( 'PC/1' , 'SUBMIT_REVIEW' , { recordVersion : 0 , idempotencyKey : 'known-key' })
recovery . pending . value ! . attempted = true
recovery . pending . value ! . result = command ( 'SUBMIT_REVIEW' , 'known-key' )
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
expect ( wrapper . text ()). toContain ( '操作已完成,页面资料待刷新' )
expect ( wrapper . text ()). not . toContain ( '结果尚待核对' )
await wrapper . findAll ( 'button' ). find ( button => button . text () === '刷新页面资料' ) ! . trigger ( 'click' )
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ()). toMatchObject ({ modelValue : true , action : 'SUBMIT_REVIEW' , dispatchNo : 'PC/1' })
wrapper . unmount ()
})
it ( 'clears an earlier input rejection after the corrected workflow succeeds' , async () => {
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const dialog = wrapper . getComponent ({ name : 'DispatchWorkflowDialog' })
dialog . vm . $emit ( 'rejected' , { message : '请补齐本趟资料' , code : 'A0400' }); await flushPromises ()
expect ( wrapper . text ()). toContain ( '请补齐本趟资料' )
dialog . vm . $emit ( 'saved' ); await flushPromises ()
expect ( wrapper . text ()). not . toContain ( '请补齐本趟资料' )
wrapper . unmount ()
})
2026-10-21 15:49:35 +08:00
it ( 'opens the exception form from the dispatching row and hides it without permission' , async () => {
const gate = { allowed : true , blockCode : null , blockReason : null }
vi . mocked ( listDispatches ). mockResolvedValue ( page ([ listRow ({ stage : 'DELIVERING' , actions : { MARK_EXCEPTION : gate } })]))
const wrapper = mount ( DispatchesView , options ); await flushPromises ()
const row = wrapper . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
expect ( row . props ( 'actions' )). toEqual ( expect . arrayContaining ([ expect . objectContaining ({ key : 'MARK_EXCEPTION' , label : '标记异常' , tone : 'danger' })]))
row . vm . $emit ( 'action' , 'MARK_EXCEPTION' ); await flushPromises ()
expect ( wrapper . getComponent ({ name : 'DispatchWorkflowDialog' }). props ()). toMatchObject ({ modelValue : true , action : 'MARK_EXCEPTION' , dispatchNo : 'PC/1' })
wrapper . unmount (); can . mockReturnValue ( false )
const denied = mount ( DispatchesView , options ); await flushPromises ()
const deniedRow = denied . findAllComponents ( RowActions ). find ( entry => entry . props ( 'moreLabel' ) === '更多操作 · PC/1' ) !
expect (( deniedRow . props ( 'actions' ) as Array < { key : string } > ). map ( item => item . key )). not . toContain ( 'MARK_EXCEPTION' )
denied . unmount ()
})