Files
admin-web/src/modules/appointment-calendar/WorkCreateAppointmentForm.test.ts
T

56 lines
5.5 KiB
TypeScript

import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, expect, it, vi } from 'vitest'
import WorkCreateAppointmentForm from './WorkCreateAppointmentForm.vue'
import { previewPlanningBatch, createSourceAppointment, type PlanningBatchPreview } from './planningApi'
import type { SourceReference } from './workApi'
import type { LeaveGuard } from './useWorkDraft'
import { batchContext } from './batch.test-support'
import { ApiResponseError } from '@/shared/api/http'
const access = vi.hoisted(() => ({ write: true }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => access.write }) }))
vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn() }, ElMessageBox: { confirm: vi.fn() } }))
vi.mock('./planningApi', async original => ({ ...await original<typeof import('./planningApi')>(), previewPlanningBatch: vi.fn(), createSourceAppointment: vi.fn() }))
const sources: SourceReference[] = [{ sourceKey: 'BOOKING:external-1', customerId: '9007199254740999', destinationKey: 'FBA:ONT8' }]
const preview: PlanningBatchPreview = { group: { originWarehouseId: 'origin-1', destinationKey: 'FBA:ONT8', packages: 30, sourceCount: 1, customerCount: 1, readinessIncomplete: true, readyFrom: null, readyTo: null, unknownQuantitySources: 0 }, originWarehouseName: '测试起运仓', destinationWarehouseCode: 'ONT8', destinationTimezone: 'America/Los_Angeles', missingFields: [], sources: [{ ...sources[0]!, destinationKey: 'FBA:ONT8', orderId: null, inboundCode: null, bookingNo: 'BK-TEST', containerNo: null, originWarehouseId: 'origin-1', originWarehouseName: '测试起运仓', destinationWarehouseCode: 'ONT8', packages: 30, sourceVersion: null, canFollowup: true, blockReasons: [] }] }
let guard: LeaveGuard
function render(input = sources) { return mount(WorkCreateAppointmentForm, { props: { sources: input, canWrite: true }, global: { provide: { 'appointment-work-leave-guard': (value: LeaveGuard) => { guard = value; return () => undefined } }, stubs: { ElButton: { props: ['disabled'], template: '<button :disabled="disabled"><slot /></button>' }, FbaAppointmentFields: { name: 'FbaAppointmentFields', props: ['model', 'disabled'], template: '<input v-model="model.plannedDate" type="date">' }, LoadErrorState: { props: ['message'], template: '<p>{{ message }}</p>' }, ApiErrorMeta: true } } }) }
beforeEach(() => { vi.clearAllMocks(); access.write = true; vi.mocked(previewPlanningBatch).mockResolvedValue(preview); vi.mocked(createSourceAppointment).mockResolvedValue(batchContext().appointment) })
it('shows readable preview requirements without exposing missing-field machine codes', async () => {
vi.mocked(previewPlanningBatch).mockResolvedValue({ ...preview, missingFields: ['READY_WINDOW_UNVERIFIED', 'CARGO_QUANTITY_REVIEW'] })
const wrapper = render(); await flushPromises()
expect(wrapper.text()).toContain('货物备妥窗口待核实')
expect(wrapper.text()).toContain('货量或重复关联需要核对。')
expect(wrapper.text()).not.toContain('READY_WINDOW_UNVERIFIED')
expect(wrapper.text()).not.toContain('CARGO_QUANTITY_REVIEW')
wrapper.unmount()
})
it('previews and creates the exact canonical external source and customer without inventing times', async () => {
const wrapper = render(); await flushPromises()
expect(previewPlanningBatch).toHaveBeenCalledWith(sources)
await wrapper.get('input').setValue('2026-10-16'); await wrapper.get('button').trigger('click'); await flushPromises()
expect(createSourceAppointment).toHaveBeenCalledWith(expect.objectContaining({ sources, originWarehouseId: 'origin-1', fbaWarehouseCode: 'ONT8', details: expect.objectContaining({ plannedDate: '2026-10-16', plannedAt: null, plannedEndAt: null, appointmentStatus: 'PENDING' }) }))
expect(wrapper.emitted('saved')).toHaveLength(1); expect(wrapper.text()).not.toContain('external-1'); wrapper.unmount()
})
it('retains the same immutable request after an unknown outcome and blocks leaving', async () => {
vi.mocked(createSourceAppointment).mockRejectedValueOnce(new Error('connection lost'))
const wrapper = render(); await flushPromises(); await wrapper.get('button').trigger('click'); await flushPromises()
const request = vi.mocked(createSourceAppointment).mock.calls[0]![0]
expect(await guard()).toBe(false)
await wrapper.get('button').trigger('click'); await flushPromises()
expect(createSourceAppointment).toHaveBeenNthCalledWith(2, request); wrapper.unmount()
})
it('refreshes qualification after conflict while keeping user-entered dates', async () => {
vi.mocked(createSourceAppointment).mockRejectedValueOnce(new ApiResponseError('来源变化', 'A0409', 409))
const wrapper = render(); await flushPromises(); await wrapper.get('input').setValue('2026-10-16')
await wrapper.get('button').trigger('click'); await flushPromises()
expect(previewPlanningBatch).toHaveBeenCalledTimes(2)
expect((wrapper.get('input').element as HTMLInputElement).value).toBe('2026-10-16')
expect(wrapper.emitted('saved')).toBeUndefined(); wrapper.unmount()
})
it('rejects missing destination scope and lacks a write path without permission', async () => {
const missing = render([{ ...sources[0]!, destinationKey: null }]); await flushPromises()
expect(previewPlanningBatch).not.toHaveBeenCalled(); expect(missing.text()).toContain('缺少完整货源'); missing.unmount()
access.write = false; const readonly = render(); await flushPromises(); await readonly.get('button').trigger('click')
expect(createSourceAppointment).not.toHaveBeenCalled(); readonly.unmount()
})