import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, expect, it, vi } from 'vitest' import WorkSharedTripForm from './WorkSharedTripForm.vue' import { getDispatch, useDispatchCommandRecovery, type DispatchDetail } from '@/modules/overseas-warehouse' import type { LeaveGuard } from './useWorkDraft' const access = vi.hoisted(() => ({ allowed: true })) const identity = vi.hoisted(() => ({ id: '' })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => access.allowed }) })) vi.mock('@/core/auth/auth', () => ({ useAuthStore: () => ({ user: identity }) })) vi.mock('@/modules/overseas-warehouse', async original => ({ ...await original(), getDispatch: vi.fn() })) vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn() } })) let guard: LeaveGuard function render() { return mount(WorkSharedTripForm, { props: { dispatchNo: 'DS-60', canWrite: true }, global: { provide: { 'appointment-work-leave-guard': (value: LeaveGuard) => { guard = value; return () => undefined } }, stubs: { ElButton: { props: ['disabled'], template: '' }, DispatchWorkflowDialog: { name: 'DispatchWorkflowDialog', props: ['modelValue', 'dispatchNo', 'action'], template: '
' }, LoadErrorState: { props: ['message'], template: '

{{ message }}

' }, ApiErrorMeta: true } } }) } function detail(no = 'DS-60', allowed = true) { return { dispatchNo: no, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: 0, counts: { packageCount: 60 }, vehicleInfo: { vehicle: null, driver: null }, actions: { ADD_CARGO: { allowed, blockCode: allowed ? null : 'PICKED', blockReason: allowed ? null : '货物已开始真实拣货' } } } as DispatchDetail } const button = (wrapper: ReturnType, label: string) => wrapper.findAll('button').find(row => row.text() === label)! beforeEach(() => { vi.clearAllMocks(); sessionStorage.clear(); identity.id = crypto.randomUUID(); access.allowed = true; vi.mocked(getDispatch).mockResolvedValue(detail()) }) it('opens the native add-cargo workflow for the same dispatch instead of asking for another customer dispatch', async () => { const wrapper = render(); await flushPromises(); expect(getDispatch).toHaveBeenCalledExactlyOnceWith('DS-60'); expect(wrapper.find('input').exists()).toBe(false) expect(wrapper.text()).toContain('60 箱'); await button(wrapper, '选择同仓未安排货物').trigger('click') const dialog = wrapper.getComponent({ name: 'DispatchWorkflowDialog' }); expect(dialog.props()).toMatchObject({ modelValue: true, dispatchNo: 'DS-60', action: 'ADD_CARGO' }); expect(await guard()).toBe(false) dialog.vm.$emit('saved'); await flushPromises(); expect(wrapper.emitted('updated')).toHaveLength(1); expect(await guard()).toBe(true); wrapper.unmount() }) it('honors native stage/gate and permissions and rejects a different returned business number', async () => { vi.mocked(getDispatch).mockResolvedValueOnce({ ...detail('DS-60', false), stage: 'LOADING' }); const scanned = render(); await flushPromises(); expect(button(scanned, '选择同仓未安排货物').attributes('disabled')).toBeDefined(); expect(scanned.text()).toContain('货物已开始真实拣货'); scanned.unmount() vi.mocked(getDispatch).mockResolvedValueOnce(detail('WRONG')); const wrong = render(); await flushPromises(); expect(wrong.get('[role="alert"]').text()).toContain('原排车单不一致'); wrong.unmount() access.allowed = false; const count = vi.mocked(getDispatch).mock.calls.length, denied = render(); await flushPromises(); expect(getDispatch).toHaveBeenCalledTimes(count); denied.unmount() }) it('ignores an old add command without blocking the calendar or another action',async()=>{ sessionStorage.setItem('dispatch-command:'+identity.id,JSON.stringify({dispatchNo:'DS-60',action:'ADD_CARGO',attempted:true,body:{idempotencyKey:'old'}})) const wrapper=render();await flushPromises() expect(button(wrapper,'选择同仓未安排货物').attributes('disabled')).toBeUndefined() expect(await guard()).toBe(true) expect(sessionStorage.getItem('dispatch-command:'+identity.id)).toBeNull() expect(wrapper.text()).not.toContain('原排车请求尚未核对') wrapper.unmount() }) it('retries lookup as a read and keeps the original detail navigation', async () => { vi.mocked(getDispatch).mockRejectedValueOnce(new Error('lookup failed')); const wrapper = render(); await flushPromises(); await wrapper.get('[role="alert"] button').trigger('click'); await flushPromises(); expect(getDispatch).toHaveBeenCalledTimes(2) await button(wrapper, '查看原排车单').trigger('click'); expect(wrapper.emitted('external')).toEqual([['/overseas-warehouse/dispatches/DS-60']]); wrapper.unmount() }) it.each([false, true])('refreshes the original add-cargo gate after a rejection and preserves its message when GET fails: %s', async readFails => { const wrapper = render(); await flushPromises(); await button(wrapper, '选择同仓未安排货物').trigger('click') if (readFails) vi.mocked(getDispatch).mockRejectedValueOnce(new Error('read unavailable')) else vi.mocked(getDispatch).mockResolvedValueOnce(detail('DS-60', false)) const dialog = wrapper.getComponent({ name: 'DispatchWorkflowDialog' }) dialog.vm.$emit('rejected', { message: '来源已变化,请重新核对', code: 'A0409' }) dialog.vm.$emit('update:modelValue', false); await flushPromises() expect(getDispatch).toHaveBeenCalledTimes(2) expect(wrapper.emitted('updated')).toHaveLength(1) expect(wrapper.text()).toContain('来源已变化,请重新核对') expect(button(wrapper, '选择同仓未安排货物').attributes('disabled')).toBeDefined() expect(wrapper.text()).not.toContain('原排车请求尚未核对') if (!readFails) expect(wrapper.text()).toContain('货物已开始真实拣货') wrapper.unmount() }) it('does not offer a stored command replay when opening the form again',async()=>{ sessionStorage.setItem('dispatch-command:'+identity.id,JSON.stringify({dispatchNo:'DS-60',action:'ADD_CARGO',attempted:true,result:{recordVersion:1}})) const wrapper=render();await flushPromises() expect(wrapper.text()).not.toContain('刷新加货资料') expect(button(wrapper,'选择同仓未安排货物').attributes('disabled')).toBeUndefined() expect(useDispatchCommandRecovery(()=>identity.id).pending.value).toBeNull() wrapper.unmount() }) it('clears a prior add-cargo rejection after the corrected original-dispatch workflow succeeds', async () => { const wrapper = render(); await flushPromises() const dialog = wrapper.getComponent({ name: 'DispatchWorkflowDialog' }) dialog.vm.$emit('rejected', { message: '来源版本需重新核对', code: 'A0409' }); await flushPromises() expect(wrapper.text()).toContain('来源版本需重新核对') dialog.vm.$emit('saved'); await flushPromises() expect(wrapper.text()).not.toContain('来源版本需重新核对') expect(wrapper.emitted('updated')).toHaveLength(2) wrapper.unmount() })