103 lines
10 KiB
TypeScript
103 lines
10 KiB
TypeScript
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<typeof import('@/modules/overseas-warehouse')>(), getDispatch: vi.fn(), getFbaAppointment: vi.fn() }))
|
|
vi.mock('./batchApi', async original => ({ ...await original<typeof import('./batchApi')>(), getAppointmentBatch: vi.fn() }))
|
|
vi.mock('./receivingApi', async original => ({ ...await original<typeof import('./receivingApi')>(), 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<InstanceType<typeof TripAppointmentPanel>['$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: '<button :disabled="disabled"><slot /></button>' },
|
|
AppDialog: { props: ['modelValue'], template: '<div v-if="modelValue"><slot /></div>' },
|
|
WorkReceivingPanel: { name: 'WorkReceivingPanel', props: ['batchKey', 'initialCustomerId'], template: '<div data-receiving />' },
|
|
WorkBatchContinuation: { name: 'WorkBatchContinuation', props: ['batchKey', 'customerId'], template: '<div data-continuation />' },
|
|
AppointmentBatchForm: true, WorkAppointmentForm: true,
|
|
LoadErrorState: { props: ['message'], template: '<p role="alert">{{ message }}</p>' },
|
|
} } }) }
|
|
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()
|
|
})
|