56 lines
5.5 KiB
TypeScript
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()
|
|
})
|