2026-10-03 15:57:53 +08:00
|
|
|
import { flushPromises, shallowMount } from '@vue/test-utils'
|
|
|
|
|
import { defineComponent, h } from 'vue'
|
|
|
|
|
import { beforeEach, expect, it, vi } from 'vitest'
|
2026-10-18 16:37:37 +08:00
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
2026-10-03 15:57:53 +08:00
|
|
|
import SmartDispatchDialog from './SmartDispatchDialog.vue'
|
2026-10-18 16:37:37 +08:00
|
|
|
import { listDispatchCandidates, listDispatchDrafts, preArrangeDispatch, preArrangeDraftDispatch, lockDispatchDraftPallet, unlockDispatchDraftPallet, releaseDispatchDraft, deleteDispatchDraftContainer, type DispatchPalletCandidateDto, type DispatchDraftDto } from '../api/dispatches'
|
2026-10-03 15:57:53 +08:00
|
|
|
|
|
|
|
|
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
|
2026-10-18 16:37:37 +08:00
|
|
|
vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn(), error: vi.fn() }, ElMessageBox: { confirm: vi.fn() } }))
|
2026-10-03 15:57:53 +08:00
|
|
|
vi.mock('../api/dispatches', () => ({
|
2026-10-05 03:30:06 +08:00
|
|
|
listDispatchCandidates: vi.fn(), listDispatchDrafts: vi.fn(), preArrangeDispatch: vi.fn(), preArrangeDraftDispatch: vi.fn(),
|
2026-10-03 15:57:53 +08:00
|
|
|
createDispatchDraftContainer: vi.fn(), deleteDispatchDraftContainer: vi.fn(),
|
|
|
|
|
lockDispatchDraftPallet: vi.fn(), unlockDispatchDraftPallet: vi.fn(),
|
2026-10-18 16:37:37 +08:00
|
|
|
releaseDispatchDraft: vi.fn(),
|
2026-10-03 15:57:53 +08:00
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const shell = defineComponent({ setup(_, { slots }) { return () => h('section', [slots.default?.(), slots.footer?.()]) } })
|
|
|
|
|
const button = defineComponent({ props: { disabled: Boolean }, emits: ['click'], setup(props, { slots, emit }) {
|
|
|
|
|
return () => h('button', { disabled: props.disabled, onClick: () => emit('click') }, slots.default?.())
|
|
|
|
|
} })
|
|
|
|
|
const retry = defineComponent({ emits: ['retry'], setup(_, { emit }) {
|
|
|
|
|
return () => h('button', { 'data-test': 'retry', onClick: () => emit('retry') }, '重试加载')
|
|
|
|
|
} })
|
|
|
|
|
const plan: DispatchPalletCandidateDto = { orderId: '1', orderNo: 'LM-A', planPalletId: '11', sourceType: 'INBOUND_PLAN',
|
|
|
|
|
warehousePalletId: '101', overseasWarehouseId: '92', customerId: 'A', palletNo: 'PAL-A', packageCount: 2, sourceStage: 'RECEIVED' }
|
|
|
|
|
const returned: DispatchPalletCandidateDto = { orderId: '2', orderNo: 'LM-B', returnReceiptId: '22', returnReceiptNo: 'RSR-B', sourceType: 'RETURN_RECEIPT',
|
|
|
|
|
warehousePalletId: '102', overseasWarehouseId: '92', customerId: 'A', palletNo: 'PAL-B', packageCount: 2, sourceStage: 'RECEIVED' }
|
|
|
|
|
|
|
|
|
|
beforeEach(() => vi.resetAllMocks())
|
|
|
|
|
|
2026-10-05 03:30:06 +08:00
|
|
|
function mountDraft() {
|
|
|
|
|
return shallowMount(SmartDispatchDialog, { props: { modelValue: true, orders: [] }, global: {
|
|
|
|
|
stubs: { AppDialog: shell, ElButton: button, LoadErrorState: retry, ElDialog: true,
|
|
|
|
|
ElInput: true, ElOption: true, ElSelect: true, ElTable: true, ElTableColumn: true,
|
2026-10-18 16:37:37 +08:00
|
|
|
ElTag: shell, ElDropdown: true, ElDropdownMenu: true, ElDropdownItem: true,
|
2026-10-05 03:30:06 +08:00
|
|
|
ElEmpty: true, ElRadio: true, ElRadioGroup: true },
|
|
|
|
|
} })
|
|
|
|
|
}
|
2026-10-18 16:37:37 +08:00
|
|
|
const secondPlan = { ...plan, orderId: '3', orderNo: 'LM-C', planPalletId: '13', warehousePalletId: '103', palletNo: 'PAL-C' }
|
|
|
|
|
const thirdPlan = { ...plan, orderId: '4', orderNo: 'LM-D', planPalletId: '14', warehousePalletId: '104', palletNo: 'PAL-D' }
|
2026-10-05 03:30:06 +08:00
|
|
|
const twoGroups: DispatchDraftDto = { draftToken: 'original-draft', containers: [
|
|
|
|
|
{ id: 'container-1', capacity: 30, palletIds: ['11', '13'] },
|
|
|
|
|
{ id: 'container-2', capacity: 30, palletIds: ['14', 'RETURN:22'] },
|
|
|
|
|
{ id: 'empty-container', capacity: 15, palletIds: [] },
|
|
|
|
|
] }
|
|
|
|
|
const expectedBatch = { draftToken: 'original-draft', groups: [
|
|
|
|
|
{ draftContainerId: 'container-1', planPalletIds: ['11', '13'], returnReceiptIds: [] },
|
|
|
|
|
{ draftContainerId: 'container-2', planPalletIds: ['14'], returnReceiptIds: ['22'] },
|
|
|
|
|
] }
|
2026-10-16 15:41:56 +08:00
|
|
|
const goodResult = [{ draftContainerId: 'container-2', dispatch: { id: '102', dispatchNo: 'DS-2', appointmentBatches: [] } },
|
|
|
|
|
{ draftContainerId: 'container-1', dispatch: { id: '101', dispatchNo: 'DS-1', appointmentBatches: [] } }]
|
2026-10-05 03:30:06 +08:00
|
|
|
async function submitDraft(wrapper: ReturnType<typeof mountDraft>) {
|
|
|
|
|
await wrapper.findAll('button').find(node => node.text() === '预排车')!.trigger('click')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
it('keeps two explicit groups in one atomic request, omits empty containers and accepts reordered matching results', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue(goodResult)
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith(expectedBatch)
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
|
|
|
|
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
['one collapsed result', goodResult.slice(0, 1)],
|
|
|
|
|
['foreign container', [{ ...goodResult[0], draftContainerId: 'foreign' }, goodResult[1]]],
|
|
|
|
|
['duplicate container', [goodResult[0], goodResult[0]]],
|
|
|
|
|
['duplicate dispatch', [goodResult[0], { ...goodResult[1], dispatch: goodResult[0].dispatch }]],
|
2026-10-16 15:41:56 +08:00
|
|
|
['missing business number', [goodResult[0], { ...goodResult[1], dispatch: { id: '101', dispatchNo: '', appointmentBatches: [] } }]],
|
2026-10-05 03:30:06 +08:00
|
|
|
])('retains the original groups and token after %s', async (_label, result) => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue(result)
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(wrapper.emitted('created')).toBeUndefined()
|
|
|
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 4 · 箱数 8')
|
|
|
|
|
await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenNthCalledWith(2, expectedBatch)
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('retains all groups on batch rejection without issuing a flat request', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockRejectedValue(new Error('草稿来源已变化'))
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenCalledTimes(1)
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.emitted('created')).toBeUndefined()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 4 · 箱数 8')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('does not submit a physical pallet fallback when its actual plan source identity is missing', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [
|
|
|
|
|
{ id: 'container', capacity: 30, palletIds: ['101'] },
|
|
|
|
|
] }])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([{ ...plan, planPalletId: undefined }])
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('来源标识缺失'))
|
|
|
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
2026-10-14 13:53:15 +08:00
|
|
|
it('submits relabel physical pallet identity without requiring an inbound plan pallet', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'relabel-draft', containers: [
|
|
|
|
|
{ id: 'container', capacity: 30, palletIds: ['101'] },
|
|
|
|
|
] }])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([{ ...plan, planPalletId: null, sourceStage: 'RELABEL', orderRefs: 'RL-1' }])
|
2026-10-16 15:41:56 +08:00
|
|
|
vi.mocked(preArrangeDispatch).mockResolvedValue({ id: '1', dispatchNo: 'DS-1', appointmentBatches: [] })
|
2026-10-14 13:53:15 +08:00
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDispatch).toHaveBeenCalledExactlyOnceWith({ planPalletIds: ['101'], returnReceiptIds: [], relabelNo: 'RL-1' })
|
|
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
2026-10-05 03:30:06 +08:00
|
|
|
it('does not send a second batch while the original command is pending', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
|
|
|
|
let resolveCommand!: (result: typeof goodResult) => void
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockReturnValue(new Promise(resolve => { resolveCommand = resolve }))
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper); await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith(expectedBatch)
|
|
|
|
|
expect(wrapper.emitted('created')).toBeUndefined()
|
|
|
|
|
resolveCommand(goodResult); await flushPromises()
|
|
|
|
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps group membership frozen while the submitted batch is awaiting its response', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
|
|
|
|
let resolveCommand!: (result: typeof goodResult) => void
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockReturnValue(new Promise(resolve => { resolveCommand = resolve }))
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
await wrapper.findAll('button').find(node => node.text() === '移除')!.trigger('click')
|
|
|
|
|
await wrapper.findAll('button').find(node => node.attributes('aria-label') === '删除装车车型1')!.trigger('click')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(unlockDispatchDraftPallet).not.toHaveBeenCalled()
|
|
|
|
|
expect(deleteDispatchDraftContainer).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 4 · 箱数 8')
|
|
|
|
|
resolveCommand(goodResult); await flushPromises()
|
|
|
|
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('retains unresolved sources when retry hydration still cannot recover the complete original draft', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, returned])
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('原选择已保留'))
|
|
|
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
{ ...twoGroups, containers: [{ id: 'c1', capacity: 30, palletIds: ['11'] }, { id: 'c2', capacity: 30, palletIds: ['11'] }] },
|
|
|
|
|
{ ...twoGroups, containers: [{ id: 'c1', capacity: 30, palletIds: ['11'] }, { id: 'c1', capacity: 30, palletIds: ['13'] }] },
|
|
|
|
|
{ ...twoGroups, containers: [{ id: '', capacity: 30, palletIds: ['11'] }] },
|
|
|
|
|
])('rejects duplicate sources or invalid container identities before writing', async draft => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([draft])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan])
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('rejects a relabel order split across multiple containers before either API command', async () => {
|
|
|
|
|
const relabel: DispatchPalletCandidateDto = { ...plan, sourceStage: 'RELABEL', orderRefs: 'RL-1' }
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [
|
2026-10-18 16:37:37 +08:00
|
|
|
{ id: 'c1', capacity: 30, palletIds: ['101'] }, { id: 'c2', capacity: 30, palletIds: ['103'] },
|
2026-10-05 03:30:06 +08:00
|
|
|
] }])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([relabel, { ...secondPlan, sourceStage: 'RELABEL', orderRefs: 'RL-1' }])
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('一组排车'))
|
|
|
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps one complete relabel order on the single-dispatch endpoint', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [
|
2026-10-18 16:37:37 +08:00
|
|
|
{ id: 'c1', capacity: 30, palletIds: ['101', '103'] },
|
2026-10-05 03:30:06 +08:00
|
|
|
] }])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan].map(p => ({ ...p, sourceStage: 'RELABEL', orderRefs: 'RL-1' })))
|
2026-10-16 15:41:56 +08:00
|
|
|
vi.mocked(preArrangeDispatch).mockResolvedValue({ id: '1', dispatchNo: 'DS-RL', appointmentBatches: [] })
|
2026-10-05 03:30:06 +08:00
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
2026-10-18 16:37:37 +08:00
|
|
|
expect(preArrangeDispatch).toHaveBeenCalledExactlyOnceWith({ planPalletIds: ['101', '103'], returnReceiptIds: [], relabelNo: 'RL-1' })
|
2026-10-05 03:30:06 +08:00
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
2026-10-03 15:57:53 +08:00
|
|
|
it('retains every restored selection on candidate failure and submits both sources only after retry hydration', async () => {
|
2026-10-16 15:41:56 +08:00
|
|
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue([{ draftContainerId: 'container', dispatch: { id: '1', dispatchNo: 'DS-1', appointmentBatches: [] } }])
|
2026-10-03 15:57:53 +08:00
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [{ id: 'container', capacity: 30, palletIds: ['11', 'RETURN:22'] }] }])
|
|
|
|
|
vi.mocked(listDispatchCandidates)
|
|
|
|
|
.mockResolvedValueOnce([plan])
|
|
|
|
|
.mockRejectedValueOnce(new Error('恢复来源失败'))
|
|
|
|
|
.mockResolvedValueOnce([plan])
|
|
|
|
|
.mockResolvedValueOnce([plan, returned])
|
|
|
|
|
const wrapper = shallowMount(SmartDispatchDialog, { props: { modelValue: true, orders: [] }, global: {
|
|
|
|
|
stubs: { AppDialog: shell, ElButton: button, LoadErrorState: retry, ElDialog: true,
|
|
|
|
|
ElInput: true, ElOption: true, ElSelect: true, ElTable: true, ElTableColumn: true,
|
|
|
|
|
ElTag: true, ElDropdown: true, ElDropdownMenu: true, ElDropdownItem: true,
|
|
|
|
|
ElEmpty: true, ElRadio: true, ElRadioGroup: true },
|
|
|
|
|
} })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const submit = () => wrapper.findAll('button').find(node => node.text() === '预排车')!
|
|
|
|
|
await submit().trigger('click')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
2026-10-05 03:30:06 +08:00
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
2026-10-03 15:57:53 +08:00
|
|
|
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('原选择已保留'))
|
|
|
|
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
|
|
|
await wrapper.get('[data-test="retry"]').trigger('click')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
await submit().trigger('click')
|
|
|
|
|
await flushPromises()
|
2026-10-05 03:30:06 +08:00
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith({ draftToken: 'original-draft', groups: [{ draftContainerId: 'container', planPalletIds: ['11'], returnReceiptIds: ['22'] }] })
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
2026-10-03 15:57:53 +08:00
|
|
|
expect(listDispatchCandidates).toHaveBeenLastCalledWith({ draftToken: 'original-draft' })
|
|
|
|
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
2026-10-18 16:37:37 +08:00
|
|
|
|
|
|
|
|
it('shows a draft recovery error and only loads candidates after a successful retry with the original token', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockRejectedValueOnce(new Error('草稿查询超时')).mockResolvedValueOnce([twoGroups])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue(goodResult)
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises()
|
|
|
|
|
expect(wrapper.get('[data-test="retry"]').attributes('title')).toBe('排车草稿恢复失败')
|
|
|
|
|
expect(listDispatchCandidates).not.toHaveBeenCalled()
|
|
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
|
|
|
|
await wrapper.get('[data-test="retry"]').trigger('click'); await flushPromises()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 4 · 箱数 8')
|
|
|
|
|
await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith(expectedBatch)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('keeps local relabel choices on close and reopen while describing their actual unlocked state', async () => {
|
|
|
|
|
const relabel: DispatchPalletCandidateDto = { ...plan, sourceStage: 'RELABEL', orderRefs: 'RL-1' }
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'local-relabel', containers: [{ id: 'c1', capacity: 30, palletIds: [] }] }])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([relabel])
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises()
|
|
|
|
|
wrapper.getComponent({ name: 'ElDropdown' }).vm.$emit('command', 'c1'); await flushPromises()
|
|
|
|
|
expect(lockDispatchDraftPallet).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.text()).toContain('已选择,尚未锁定')
|
|
|
|
|
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 1 · 箱数 2')
|
|
|
|
|
expect(wrapper.text()).toContain('已选择,尚未锁定')
|
|
|
|
|
expect(listDispatchDrafts).toHaveBeenCalledTimes(2)
|
|
|
|
|
await wrapper.findAll('button').find(node => node.text() === '移除')!.trigger('click'); await flushPromises()
|
|
|
|
|
expect(unlockDispatchDraftPallet).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 0 · 箱数 0')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each(['RELABEL_APPLICATION', 'RETURN_RELABEL'])('rejects a partial %s application after refreshing its complete candidate set', async orderType => {
|
|
|
|
|
const relabel = { ...plan, sourceStage: 'RELABEL' as const, orderRefs: 'RL-1', orderType }
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'local-relabel', containers: [{ id: 'c1', capacity: 30, palletIds: ['101'] }] }])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([relabel, { ...relabel, planPalletId: '13', warehousePalletId: '103' }])
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
|
|
|
|
expect(listDispatchCandidates).toHaveBeenLastCalledWith({ sourceStage: 'RELABEL', orderNo: 'RL-1', draftToken: 'local-relabel' })
|
|
|
|
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
|
|
|
|
expect(wrapper.getComponent({ name: 'OperationErrorToast' }).props('details')?.message).toContain('全部应排托盘')
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 1 · 箱数 2')
|
|
|
|
|
expect(wrapper.emitted('created')).toBeUndefined()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('preserves all previous selections when reopening fails to read drafts, then recovers the same groups on retry', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValueOnce([twoGroups]).mockRejectedValueOnce(new Error('查询超时')).mockResolvedValueOnce([twoGroups])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
|
|
|
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue(goodResult)
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises()
|
|
|
|
|
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
|
|
|
|
expect(wrapper.get('[data-test="retry"]').attributes('title')).toBe('排车草稿恢复失败')
|
|
|
|
|
expect(listDispatchCandidates).toHaveBeenCalledTimes(1)
|
|
|
|
|
expect(wrapper.findAll('button').find(button => button.text() === '预排车')!.attributes('disabled')).toBeDefined()
|
|
|
|
|
await wrapper.get('[data-test="retry"]').trigger('click'); await flushPromises()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 4 · 箱数 8')
|
|
|
|
|
await submitDraft(wrapper)
|
|
|
|
|
expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith(expectedBatch)
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('requires explicit release to restart an expired draft and keeps choices when release fails', async () => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValueOnce([twoGroups]).mockResolvedValue([])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
|
|
|
|
vi.mocked(ElMessageBox.confirm).mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
|
|
|
|
|
vi.mocked(releaseDispatchDraft).mockRejectedValueOnce(new Error('释放超时')).mockResolvedValueOnce(null)
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises()
|
|
|
|
|
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
|
|
|
|
expect(wrapper.get('[data-test="retry"]').attributes('message')).toContain('原草稿已失效')
|
|
|
|
|
expect(releaseDispatchDraft).not.toHaveBeenCalled()
|
|
|
|
|
const restart = () => wrapper.findAll('button').find(button => button.text() === '释放草稿并重新选择')!
|
|
|
|
|
await restart().trigger('click'); await flushPromises()
|
|
|
|
|
expect(releaseDispatchDraft).toHaveBeenLastCalledWith('original-draft')
|
|
|
|
|
expect(wrapper.get('[data-test="retry"]').attributes('message')).toContain('原选择已保留')
|
|
|
|
|
await restart().trigger('click'); await flushPromises()
|
|
|
|
|
expect(releaseDispatchDraft).toHaveBeenNthCalledWith(2, 'original-draft')
|
|
|
|
|
expect(wrapper.find('[data-test="retry"]').exists()).toBe(false)
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 0 · 箱数 0')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it.each([plan, returned])('does not claim a failed source lock succeeded for $sourceType', async candidate => {
|
|
|
|
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'lock-draft', containers: [{ id: 'c1', capacity: 30, palletIds: [] }] }])
|
|
|
|
|
vi.mocked(listDispatchCandidates).mockResolvedValue([candidate])
|
|
|
|
|
vi.mocked(lockDispatchDraftPallet).mockRejectedValueOnce({ isAxiosError: true, response: { status: 409, data: { code: 'A0409', message: '来源已被其他草稿锁定', requestId: 'lock-request', traceId: 'lock-trace' } } }).mockResolvedValueOnce(null)
|
|
|
|
|
const wrapper = mountDraft(); await flushPromises()
|
|
|
|
|
wrapper.getComponent({ name: 'ElDropdown' }).vm.$emit('command', 'c1'); await flushPromises()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 0 · 箱数 0')
|
|
|
|
|
expect(wrapper.getComponent({ name: 'OperationErrorToast' }).props('details')).toMatchObject({ message: '来源已被其他草稿锁定', requestId: 'lock-request', traceId: 'lock-trace' })
|
|
|
|
|
wrapper.getComponent({ name: 'ElDropdown' }).vm.$emit('command', 'c1'); await flushPromises()
|
|
|
|
|
expect(wrapper.text()).toContain('托盘 1 · 箱数 2')
|
|
|
|
|
expect(wrapper.text()).toContain('已锁定')
|
|
|
|
|
expect(lockDispatchDraftPallet).toHaveBeenLastCalledWith(candidate.sourceType === 'RETURN_RECEIPT' ? 'RETURN:22' : '11', 'lock-draft', 'c1')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|