import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, expect, it, vi } from 'vitest' import { ElMessageBox } from 'element-plus' import AppointmentConfirmationCargo from './AppointmentConfirmationCargo.vue' import { resolvePlanningOrder, resolvePlanningSource, type ResolvedPlanningSource } from './planningApi' import { getCargoVerification, saveCargoVerification, type CargoVerificationContext } from './cargoVerificationApi' const access = vi.hoisted(() => ({ read: true })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => access.read }) })) vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn() }, ElMessageBox: { confirm: vi.fn() } })) vi.mock('./planningApi', () => ({ resolvePlanningOrder: vi.fn(), resolvePlanningSource: vi.fn() })) vi.mock('./cargoVerificationApi', () => ({ getCargoVerification: vi.fn(), saveCargoVerification: vi.fn() })) const source: ResolvedPlanningSource['source'] = { sourceKey: 'BOOKING:BK-TEST', customerId: 'customer-1', orderId: '9007199254740993', inboundCode: 'IN-TEST', bookingNo: 'BK-TEST', containerNo: null, originWarehouseId: 'warehouse-1', originWarehouseName: '测试仓', destinationKey: 'FBA:ONT8', destinationWarehouseCode: 'ONT8', packages: 20, sourceVersion: 'BOOKING:1/ORDER:1', canFollowup: true, blockReasons: [] } const resolved: ResolvedPlanningSource = { source, coverage: { status: 'PENDING', appointments: [], missingFields: ['FBA_SHIPMENT_REFERENCES'], changeReviewRequired: false } } const context: CargoVerificationContext = { source, verification: null, allocatedPackages: 20, unknownAllocationCount: 0, effectivePackages: 20, availablePackages: 0, sourceChanged: false, allowedActions: ['VERIFY_CARGO'], blockReasons: [] } const lookup = { orderId: source.orderId!, customerId: source.customerId, destinationKey: source.destinationKey } function render(active = true, disabled = false) { return mount(AppointmentConfirmationCargo, { props: { sources: [lookup], active, disabled }, global: { stubs: { ElButton: { props: ['disabled', 'nativeType'], template: '' }, InlineHelp: { template: '' }, ApiErrorMeta: true, LoadErrorState: { props: ['message'], template: '
{{ message }}
' }, } } }) } beforeEach(() => { vi.clearAllMocks(); access.read = true vi.mocked(resolvePlanningOrder).mockResolvedValue(resolved) vi.mocked(resolvePlanningSource).mockResolvedValue(resolved) vi.mocked(getCargoVerification).mockResolvedValue(context) vi.mocked(saveCargoVerification).mockResolvedValue({ version: 1, sourceVersion: source.sourceVersion, destinationKey: source.destinationKey, sourcePackages: 20, packageCount: 20, shipmentReferences: ['FBA-TEST'], remark: null, operatorName: '测试员', verifiedAt: '2026-10-21T03:00:00Z' }) }) it('names the missing material and supplements the canonical booking inside the appointment', async () => { const wrapper = render(); await flushPromises() expect(resolvePlanningOrder).toHaveBeenCalledWith(lookup) expect(wrapper.text()).toContain('IN-TEST'); expect(wrapper.text()).toContain('缺少本票 FBA 货件号或追踪号') expect(wrapper.text()).toContain('不能代替货件号'); expect(wrapper.text()).not.toContain(source.orderId) await wrapper.findAll('button').find(button => button.text() === '在此补齐资料')!.trigger('click'); await flushPromises() expect((wrapper.get('input[type=number]').element as HTMLInputElement).value).toBe('20') await wrapper.get('form').trigger('submit'); await flushPromises() expect(saveCargoVerification).not.toHaveBeenCalled() await wrapper.get('textarea[aria-label="本票货件号"]').setValue('FBA-TEST') vi.mocked(resolvePlanningOrder).mockResolvedValue({ ...resolved, coverage: { ...resolved.coverage, missingFields: [] } }) await wrapper.get('form').trigger('submit'); await flushPromises() expect(saveCargoVerification).toHaveBeenCalledWith(source, expect.objectContaining({ packageCount: 20, shipmentReferences: ['FBA-TEST'], expectedSourceVersion: source.sourceVersion })) expect(wrapper.text()).toContain('资料齐全,无需重复填写') expect(wrapper.emitted('ready')?.at(-1)).toEqual([true]); wrapper.unmount() }) it('leaves pending arrangements lazy and reads only when formal confirmation is selected', async () => { const wrapper = render(false); await flushPromises() expect(resolvePlanningOrder).not.toHaveBeenCalled() await wrapper.setProps({ active: true }); await flushPromises() expect(resolvePlanningOrder).toHaveBeenCalledTimes(1); wrapper.unmount() }) it('does not ask for known materials to be entered again', async () => { vi.mocked(resolvePlanningOrder).mockResolvedValue({ ...resolved, coverage: { ...resolved.coverage, missingFields: [] } }) const wrapper = render(); await flushPromises() expect(wrapper.text()).toContain('资料齐全,无需重复填写') expect(wrapper.findAll('button').some(button => button.text() === '在此补齐资料')).toBe(false) expect(wrapper.emitted('ready')?.at(-1)).toEqual([true]); wrapper.unmount() }) it('retains unsaved inputs when the appointment mode changes and blocks leaving an unknown save outcome', async () => { const wrapper = render(); await flushPromises() await wrapper.findAll('button').find(button => button.text() === '在此补齐资料')!.trigger('click'); await flushPromises() await wrapper.get('textarea[aria-label="本票货件号"]').setValue('FBA-KEEP') await wrapper.setProps({ active: false }) expect(wrapper.get('textarea[aria-label="本票货件号"]').isVisible()).toBe(true) vi.mocked(ElMessageBox.confirm).mockRejectedValueOnce(new Error('keep editing')) expect(await wrapper.vm.canLeave()).toBe(false) vi.mocked(saveCargoVerification).mockRejectedValueOnce(new Error('connection lost')) await wrapper.get('form').trigger('submit'); await flushPromises() expect(await wrapper.vm.canLeave()).toBe(false) const payload = vi.mocked(saveCargoVerification).mock.calls[0]![1] await wrapper.get('form').trigger('submit'); await flushPromises() expect(saveCargoVerification).toHaveBeenNthCalledWith(2, source, payload); wrapper.unmount() }) it('reports unknown quantity and unmaterialized cargo without inventing values', async () => { vi.mocked(resolvePlanningOrder).mockResolvedValue({ ...resolved, source: { ...source, orderId: null } }) vi.mocked(getCargoVerification).mockResolvedValue({ ...context, effectivePackages: null }) const wrapper = render(); await flushPromises() expect(wrapper.text()).toContain('缺少本票箱数'); expect(wrapper.text()).toContain('原来货登记完成实收关联') expect(wrapper.findAll('button').some(button => button.text() === '在此补齐资料')).toBe(false) expect(wrapper.emitted('ready')?.at(-1)).toEqual([false]); wrapper.unmount() }) it('keeps failed reads blocked, supports retry, and denies reads without permission', async () => { vi.mocked(resolvePlanningOrder).mockRejectedValueOnce(new Error('network lost')) const wrapper = render(); await flushPromises() expect(wrapper.find('[role=alert]').exists()).toBe(true) expect(wrapper.emitted('ready')?.at(-1)).toEqual([false]) await wrapper.get('[role=alert] button').trigger('click'); await flushPromises() expect(resolvePlanningOrder).toHaveBeenCalledTimes(2); wrapper.unmount() vi.clearAllMocks(); access.read = false const denied = render(); await flushPromises() expect(resolvePlanningOrder).not.toHaveBeenCalled(); expect(denied.text()).toContain('查看权限'); denied.unmount() }) it('keeps the supplement action disabled for read-only appointment views', async () => { const wrapper = render(true, true); await flushPromises() expect(wrapper.findAll('button').find(button => button.text() === '在此补齐资料')!.attributes('disabled')).toBeDefined() expect(saveCargoVerification).not.toHaveBeenCalled(); wrapper.unmount() })