106 lines
8.6 KiB
TypeScript
106 lines
8.6 KiB
TypeScript
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 { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
|
|
import { getImpact, handleImpact, type ImpactDetail } from './impactApi'
|
|
vi.mock('./impactApi', async importOriginal => ({ ...await importOriginal<typeof import('./impactApi')>(), getImpact: vi.fn(), handleImpact: vi.fn() }))
|
|
const guards = vi.hoisted(() => ({ register: vi.fn(), unregister: vi.fn(), push: vi.fn() }))
|
|
vi.mock('vue-router', () => ({ onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn(), useRouter: () => ({ push: guards.push }) }))
|
|
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: { provide: { 'appointment-register-leave-guard': guards.register }, plugins: [ElementPlus], stubs: { AppDialog: { name: 'AppDialog', props: ['beforeClose'], template: '<section><slot /><button data-close @click="$emit(\'update:model-value\',false)">关闭</button></section>' } } } })
|
|
describe('appointment impact handling', () => {
|
|
beforeEach(() => { vi.resetAllMocks(); guards.register.mockReturnValue(guards.unregister); vi.mocked(getImpact).mockResolvedValue(detail) })
|
|
function deferred() {
|
|
let resolve!: (value: ImpactDetail) => void, reject!: (reason: Error) => void
|
|
const promise=new Promise<ImpactDetail>((yes,no)=>{resolve=yes;reject=no})
|
|
return {promise,resolve,reject}
|
|
}
|
|
const pageDetail=(label:string):ImpactDetail=>({...detail,history:{total:60,records:[{version:1,action:'RECORD',operatorName:null,result:label,nextAction:null,followupDate:null,createdAt:'2026-10-11T00:00:00'}]}})
|
|
it('keeps the latest history page when responses finish in reverse order',async()=>{
|
|
vi.mocked(getImpact).mockResolvedValueOnce(pageDetail('第一页'))
|
|
const wrapper=render();await flushPromises()
|
|
const second=deferred(),third=deferred()
|
|
vi.mocked(getImpact).mockReturnValueOnce(second.promise).mockReturnValueOnce(third.promise)
|
|
const pager=wrapper.findComponent({name:'ElPagination'})
|
|
pager.vm.$emit('update:current-page',2);pager.vm.$emit('current-change',2)
|
|
pager.vm.$emit('update:current-page',3);pager.vm.$emit('current-change',3)
|
|
await flushPromises()
|
|
third.resolve(pageDetail('第三页最新'));await flushPromises()
|
|
second.resolve(pageDetail('第二页过期'));await flushPromises()
|
|
expect(wrapper.text()).toContain('第三页最新');expect(wrapper.text()).not.toContain('第二页过期')
|
|
wrapper.unmount()
|
|
})
|
|
it('ignores an obsolete failure and keeps loading until the current request finishes',async()=>{
|
|
vi.mocked(getImpact).mockResolvedValueOnce(pageDetail('第一页'))
|
|
const wrapper=render();await flushPromises()
|
|
const second=deferred(),third=deferred()
|
|
vi.mocked(getImpact).mockReturnValueOnce(second.promise).mockReturnValueOnce(third.promise)
|
|
const pager=wrapper.findComponent({name:'ElPagination'})
|
|
pager.vm.$emit('update:current-page',2);pager.vm.$emit('current-change',2)
|
|
pager.vm.$emit('update:current-page',3);pager.vm.$emit('current-change',3)
|
|
second.reject(new Error('obsolete page failure'));await flushPromises()
|
|
expect(wrapper.text()).not.toContain('obsolete page failure')
|
|
expect(pager.props('disabled')).toBe(true)
|
|
expect(wrapper.findAll('button').find(b=>b.text()==='认领跟进')!.attributes('disabled')).toBeDefined()
|
|
wrapper.findAllComponents({name:'ElButton'}).find(b=>b.text()==='认领跟进')!.vm.$emit('click')
|
|
await flushPromises();expect(handleImpact).not.toHaveBeenCalled()
|
|
third.resolve(pageDetail('第三页最新'));await flushPromises()
|
|
expect(pager.props('disabled')).toBe(false);wrapper.unmount()
|
|
})
|
|
it('does not let a closed dialog response replace a newly opened impact',async()=>{
|
|
const old=deferred();vi.mocked(getImpact).mockReturnValueOnce(old.promise)
|
|
const first=render();await flushPromises();first.unmount()
|
|
vi.mocked(getImpact).mockResolvedValueOnce({...pageDetail('新影响历史'),impact:{...detail.impact,impactNo:'AI-NEW'}})
|
|
const next=render();await flushPromises()
|
|
old.resolve(pageDetail('已关闭历史'));await flushPromises()
|
|
expect(next.text()).toContain('AI-NEW');expect(next.text()).not.toContain('已关闭历史');next.unmount()
|
|
})
|
|
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()).toContain('来源:carrier')
|
|
expect(wrapper.text()).toContain('核对原因:到港预测变化')
|
|
expect(wrapper.text()).not.toContain('认领跟进')
|
|
expect(handleImpact).not.toHaveBeenCalled(); wrapper.unmount()
|
|
})
|
|
it('labels manual observations without exposing internal operator references and explains missing forecasts', async () => {
|
|
vi.mocked(getImpact).mockResolvedValueOnce({ ...detail, impact: { ...detail.impact, reason: 'FORECAST_MISSING', latest: { ...detail.impact.latest!, providerCode: 'MANUAL', sourceReference: 'MANUAL:1000001', estimatedAt: null } } })
|
|
const wrapper = render(); await flushPromises()
|
|
expect(wrapper.text()).toContain('来源:人工维护')
|
|
expect(wrapper.text()).toContain('来源预计到港缺失,待核实')
|
|
expect(wrapper.text()).not.toContain('1000001')
|
|
expect(wrapper.text()).not.toContain('MANUAL:')
|
|
expect(wrapper.text()).not.toContain('已延误')
|
|
expect(wrapper.text()).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 })
|
|
vi.mocked(getImpact).mockResolvedValueOnce({...detail,transport:{trips:[{tripNo:'TRIP-1',status:'PLANNED',plannedDepartureAt:null}],sameTripBookings:[]}})
|
|
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()
|
|
const done=vi.fn(); wrapper.findComponent({name:'AppDialog'}).props('beforeClose')(done); expect(done).not.toHaveBeenCalled()
|
|
expect(guards.register.mock.calls[0]![0]()).toBe(false)
|
|
await wrapper.findAll('button').find(b=>b.text()==='TRIP-1')!.trigger('click');expect(guards.push).not.toHaveBeenCalled()
|
|
for (const hook of [onBeforeRouteLeave,onBeforeRouteUpdate]) expect((vi.mocked(hook).mock.calls[0]![0] as () => boolean)()).toBe(false)
|
|
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)
|
|
expect(guards.register.mock.calls[0]![0]()).toBe(true)
|
|
wrapper.unmount(); expect(guards.unregister).toHaveBeenCalledOnce()
|
|
})
|
|
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()
|
|
})
|
|
})
|