114 lines
9.7 KiB
TypeScript
114 lines
9.7 KiB
TypeScript
import { createPinia } from 'pinia'
|
|
import ElementPlus 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 RowActions from '@/shared/components/RowActions.vue'
|
|
import { getDispatch, getDispatchBolPdf, listDispatches } from '../api/dispatches'
|
|
import { command, detail, listRow, page } 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() }))
|
|
const options = { global: { plugins: [createPinia(), ElementPlus], stubs: { SmartDispatchDialog: true, DispatchWorkflowDialog: 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 recovery of the original pending command independently of the filtered list rows', async () => {
|
|
const pending = useDispatchCommandRecovery(() => identity.id); pending.begin('PC-OTHER', 'DEPART', { 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('shows exactly five phases and uses server stage counts instead of current page length', async () => {
|
|
const wrapper = mount(DispatchesView, options); await flushPromises()
|
|
const tabs = wrapper.get('[aria-label="排车执行阶段"]').findAll('button')
|
|
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('queries each review section on the server and includes only its legal review filter', 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' }))
|
|
wrapper.getComponent(QueryToolbar).vm.$emit('search', { status: 'ACTIVE', reviewStatus: 'PENDING', dispatchNo: '', vehiclePlate: '' }); await flushPromises()
|
|
expect(listDispatches).toHaveBeenLastCalledWith(expect.objectContaining({ reviewStatus: 'PENDING' })); 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', '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()
|
|
})
|
|
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()
|
|
})
|
|
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()
|
|
})
|