import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, expect, it, vi } from 'vitest' import TripAppointmentPanel from './TripAppointmentPanel.vue' import { getDispatch, getFbaAppointment, type DispatchBatchReference } from '@/modules/overseas-warehouse' import { getAppointmentBatch } from './batchApi' import { getReceivingContext } from './receivingApi' import { batchContext } from './batch.test-support' vi.mock('vue-router', () => ({ onBeforeRouteLeave: vi.fn() })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('@/modules/overseas-warehouse', async original => ({ ...await original(), getDispatch: vi.fn(), getFbaAppointment: vi.fn() })) vi.mock('./batchApi', async original => ({ ...await original(), getAppointmentBatch: vi.fn() })) vi.mock('./receivingApi', async original => ({ ...await original(), getReceivingContext: vi.fn() })) const reference = (batchKey = 'FBA_APPOINTMENT:1', batchNo: string | null = 'AP-001'): DispatchBatchReference => ({ batchKey, batchNo, recordVersion: 0, customerId: 'customer-a', sources: [{ sourceKey: 'ORDER:1', destinationKey: 'FBA:ONT8', packageCount: 70 }] }) function render(overrides: Partial['$props']> = {}) { return mount(TripAppointmentPanel, { props: { dispatches: [{ dispatchNo: 'DS-A', customerId: 'customer-a', packageCount: 70, appointmentBatches: [reference()] }], previousDispatches: [], historyUnavailableReason: null, canWrite: true, ...overrides }, global: { stubs: { ElButton: { props: ['disabled'], template: '' }, AppDialog: { props: ['modelValue'], template: '
' }, WorkReceivingPanel: { name: 'WorkReceivingPanel', props: ['batchKey', 'initialCustomerId'], template: '
' }, WorkBatchContinuation: { name: 'WorkBatchContinuation', props: ['batchKey', 'customerId'], template: '
' }, AppointmentBatchForm: true, WorkAppointmentForm: true, LoadErrorState: { props: ['message'], template: '

{{ message }}

' }, } } }) } beforeEach(() => { vi.clearAllMocks() vi.mocked(getAppointmentBatch).mockResolvedValue(batchContext()) vi.mocked(getReceivingContext).mockResolvedValue({ batch: batchContext().appointment, agreements: [], pendingChanges: [], plans: [], executionScope: { version: 'execution-v1', members: [] }, transport: { version: 'scope-v1', vehicles: [], blockReasons: [] }, allowedActions: [], blockReasons: [] }) }) it('renders all current 40/20 destinations for the bound customer and directly opens the exact customer adjustment', async () => { const context = batchContext(), forty = batchContext('AP-40', 'FBA_APPOINTMENT:40').appointment, twenty = batchContext('AP-20', 'FBA_APPOINTMENT:20').appointment forty.items[0]!.packageCount = 40; forty.items[1]!.packageCount = 999; twenty.items = [{ ...context.appointment.items[0]!, packageCount: 20 }] context.currentBatches = [forty, twenty]; vi.mocked(getAppointmentBatch).mockResolvedValue(context) const wrapper = render(); await flushPromises() expect(wrapper.text()).toContain('40 箱'); expect(wrapper.text()).toContain('20 箱'); expect(wrapper.text()).not.toContain('999') expect(wrapper.text()).not.toContain('internal-dispatch'); expect(wrapper.text()).not.toContain('customer-a') await wrapper.findAll('button').find(button => button.text() === '调整本客户先送 / 后送')!.trigger('click'); await flushPromises() expect(wrapper.getComponent({ name: 'WorkReceivingPanel' }).props()).toMatchObject({ batchKey: 'FBA_APPOINTMENT:1', initialCustomerId: 'customer-a' }) wrapper.unmount() }) it('continues a current destination by its batch and customer rather than guessing a work key', async () => { const wrapper = render(); await flushPromises() await wrapper.findAll('button').find(button => button.text() === '继续本批当前工作')!.trigger('click'); await flushPromises() const continuation = wrapper.getComponent({ name: 'WorkBatchContinuation' }) expect(continuation.props()).toMatchObject({ batchKey: 'FBA_APPOINTMENT:1', customerId: 'customer-a' }) continuation.vm.$emit('open', 'real-work-key'); expect(wrapper.emitted('work')).toEqual([['real-work-key']]); wrapper.unmount() }) it('uses historical source refs when the bound customer has completely left the original batch', async () => { const context = batchContext(), moved = context.appointment.items[0]! context.appointment.items = [context.appointment.items[1]!] const current = batchContext('AP-LATER', 'FBA_APPOINTMENT:2').appointment current.items = [{ ...moved, packageCount: 70 }]; context.currentBatches = [context.appointment, current] vi.mocked(getAppointmentBatch).mockResolvedValue(context) const wrapper = render(); await flushPromises() expect(wrapper.text()).toContain('AP-LATER') expect(wrapper.text()).toContain('IN-A · 70 箱') expect(wrapper.text()).not.toContain('范围当前不可匹配'); wrapper.unmount() }) it('reports missing references and read failures without looking up legacy appointment numbers', async () => { const wrapper = render({ dispatches: [{ dispatchNo: 'DS-A', customerId: 'customer-a', packageCount: 70, appointmentBatches: [] }] }); await flushPromises() expect(wrapper.text()).toContain('没有预约批次引用') expect(getDispatch).not.toHaveBeenCalled(); expect(getFbaAppointment).not.toHaveBeenCalled(); expect(getAppointmentBatch).not.toHaveBeenCalled() vi.mocked(getAppointmentBatch).mockRejectedValueOnce(new Error('read failed')) await wrapper.setProps({ dispatches: [{ dispatchNo: 'DS-A', customerId: 'customer-a', packageCount: 70, appointmentBatches: [reference()] }] }); await flushPromises() expect(wrapper.get('[role="alert"]').text()).toContain('读取失败'); wrapper.unmount() }) it('reads all stable references without a legacy appointment number and deduplicates repeated bindings', async () => { vi.mocked(getAppointmentBatch).mockImplementation(async key => batchContext(key === 'FBA_APPOINTMENT:1' ? 'AP-001' : 'AP-002', key)) vi.mocked(getReceivingContext).mockImplementation(async key => ({ batch: batchContext('AP', key).appointment, agreements: [], pendingChanges: [], plans: [], executionScope: { version: 'v1', members: [] }, transport: { version: 'v1', vehicles: [], blockReasons: [] }, allowedActions: [], blockReasons: [] })) const refs = [reference(), reference('FBA_APPOINTMENT:2', 'AP-002')] const wrapper = render({ dispatches: [{ dispatchNo: 'DS-A', customerId: 'customer-a', packageCount: 100, appointmentBatches: refs }, { dispatchNo: 'DS-B', customerId: 'customer-a', packageCount: 70, appointmentBatches: [reference()] }] }); await flushPromises() expect(wrapper.findAll('article')).toHaveLength(2) expect(wrapper.text()).toContain('DS-A、DS-B'); expect(wrapper.text()).toContain('AP-002') expect(getAppointmentBatch).toHaveBeenCalledTimes(2); expect(getReceivingContext).toHaveBeenCalledTimes(2) expect(getDispatch).not.toHaveBeenCalled(); expect(getFbaAppointment).not.toHaveBeenCalled(); wrapper.unmount() }) it('reenters a removed customer through history and displays only current 40/20 source destinations', async () => { const context = batchContext(), source = context.appointment.items[0]! const forty = batchContext('AP-40', 'FBA_APPOINTMENT:40').appointment, twenty = batchContext('AP-20', 'FBA_APPOINTMENT:20').appointment forty.items = [{ ...source, packageCount: 40 }, { ...source, id: 'other', sourceKey: 'ORDER:OTHER', inboundCode: 'OTHER-SOURCE', packageCount: 888 }] twenty.items = [{ ...source, packageCount: 20 }, { ...context.appointment.items[1]!, packageCount: 777 }] context.originalSourceRefs.push({ customerId: 'customer-a', sourceKey: 'ORDER:OTHER', destinationKey: 'FBA:ONT8' }) context.appointment.items = []; context.currentBatches = [forty, twenty, forty] vi.mocked(getAppointmentBatch).mockResolvedValue(context) const previous = { dispatchNo: 'DS-REMOVED', customerId: 'customer-a', recordVersion: 4, recordedAt: '2026-10-16T01:00:00Z', appointmentBatches: [{ ...reference(), sources: [{ ...reference().sources[0]!, packageCount: 9999 }] }] } const wrapper = render({ dispatches: [], previousDispatches: [previous, { ...previous, recordVersion: 5 }] }); await flushPromises() expect(wrapper.findAll('article')).toHaveLength(1) expect(wrapper.text()).toContain('仅供追溯'); expect(wrapper.text()).toContain('40 箱'); expect(wrapper.text()).toContain('20 箱') for (const unexpected of ['9999', '888', '777', 'OTHER-SOURCE', '调整本客户先送 / 后送']) expect(wrapper.text()).not.toContain(unexpected) const buttons = wrapper.findAll('button').filter(button => button.text() === '继续本批当前工作'); expect(buttons).toHaveLength(2) await buttons[1]!.trigger('click'); await flushPromises() expect(wrapper.getComponent({ name: 'WorkBatchContinuation' }).props()).toMatchObject({ batchKey: 'FBA_APPOINTMENT:20', customerId: 'customer-a' }) expect(getAppointmentBatch).toHaveBeenCalledTimes(1); wrapper.unmount() }) it('keeps current cards readable with incomplete history and never fabricates historical entries', async () => { const wrapper = render({ historyUnavailableReason: '部分原绑定记录缺少来源证据' }); await flushPromises() expect(wrapper.get('[role="alert"]').text()).toContain('部分原绑定记录缺少来源证据') expect(wrapper.findAll('article')).toHaveLength(1); expect(wrapper.text()).toContain('当前关联排车单:DS-A') expect(wrapper.text()).not.toContain('历史关联排车单:'); expect(wrapper.text()).toContain('70 箱'); wrapper.unmount() }) it('blocks invalid reference customer identity before reading or opening current work', async () => { const wrapper = render({ dispatches: [{ dispatchNo: 'DS-A', customerId: 'another-customer', packageCount: 70, appointmentBatches: [reference()] }] }); await flushPromises() expect(wrapper.get('[role="alert"]').text()).toContain('客户或来源范围不完整') expect(getAppointmentBatch).not.toHaveBeenCalled(); expect(wrapper.text()).not.toContain('继续本批当前工作'); wrapper.unmount() })