import { flushPromises, mount } from '@vue/test-utils' import ElementPlus from 'element-plus' import { beforeEach, describe, expect, it, vi } from 'vitest' import AppointmentImpactDialog from './AppointmentImpactDialog.vue' import { getImpact, handleImpact, type ImpactDetail } from './impactApi' vi.mock('./impactApi', async importOriginal => ({ ...await importOriginal(), getImpact: vi.fn(), handleImpact: vi.fn() })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) const detail: ImpactDetail = { impact: { containerBookingId: '1', impactNo: 'AI-TEST', version: 1, status: 'OPEN', reason: 'FORECAST_CHANGED', ownerId: null, ownerName: null, baseline: { estimatedAt: '2026-10-15T09:00:00', actualAt: null, observedAt: null, providerCode: 'carrier', sourceReference: 'ref' }, latest: { estimatedAt: '2026-10-17T09:00:00', actualAt: null, observedAt: null, providerCode: 'carrier', sourceReference: 'ref' }, forecastToken: 'current', updatedAt: '2026-10-10T12:00:00' }, cargo: [{ inboundCode: 'IN-A', customerName: '客户甲', packages: 10, destination: 'ONT8' }], appointments: [{ appointmentNo: 'AP-1', status: 'SUCCESS', destination: 'ONT8', confirmedAt: '2026-10-16T09:00:00', timezone: 'America/Los_Angeles' }], transport: null, history: { records: [], total: 0 } } const render = (canWrite = true) => mount(AppointmentImpactDialog, { props: { containerId: '1', canWrite }, global: { plugins: [ElementPlus], stubs: { AppDialog: { template: '
' } } } }) describe('appointment impact handling', () => { beforeEach(() => { vi.resetAllMocks(); vi.mocked(getImpact).mockResolvedValue(detail) }) it('shows separate forecasts, unchanged confirmed appointment and omitted transport permission', async () => { const wrapper = render(false); await flushPromises() expect(wrapper.text()).toContain('2026-10-15T09:00:00') expect(wrapper.text()).toContain('2026-10-17T09:00:00') expect(wrapper.text()).toContain('2026-10-16T09:00:00') expect(wrapper.text()).toContain('无车次查看权限') expect(wrapper.text()).not.toContain('认领跟进') expect(handleImpact).not.toHaveBeenCalled(); wrapper.unmount() }) it('freezes an uncertain command and retries exactly once with original version and key', async () => { vi.mocked(handleImpact).mockRejectedValueOnce(new Error('connection lost')).mockResolvedValueOnce({ ...detail.impact, version: 2 }) const wrapper = render(); await flushPromises() await wrapper.findAll('button').find(b => b.text() === '认领跟进')!.trigger('click'); await flushPromises() const original = vi.mocked(handleImpact).mock.calls[0]![1] expect(original).toMatchObject({ action: 'CLAIM', expectedVersion: 1, forecastToken: 'current' }) await wrapper.get('[data-close]').trigger('click'); expect(wrapper.emitted('close')).toBeUndefined() await wrapper.findAll('button').find(b => b.text() === '重试本次处理')!.trigger('click'); await flushPromises() expect(handleImpact).toHaveBeenNthCalledWith(2, '1', original) expect(wrapper.emitted('saved')).toHaveLength(1) expect(getImpact).toHaveBeenCalledTimes(2); wrapper.unmount() }) it('does not resolve without an actual handling result', async () => { const wrapper = render(); await flushPromises() await wrapper.findAll('button').find(b => b.text() === '完成本次核对')!.trigger('click'); await flushPromises() expect(handleImpact).not.toHaveBeenCalled(); wrapper.unmount() }) })