436 lines
36 KiB
TypeScript
436 lines
36 KiB
TypeScript
import { createPinia } from 'pinia'
|
|
import ElementPlus, { ElMessageBox, ElSelect, ElTag } from 'element-plus'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { beforeEach, expect, it, vi } from 'vitest'
|
|
import DispatchesView from './DispatchesView.vue'
|
|
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
|
import DataTable from '@/shared/components/DataTable.vue'
|
|
import RowActions from '@/shared/components/RowActions.vue'
|
|
import { getDispatch, getDispatchBolPdf, listDispatches } from '../api/dispatches'
|
|
import { cancelTransport, getTransport } from '../api/transportTasks'
|
|
import { command, detail, listRow, page, reviews } from '../dispatchTestFixtures'
|
|
import type { DispatchPage } from '../dispatchContracts'
|
|
import { useDispatchCommandRecovery } from '../dispatchCommandRecovery'
|
|
const identity = vi.hoisted(() => ({ id: '' }))
|
|
const push = vi.hoisted(() => vi.fn())
|
|
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() }))
|
|
vi.mock('../api/transportTasks', () => ({ transportLabels: { DELIVERY: '仓库派送', PICKUP: '到仓取货', LOCAL_TRUCK: '本土卡车' }, cancelTransport: vi.fn(), getTransport: vi.fn() }))
|
|
const options = { global: { plugins: [createPinia(), ElementPlus], stubs: { SmartDispatchDialog: true, DispatchWorkflowDialog: true, TransportTaskCreateDialog: true, TransportExecutionPanel: true } } }
|
|
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()) })
|
|
it('offers pickup detail, transport vehicle maintenance and the shared review workflow', async () => {
|
|
const allowed = { allowed: true, blockCode: null, blockReason: null }
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ taskType: 'PICKUP', actions: { EDIT: allowed, SUBMIT_REVIEW: allowed, CANCEL: allowed } })]))
|
|
const wrapper = mount(DispatchesView, options); await flushPromises()
|
|
const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
|
expect(row.props('actions').map((item: { key: string }) => item.key)).toEqual(['EDIT', 'SUBMIT_REVIEW', 'CANCEL', 'detail'])
|
|
expect(row.findAll('button').map(button => button.text())).toEqual(['维护车辆', '提交调度审核', '详情', '更多'])
|
|
row.vm.$emit('action', 'EDIT'); await flushPromises()
|
|
expect(wrapper.getComponent({ name: 'TransportExecutionPanel' }).props('dispatchNo')).toBe('PC/1')
|
|
expect(wrapper.getComponent({ name: 'TransportExecutionPanel' }).props('editResources')).toBe(true)
|
|
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()
|
|
})
|
|
it('confirms cancelling pickup and linked transfers using the transport version', async () => {
|
|
const allowed = { allowed: true, blockCode: null, blockReason: null }
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ taskType: 'PICKUP', recordVersion: 10, actions: { CANCEL: allowed } })]))
|
|
vi.mocked(getTransport).mockResolvedValue({ execution: { trip: { recordVersion: 4 } }, pickups: [{ transferNo: 'WT-1' }] } as Awaited<ReturnType<typeof getTransport>>)
|
|
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
|
|
const wrapper = mount(DispatchesView, options); await flushPromises()
|
|
const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
|
row.vm.$emit('action', 'CANCEL'); await flushPromises()
|
|
expect(confirm.mock.calls[0]?.[0]).toContain('WT-1')
|
|
expect(cancelTransport).toHaveBeenCalledWith('PC/1', { recordVersion: 4, reason: '平台取消取货排车及对应转运单' })
|
|
expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props('modelValue')).toBe(false)
|
|
expect(listDispatches).toHaveBeenCalledTimes(2)
|
|
wrapper.unmount(); confirm.mockRestore()
|
|
})
|
|
it.each([['LOADING', '上传装货照片'], ['DELIVERING', '上传到仓照片'], ['DELIVERED', '结束资源任务']] as const)('opens pickup %s work from the list while detail stays separate', async (stage, label) => {
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ taskType: 'PICKUP', stage, reviewState: 'APPROVED', actions: {} })]))
|
|
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.objectContaining({ key: 'transport', label }), expect.objectContaining({ key: 'detail', label: '详情' })])
|
|
row.vm.$emit('action', 'transport'); await flushPromises()
|
|
expect(wrapper.getComponent({ name: 'TransportExecutionPanel' }).props()).toMatchObject({ dispatchNo: 'PC/1', pickupApproved: true, editResources: false })
|
|
expect(push).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
it('does not expose pickup work for a cancelled order or an operator without edit permission', async () => {
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ taskType: 'PICKUP', stage: 'DELIVERING', reviewState: 'APPROVED', status: 'CANCELLED', actions: {} })]))
|
|
const wrapper = mount(DispatchesView, options); await flushPromises()
|
|
const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
|
expect(row.props('actions').map((item: { key: string }) => item.key)).toEqual(['detail'])
|
|
row.vm.$emit('action', 'transport'); await flushPromises()
|
|
expect(wrapper.findComponent({ name: 'TransportExecutionPanel' }).exists()).toBe(false)
|
|
wrapper.unmount()
|
|
can.mockReturnValue(false)
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ taskType: 'PICKUP', stage: 'DELIVERING', reviewState: 'APPROVED', actions: {} })]))
|
|
const readonly = mount(DispatchesView, options); await flushPromises()
|
|
expect(readonly.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!.props('actions').map((item: { key: string }) => item.key)).toEqual(['detail'])
|
|
readonly.unmount()
|
|
})
|
|
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 } })]))
|
|
const wrapper = mount(DispatchesView, options); await flushPromises()
|
|
expect(wrapper.findAll('[role="tab"]').some(tab => tab.text().startsWith('装车后'))).toBe(true)
|
|
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('keeps return-to-review on the loading list with its service gate and permission', async () => {
|
|
const allowed = { allowed: true, blockCode: null, blockReason: null }
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ stage: 'LOADING', actions: { RETURN_TO_REVIEW: allowed } })]))
|
|
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: 'RETURN_TO_REVIEW' })]))
|
|
row.vm.$emit('action', 'RETURN_TO_REVIEW'); await flushPromises()
|
|
expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props()).toMatchObject({ modelValue: true, action: 'RETURN_TO_REVIEW', 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').map((item: { key: string }) => item.key)).not.toContain('RETURN_TO_REVIEW')
|
|
deniedRow.vm.$emit('action', 'RETURN_TO_REVIEW'); await flushPromises()
|
|
expect(denied.getComponent({ name: 'DispatchWorkflowDialog' }).props('modelValue')).toBe(false)
|
|
denied.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 的确认装车完成结果尚待核对')
|
|
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()
|
|
})
|
|
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()
|
|
})
|
|
it('keeps vehicle maintenance but hides standalone review submission and route ordering', 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); await flushPromises()
|
|
const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
|
const keys = (row.props('actions') as Array<{ key: string }>).map(item => item.key)
|
|
expect(keys).toContain('EDIT'); expect(keys).toContain('detail')
|
|
expect(keys).not.toContain('SUBMIT_REVIEW'); expect(keys).not.toContain('route-order')
|
|
row.vm.$emit('action', 'SUBMIT_REVIEW'); row.vm.$emit('action', 'route-order'); await flushPromises()
|
|
expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props('modelValue')).toBe(false)
|
|
row.vm.$emit('action', 'EDIT'); await flushPromises()
|
|
expect(wrapper.getComponent({ name: 'DispatchWorkflowDialog' }).props()).toMatchObject({ modelValue: true, action: 'EDIT', 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 }
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ actions: { EDIT: allowed, CANCEL: allowed, SUBMIT_REVIEW: 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.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()
|
|
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()
|
|
})
|
|
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()
|
|
})
|
|
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()
|
|
})
|
|
it('shows five working phases and uses server stage counts instead of current page length', async () => {
|
|
const wrapper = mount(DispatchesView, options); await flushPromises()
|
|
const tabs = wrapper.get('.data-table__toolbar-start .workbench-list-tabs[aria-label="排车执行阶段"]').findAll('button')
|
|
expect(wrapper.find('.data-table__filters [aria-label="装车前审核分区"]').exists()).toBe(true)
|
|
expect(tabs.map(tab => tab.text())).toEqual(['装车前 3', '装车中 1', '装车后 1', '派送中 1', '派送完成 1'])
|
|
expect(listDispatches).toHaveBeenCalledWith(expect.objectContaining({ stage: 'BEFORE_LOADING', status: 'ACTIVE', beforeLoadingSection: 'VEHICLE_INFO' }))
|
|
await tabs[3]!.trigger('click'); await flushPromises()
|
|
expect(listDispatches).toHaveBeenLastCalledWith(expect.objectContaining({ stage: 'DELIVERING' })); expect(wrapper.find('[aria-label="装车前审核分区"]').exists()).toBe(false); wrapper.unmount()
|
|
})
|
|
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[3]!.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()
|
|
})
|
|
it('queries the current review queue without offering historical result filters', async () => {
|
|
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' }))
|
|
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()
|
|
})
|
|
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']); 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()
|
|
})
|
|
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')!
|
|
expect(row.props('actions')).toEqual([...(taskType === 'LOCAL_TRUCK' ? [expect.objectContaining({ key: 'transport', label: '运输操作' })] : []), expect.objectContaining({ key: 'detail', label: '详情' })])
|
|
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()
|
|
})
|
|
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()
|
|
})
|
|
it('reads one BOL on demand and preserves popup failure handling', async () => {
|
|
const current = reviews()
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ reviews: { ...current, dispatchReview: { ...current.dispatchReview, status: 'APPROVED' } } })]))
|
|
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()
|
|
})
|
|
it.each(['NOT_SUBMITTED', 'PENDING', 'REJECTED', 'APPROVED'] as const)('offers BOL only after current dispatch review is approved: %s', async status => {
|
|
const current = reviews()
|
|
vi.mocked(listDispatches).mockResolvedValue(page([listRow({ reviews: { ...current, dispatchReview: { ...current.dispatchReview, status } } })]))
|
|
const popup = { close: vi.fn(), location: { href: '' } }
|
|
vi.stubGlobal('open', vi.fn().mockReturnValue(popup))
|
|
vi.mocked(getDispatchBolPdf).mockRejectedValue(new Error('print unavailable'))
|
|
const wrapper = mount(DispatchesView, options); await flushPromises()
|
|
const actions = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
|
|
expect((actions.props('actions') as Array<{ key: string }>).some(item => item.key === 'bol')).toBe(status === 'APPROVED')
|
|
actions.vm.$emit('action', 'bol'); await flushPromises()
|
|
expect(getDispatchBolPdf).toHaveBeenCalledTimes(status === 'APPROVED' ? 1 : 0)
|
|
expect(window.open).toHaveBeenCalledTimes(status === 'APPROVED' ? 1 : 0)
|
|
if (status === 'APPROVED') expect(popup.close).toHaveBeenCalledOnce()
|
|
wrapper.unmount()
|
|
})
|
|
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: { EDIT: { 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: 'EDIT' }))
|
|
row.vm.$emit('action', 'EDIT'); await flushPromises()
|
|
vi.mocked(listDispatches).mockResolvedValueOnce(page([listRow({ recordVersion: 1, actions: { EDIT: { 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: 'EDIT' }))
|
|
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()
|
|
})
|
|
|
|
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()
|
|
})
|