2026-10-15 17:11:03 +08:00
|
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
|
|
|
import { beforeEach, expect, it, vi } from 'vitest'
|
|
|
|
|
import WorkDispatchForm from './WorkDispatchForm.vue'
|
|
|
|
|
import { getBatchDispatchCandidates, createDispatchDraftContainer, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDraftDispatch, releaseDispatchDraft, type BatchDispatchCandidate } from '@/modules/overseas-warehouse'
|
|
|
|
|
import type { LeaveGuard } from './useWorkDraft'
|
|
|
|
|
const access = vi.hoisted(() => ({ allowed: true }))
|
|
|
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => access.allowed }) }))
|
|
|
|
|
vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn() }, ElMessageBox: { confirm: vi.fn() } }))
|
|
|
|
|
vi.mock('@/modules/overseas-warehouse', async original => ({ ...await original<typeof import('@/modules/overseas-warehouse')>(), getBatchDispatchCandidates: vi.fn(), createDispatchDraftContainer: vi.fn(), listDispatchDrafts: vi.fn(), lockDispatchDraftPallet: vi.fn(), preArrangeDraftDispatch: vi.fn(), releaseDispatchDraft: vi.fn() }))
|
|
|
|
|
const candidate: BatchDispatchCandidate = { planPalletId: '9007199254740993', palletNo: 'PL-TEST', planId: 'plan-1', planNo: 'IP-001', customerId: 'customer-a', sourceStage: 'RECEIVED', packageCount: 12, allocations: [{ planPalletId: '9007199254740993', batchKey: 'FBA_APPOINTMENT:1', customerId: 'customer-a', sourceKey: 'BOOKING:9', destinationKey: 'FBA:ONT8', packageCount: 12 }], blockReasons: [] }
|
|
|
|
|
let guard: LeaveGuard
|
2026-10-16 15:41:56 +08:00
|
|
|
function render() { return mount(WorkDispatchForm, { props: { batchKeys: ['FBA_APPOINTMENT:1'], canWrite: true }, global: { provide: { 'appointment-work-leave-guard': (value: LeaveGuard) => { guard = value; return () => undefined } }, stubs: { WorkSharedTripForm: { name: 'WorkSharedTripForm', props: ['dispatchNo', 'canWrite'], template: '<div />' }, ElButton: { props: ['disabled'], template: '<button :disabled="disabled"><slot /></button>' }, LoadErrorState: { props: ['message'], template: '<p>{{ message }}</p>' }, ApiErrorMeta: true, TripPreparationDialog: { name: 'TripPreparationDialog', props: ['warehouseId', 'dispatchNos'], template: '<div />' } } } }) }
|
2026-10-15 17:11:03 +08:00
|
|
|
function button(wrapper: ReturnType<typeof render>, text: string) { return wrapper.findAll('button').find(button => button.text() === text)! }
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks(); access.allowed = true
|
|
|
|
|
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:1', version: 4, candidates: [structuredClone(candidate)], blockReasons: [] })
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([])
|
|
|
|
|
vi.mocked(createDispatchDraftContainer).mockResolvedValue({ id: 'container-1', capacity: 30, palletIds: [] })
|
|
|
|
|
vi.mocked(lockDispatchDraftPallet).mockResolvedValue(null)
|
2026-10-16 15:41:56 +08:00
|
|
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue([{ draftContainerId: 'container-1', dispatch: { id: 'internal-dispatch', dispatchNo: 'DS-001', appointmentBatches: [], overseasWarehouseId: 'origin-1', overseasWarehouseName: '测试起运仓' } }])
|
2026-10-15 17:11:03 +08:00
|
|
|
})
|
|
|
|
|
it('copies exact server allocations into a real draft group and continues into original trip preparation', async () => {
|
|
|
|
|
const wrapper = render(); await flushPromises(); await wrapper.get('input[type=checkbox]').setValue(true)
|
|
|
|
|
await button(wrapper, '创建本客户排车单').trigger('click'); await flushPromises()
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenCalledWith({ draftToken: expect.any(String), groups: [{ draftContainerId: 'container-1', planPalletIds: [candidate.planPalletId], returnReceiptIds: [], batchAllocations: candidate.allocations }] })
|
|
|
|
|
expect(wrapper.text()).toContain('DS-001'); expect(wrapper.text()).not.toContain('internal-dispatch')
|
|
|
|
|
const prepare = wrapper.getComponent({ name: 'TripPreparationDialog' })
|
|
|
|
|
expect(prepare.props()).toMatchObject({ warehouseId: 'origin-1', dispatchNos: ['DS-001'] })
|
|
|
|
|
prepare.vm.$emit('saved', { trip: { tripNo: 'TR-001' } }); expect(wrapper.emitted('external')).toEqual([['/overseas-master-data/trips/TR-001']])
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
2026-10-16 15:41:56 +08:00
|
|
|
it('opens quantity splitting for only the current batch without locking physical cargo or creating a new full-source appointment', async () => {
|
|
|
|
|
const wrapper = render(); await flushPromises()
|
|
|
|
|
await button(wrapper, '按当前批次箱数分批').trigger('click')
|
|
|
|
|
expect(wrapper.emitted('batch')).toEqual([['FBA_APPOINTMENT:1']])
|
|
|
|
|
expect(lockDispatchDraftPallet).not.toHaveBeenCalled(); expect(preArrangeDraftDispatch).not.toHaveBeenCalled(); wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
it('preselects only eligible physical cargo of the entered batch, never unrelated allocations or unknown quantity', async () => {
|
|
|
|
|
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:1', version: 4, blockReasons: [], candidates: [candidate, { ...candidate, planPalletId: 'other-batch', allocations: [{ ...candidate.allocations[0]!, batchKey: 'FBA_APPOINTMENT:999' }] }, { ...candidate, planPalletId: 'unknown', packageCount: null }] })
|
|
|
|
|
const wrapper = render(); await flushPromises()
|
|
|
|
|
const inputs = wrapper.findAll('input[type=checkbox]')
|
|
|
|
|
expect(inputs.map(input => (input.element as HTMLInputElement).checked)).toEqual([true, false, false])
|
|
|
|
|
expect(inputs[1]!.attributes('disabled')).toBeDefined()
|
|
|
|
|
await button(wrapper, '创建本客户排车单').trigger('click'); await flushPromises()
|
|
|
|
|
expect(vi.mocked(preArrangeDraftDispatch).mock.calls[0]![0].groups[0]!.planPalletIds).toEqual([candidate.planPalletId])
|
|
|
|
|
expect(vi.mocked(preArrangeDraftDispatch).mock.calls[0]![0].groups[0]!.batchAllocations).toEqual(candidate.allocations)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
2026-10-15 17:11:03 +08:00
|
|
|
it('replays a submitted draft without re-locking already dispatched pallets after an unknown response', async () => {
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockRejectedValueOnce(new Error('lost response'))
|
|
|
|
|
const wrapper = render(); await flushPromises(); await wrapper.get('input[type=checkbox]').setValue(true)
|
|
|
|
|
await button(wrapper, '创建本客户排车单').trigger('click'); await flushPromises()
|
|
|
|
|
const first = vi.mocked(preArrangeDraftDispatch).mock.calls[0]![0]
|
|
|
|
|
expect(await guard()).toBe(false)
|
|
|
|
|
await button(wrapper, '重试本次预排').trigger('click'); await flushPromises()
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenNthCalledWith(2, first); expect(lockDispatchDraftPallet).toHaveBeenCalledTimes(1); expect(createDispatchDraftContainer).toHaveBeenCalledTimes(1)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
it('recovers a draft container from the same token when its creation response was lost', async () => {
|
|
|
|
|
vi.mocked(createDispatchDraftContainer).mockRejectedValueOnce(new Error('lost create response'))
|
|
|
|
|
const wrapper = render(); await flushPromises(); await wrapper.get('input[type=checkbox]').setValue(true)
|
|
|
|
|
await button(wrapper, '创建本客户排车单').trigger('click'); await flushPromises()
|
|
|
|
|
const token = vi.mocked(createDispatchDraftContainer).mock.calls[0]![0]
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: token, containers: [{ id: 'container-1', capacity: 30, palletIds: [] }] }])
|
|
|
|
|
await button(wrapper, '重试本次预排').trigger('click'); await flushPromises()
|
|
|
|
|
expect(createDispatchDraftContainer).toHaveBeenCalledTimes(1); expect(preArrangeDraftDispatch).toHaveBeenCalledTimes(1); wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
it('shows server blockers and prevents mixing customers or selecting blocked physical cargo', async () => {
|
2026-10-16 15:41:56 +08:00
|
|
|
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:1', version: 4, blockReasons: [], candidates: [candidate, { ...candidate, planPalletId: '2', customerId: 'customer-b', allocations: [{ ...candidate.allocations[0]!, customerId: 'customer-b', planPalletId: '2' }] }, { ...candidate, planPalletId: '3', blockReasons: ['本托货量超过本批剩余可排量'] }] })
|
2026-10-15 17:11:03 +08:00
|
|
|
const wrapper = render(); await flushPromises(); const inputs = wrapper.findAll('input[type=checkbox]')
|
|
|
|
|
expect(inputs[2]!.attributes('disabled')).toBeDefined(); expect(wrapper.text()).toContain('本托货量超过本批剩余可排量')
|
|
|
|
|
await inputs[0]!.setValue(true); await inputs[1]!.setValue(true)
|
|
|
|
|
expect((inputs[1]!.element as HTMLInputElement).checked).toBe(false)
|
|
|
|
|
await button(wrapper, '创建本客户排车单').trigger('click'); await flushPromises()
|
|
|
|
|
expect(vi.mocked(preArrangeDraftDispatch).mock.calls[0]![0].groups[0]!.planPalletIds).toEqual([candidate.planPalletId]); wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
it('does not read or write scoped cargo without all original permissions', async () => {
|
|
|
|
|
access.allowed = false; const wrapper = render(); await flushPromises()
|
|
|
|
|
expect(getBatchDispatchCandidates).not.toHaveBeenCalled(); expect(preArrangeDraftDispatch).not.toHaveBeenCalled(); wrapper.unmount()
|
|
|
|
|
})
|
2026-10-16 15:41:56 +08:00
|
|
|
|
|
|
|
|
it('continues the actual later 60-box dispatch through the shared-trip entry without re-creating or reallocating cargo', async () => {
|
|
|
|
|
const sixty = { ...candidate, packageCount: 60, allocations: [{ ...candidate.allocations[0]!, packageCount: 60 }] }
|
|
|
|
|
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:1', version: 4, candidates: [sixty], blockReasons: [] })
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue([{ draftContainerId: 'container-1', dispatch: { id: 'internal-later', dispatchNo: 'DS-60', appointmentBatches: [], overseasWarehouseId: 'origin-1', packageCount: 60 } }])
|
|
|
|
|
const wrapper = render(); await flushPromises(); await button(wrapper, '创建本客户排车单').trigger('click'); await flushPromises()
|
|
|
|
|
await button(wrapper, '与同仓其他排车单共车').trigger('click'); await flushPromises()
|
|
|
|
|
const shared = wrapper.getComponent({ name: 'WorkSharedTripForm' })
|
|
|
|
|
expect(shared.props()).toMatchObject({ dispatchNo: 'DS-60', canWrite: true })
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenCalledTimes(1); expect(vi.mocked(preArrangeDraftDispatch).mock.calls[0]![0].groups[0]!.batchAllocations).toEqual(sixty.allocations)
|
|
|
|
|
shared.vm.$emit('busy', true); await flushPromises(); expect(button(wrapper, '继续安排其他实物').attributes('disabled')).toBeDefined()
|
|
|
|
|
shared.vm.$emit('external', '/overseas-master-data/trips/TR-100'); expect(wrapper.emitted('external')).toEqual([['/overseas-master-data/trips/TR-100']])
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|