81 lines
6.3 KiB
TypeScript
81 lines
6.3 KiB
TypeScript
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import AppointmentFollowupDialog from './AppointmentFollowupDialog.vue'
|
|
import { getFollowups, saveFollowup, getContainerFollowups, saveContainerFollowup, type PlanningSource, type ContainerFollowupContext } from './planningApi'
|
|
|
|
vi.mock('./planningApi', () => ({ getFollowups: vi.fn(), saveFollowup: vi.fn(), getContainerFollowups: vi.fn(), saveContainerFollowup: vi.fn() }))
|
|
vi.mock('vue-router', () => ({ onBeforeRouteLeave: vi.fn(), onBeforeRouteUpdate: vi.fn() }))
|
|
vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn() } }))
|
|
const history = { history: { records: [], page: 1, size: 10, total: 0, pages: 0 }, versions: { DRAYAGE_CONTACT: 0, TERMINAL_APPOINTMENT: 0, WAREHOUSE_RECEIVING: 0, FBA_RECEIVING: 0 } }
|
|
const source: PlanningSource = { sourceKey: 'ORDER:1', customerId: 'A', canFollowup: true, blockReasons: [], orderId: '1', inboundCode: 'IN-1', bookingNo: null, containerNo: null, originWarehouseId: 'US-1', originWarehouseName: '美国仓', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', packages: null, sourceVersion: null }
|
|
function render(canWrite = true, container?: ContainerFollowupContext) {
|
|
return mount(AppointmentFollowupDialog, { props: container ? { container, canWrite } : { source, coverage: { status: 'UNASSIGNED', appointments: [], missingFields: [], changeReviewRequired: false }, canWrite }, global: { stubs: {
|
|
AppDialog: { name: 'AppDialog', props: ['confirmDisabled', 'beforeClose'], template: '<div><slot/><button data-close @click="$emit(\'update:model-value\',false)">关闭</button><button data-save :disabled="confirmDisabled" @click="$emit(\'confirm\')">保存</button></div>' },
|
|
ElForm: { template: '<form><slot/></form>' }, ElFormItem: { template: '<label><slot/></label>' },
|
|
ElInput: { props: ['modelValue', 'placeholder'], template: '<input :value="modelValue" :placeholder="placeholder" @input="$emit(\'update:modelValue\', $event.target.value)" />' },
|
|
ElSelect: true, ElOption: true, ElDatePicker: true,
|
|
DataTable: true, LoadErrorState: true, ApiErrorMeta: true,
|
|
} } })
|
|
}
|
|
describe('appointment followup recovery', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks(); vi.mocked(getFollowups).mockResolvedValue(history)
|
|
vi.mocked(getContainerFollowups).mockResolvedValue(history)
|
|
vi.mocked(saveFollowup).mockResolvedValue({ stage: 'FBA_RECEIVING', version: 1, contactedParty: null, result: '已联系', nextAction: null, followupDate: null, operatorName: '运营', createdAt: '2026-10-09T12:00:00Z' })
|
|
})
|
|
it.each([null, '100'])('saves the correct container stage for warehouse %s without customer-scoped calls', async warehouseId => {
|
|
const container = { containerBookingId: 'C1', containerNo: 'CONTAINER-A', warehouseId, warehouseName: warehouseId ? '洛杉矶仓' : null, canFollowup: true, blockReasons: [] }
|
|
const wrapper = render(true, container); await flushPromises()
|
|
await wrapper.get('input[placeholder="记录对方反馈,不会自动确认预约"]').setValue('同柜统一联系')
|
|
await wrapper.get('[data-save]').trigger('click'); await flushPromises()
|
|
expect(saveContainerFollowup).toHaveBeenCalledWith(container, expect.objectContaining({ stage: warehouseId ? 'WAREHOUSE_RECEIVING' : 'DRAYAGE_CONTACT', expectedVersion: 0, result: '同柜统一联系' }))
|
|
expect(getContainerFollowups).toHaveBeenCalledTimes(2)
|
|
expect(getFollowups).not.toHaveBeenCalled()
|
|
expect(saveFollowup).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
it('describes FBA followup independently of batch action visibility', async () => {
|
|
const wrapper = render(); await flushPromises()
|
|
expect(wrapper.text()).toContain('FBA收货跟进')
|
|
expect(wrapper.text()).not.toContain('同柜多客户共用提柜')
|
|
wrapper.unmount()
|
|
})
|
|
it('saves contact facts without requiring appointment, inbound plan or driver fields', async () => {
|
|
const wrapper = render(); await flushPromises()
|
|
await wrapper.get('input[placeholder="记录对方反馈,不会自动确认预约"]').setValue('已联系,等待回复')
|
|
await wrapper.get('[data-save]').trigger('click'); await flushPromises()
|
|
expect(saveFollowup).toHaveBeenCalledWith('ORDER:1', 'A', expect.objectContaining({ stage: 'FBA_RECEIVING', expectedVersion: 0, result: '已联系,等待回复', followupDate: null }))
|
|
const body = vi.mocked(saveFollowup).mock.calls[0]![2]
|
|
expect(body).not.toHaveProperty('appointmentNo'); expect(body).not.toHaveProperty('driverId'); expect(body).not.toHaveProperty('operatorId')
|
|
expect(getFollowups).toHaveBeenCalledTimes(2)
|
|
})
|
|
it('retries an unknown outcome with the exact same body and persisted command key', async () => {
|
|
vi.mocked(saveFollowup).mockRejectedValueOnce(new Error('network'))
|
|
const wrapper = render(); await flushPromises()
|
|
await wrapper.get('input[placeholder="记录对方反馈,不会自动确认预约"]').setValue('等待码头开放')
|
|
await wrapper.get('[data-save]').trigger('click'); await flushPromises()
|
|
const first = vi.mocked(saveFollowup).mock.calls[0]![2]
|
|
await wrapper.get('[data-close]').trigger('click')
|
|
expect(wrapper.emitted('close')).toBeUndefined()
|
|
const done = vi.fn()
|
|
wrapper.findAllComponents({ name: 'AppDialog' }).at(-1)!.props('beforeClose')(done)
|
|
expect(done).not.toHaveBeenCalled()
|
|
for (const hook of [onBeforeRouteLeave, onBeforeRouteUpdate]) {
|
|
const guard = vi.mocked(hook).mock.calls.at(-1)![0] as () => boolean
|
|
expect(guard()).toBe(false)
|
|
}
|
|
await wrapper.get('[data-save]').trigger('click'); await flushPromises()
|
|
expect(vi.mocked(saveFollowup).mock.calls[1]![2]).toEqual(first)
|
|
expect(first.idempotencyKey).toBeTruthy()
|
|
})
|
|
it('does not allow a write without write permission or a successfully loaded version', async () => {
|
|
const readonly = render(false); await flushPromises()
|
|
expect(readonly.get('[data-save]').attributes('disabled')).toBeDefined()
|
|
vi.mocked(getFollowups).mockRejectedValueOnce(new Error('unavailable'))
|
|
const failed = render(); await flushPromises()
|
|
expect(failed.get('[data-save]').attributes('disabled')).toBeDefined()
|
|
expect(saveFollowup).not.toHaveBeenCalled()
|
|
})
|
|
})
|