This commit is contained in:
2026-10-21 17:49:04 +08:00
parent 22e60248d2
commit 4f5cec1b94
20 changed files with 192 additions and 56 deletions
@@ -7,7 +7,7 @@ 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 { command, detail, listRow, page } from '../dispatchTestFixtures'
import { command, detail, listRow, page, reviews } from '../dispatchTestFixtures'
import type { DispatchPage } from '../dispatchContracts'
import { useDispatchCommandRecovery } from '../dispatchCommandRecovery'
const identity = vi.hoisted(() => ({ id: '' }))
@@ -54,17 +54,18 @@ it('centralizes cargo changes in smart dispatch and never exposes row add/remove
expect(wrapper.getComponent({ name: 'SmartDispatchDialog' }).props('modelValue')).toBe(true)
wrapper.unmount()
})
it('keeps vehicle maintenance and detail visible while submitting review remains available in the menu', async () => {
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, attachTo: document.body }); await flushPromises()
const wrapper = mount(DispatchesView, options); 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' })
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) => {
@@ -252,7 +253,7 @@ it('gates every mutation with both its literal service capability and independen
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()
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'))
@@ -285,21 +286,38 @@ it('keeps pickup workflow actions but hides route ordering and BOL even when edi
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: { SUBMIT_REVIEW: { allowed: true, blockCode: null, blockReason: null } } })]))
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: '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: '审核已提交' } } })]))
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: 'SUBMIT_REVIEW' }))
expect(row.props('actions')).not.toContainEqual(expect.objectContaining({ key: 'EDIT' }))
if (event === 'rejected') {
expect(wrapper.text()).toContain('本轮审核已经提交')
expect(wrapper.text()).toContain('A0409')