Files
admin-web/src/modules/overseas-warehouse/components/SmartDispatchCargoBoard.test.ts
T
2026-10-21 15:49:35 +08:00

209 lines
14 KiB
TypeScript

import ElementPlus from 'element-plus'
import { defineComponent, h } from 'vue'
import { enableAutoUnmount, mount, type VueWrapper } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import SmartDispatchCargoBoard from './SmartDispatchCargoBoard.vue'
import { candidate, sharedPalletCandidates } from '../dispatchTestFixtures'
import type { Candidate, DispatchDraft } from '../dispatchContracts'
enableAutoUnmount(afterEach)
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
function draft(): DispatchDraft {
return { draftToken: 'server-token', recordVersion: 2, warehouseId: '321', status: 'ACTIVE', expiresAt: '2026-12-01T00:00:00Z', sources: [], containers: [
{ draftContainerId: 'car-1', sequenceNo: 1, capacity: 30, sourceKeys: [] },
{ draftContainerId: 'car-2', sequenceNo: 2, capacity: 15, sourceKeys: [] },
] }
}
function render(candidates: Candidate[] = [candidate()], extra: Record<string, unknown> = {}) {
return mount(SmartDispatchCargoBoard, { props: { candidates, draft: draft(), activeContainerId: 'car-1', capacity: 30, disabled: false, warehouseSelected: true, page: 1, size: 20, total: candidates.length, ...extra }, global: { plugins: [ElementPlus] } })
}
async function select(wrapper: VueWrapper, index = 0) { await wrapper.get('[aria-label="待排订单"]').findAll('tbody tr')[index]!.get('button').trigger('click') }
function pallet(wrapper: VueWrapper) { return wrapper.get('[aria-label="托盘明细"] tr[draggable]') }
function transfer() { return { setData: vi.fn(), getData: vi.fn(() => '{"sourceKey":"forged","containerId":"car-2"}'), effectAllowed: '', dropEffect: '' } }
describe('smart dispatch three-column cargo board', () => {
it('shows only the chosen order pallets and a default empty car without emitting writes', async () => {
const first = candidate(), second = candidate({ sourceKey: 'source-2', orderNo: 'ORDER-2', palletNo: 'PALLET-2' })
const wrapper = render([first, second], { draft: null, activeContainerId: '' })
expect(wrapper.get('[aria-label="托盘明细"]').text()).toContain('请选择左侧订单')
expect(wrapper.get('[aria-label="托盘明细"]').findAll('thead th').map(cell => cell.text())).toEqual(['托盘号', '箱数'])
expect(wrapper.findAll('[aria-label="车辆装载"] article')).toHaveLength(1)
expect(wrapper.get('[aria-label="第 1 辆车"]').text()).toContain('30 托位')
await select(wrapper, 1)
expect(pallet(wrapper).text()).toContain('PALLET-2')
expect(wrapper.get('[aria-label="托盘明细"]').text()).not.toContain('PALLET-1')
expect(wrapper.emitted('add')).toBeUndefined()
await pallet(wrapper).get('button').trigger('click')
expect(wrapper.emitted('add')).toEqual([[second, '']])
})
it('drops a current pallet into a non-active car using the drop target identity', async () => {
const source = candidate(), wrapper = render([source]), dataTransfer = transfer()
await select(wrapper)
await pallet(wrapper).trigger('dragstart', { dataTransfer })
await wrapper.get('[aria-label="第 2 辆车"]').trigger('dragover', { dataTransfer })
await wrapper.get('[aria-label="第 2 辆车"]').trigger('drop', { dataTransfer })
expect(wrapper.emitted('add')).toEqual([[source, 'car-2']])
expect(dataTransfer.getData).not.toHaveBeenCalled()
await wrapper.get('[aria-label="第 1 辆车"]').trigger('drop', { dataTransfer })
expect(wrapper.emitted('add')).toHaveLength(1)
})
it('rejects external or forged drag payloads that were never selected inside this component', async () => {
const wrapper = render(), dataTransfer = transfer()
await select(wrapper)
await wrapper.get('[aria-label="第 2 辆车"]').trigger('dragover', { dataTransfer })
await wrapper.get('[aria-label="第 2 辆车"]').trigger('drop', { dataTransfer })
expect(wrapper.emitted('add')).toBeUndefined()
expect(dataTransfer.getData).not.toHaveBeenCalled()
})
it.each(['refresh', 'page', 'size', 'disabled', 'draft'] as const)('rejects a previously started drag after %s changes', async change => {
const source = candidate(), wrapper = render([source]), dataTransfer = transfer()
await select(wrapper); await pallet(wrapper).trigger('dragstart', { dataTransfer })
if (change === 'refresh') await wrapper.setProps({ candidates: [clone(source)] })
if (change === 'page') await wrapper.setProps({ page: 2 })
if (change === 'size') await wrapper.setProps({ size: 10 })
if (change === 'disabled') await wrapper.setProps({ disabled: true })
if (change === 'draft') await wrapper.setProps({ draft: { ...draft(), recordVersion: 3 } })
await wrapper.get('[aria-label="第 2 辆车"]').trigger('drop', { dataTransfer })
expect(wrapper.emitted('add')).toBeUndefined()
})
it('freezes add, removal, selection and pagination while a command is pending', async () => {
const source = candidate(), state = draft()
state.sources = [source]; state.containers[0]!.sourceKeys = [source.sourceKey]
const wrapper = render([source], { draft: state, total: 21 })
await select(wrapper); await wrapper.setProps({ disabled: true })
expect(wrapper.findAll('button').filter(button => ['添加车辆', '移除货物', '删除车辆', '查看托盘'].includes(button.text())).every(button => button.attributes('disabled') !== undefined)).toBe(true)
expect(wrapper.findAll('input[type="radio"]').every(input => input.attributes('disabled') !== undefined)).toBe(true)
expect(wrapper.find('[aria-label="托盘明细"] tr[draggable]').exists()).toBe(false)
await wrapper.get('[aria-label="第 2 辆车"]').trigger('drop')
await wrapper.get('[aria-label="候选货物分页"] button.btn-next').trigger('click')
expect(wrapper.emitted('add')).toBeUndefined()
expect(wrapper.emitted('changePage')).toBeUndefined()
})
it('keeps a shared pallet as one full source group even when only one member is on the current page', async () => {
const sources = sharedPalletCandidates(), wrapper = render([sources[0]!])
await select(wrapper)
expect(wrapper.findAll('[aria-label="托盘明细"] tr[draggable]')).toHaveLength(1)
const group = pallet(wrapper)
expect(group.text()).toContain('整托关联货物一起加入')
expect(group.findAll('details li')).toHaveLength(2)
expect(group.text()).toContain('客户A'); expect(group.text()).toContain('客户B')
expect(group.text()).toContain('8 箱')
await group.get('button').trigger('click')
expect(wrapper.emitted('add')).toEqual([[sources[0], 'car-1']])
expect((wrapper.emitted('add')![0]![0] as Candidate).selectionGroup.sources).toHaveLength(2)
})
it('shows a shared loaded pallet only once and blocks another member from being added to another car', async () => {
const sources = sharedPalletCandidates(), state = draft()
state.sources = sources; state.containers[0]!.sourceKeys = sources.map(row => row.sourceKey)
const wrapper = render(sources, { draft: state, activeContainerId: 'car-2' })
expect(wrapper.get('[aria-label="第 1 辆车"]').text()).toContain('托数 1/30')
expect(wrapper.get('[aria-label="第 1 辆车"]').findAll('.cargo-board-loaded-pallet')).toHaveLength(1)
for (const index of [0, 1]) {
await select(wrapper, index)
expect(wrapper.find('[aria-label="托盘明细"] tr[draggable]').exists()).toBe(false)
}
expect(wrapper.emitted('add')).toBeUndefined()
})
it('moves a confirmed pallet out of the middle list and restores it when removed; only empty cars can be deleted', async () => {
const source = candidate(), state = draft(), wrapper = render([source])
await select(wrapper)
expect(pallet(wrapper).findAll('td')).toHaveLength(2)
expect(pallet(wrapper).get('button').text()).toBe('PALLET-1')
expect(wrapper.text()).not.toContain('加入这辆车')
state.sources = [source]; state.containers[0]!.sourceKeys = [source.sourceKey]
await wrapper.setProps({ draft: clone(state) })
expect(wrapper.find('[aria-label="托盘明细"] tr[draggable]').exists()).toBe(false)
const loaded = wrapper.get('[aria-label="第 1 辆车"]')
expect(loaded.text()).not.toContain('删除车辆')
expect(wrapper.get('[aria-label="第 2 辆车"]').text()).toContain('删除车辆')
await loaded.get('[aria-label="移回托盘 PALLET-1"]').trigger('click')
expect(wrapper.emitted('removeCargo')).toEqual([[source, 'car-1']])
await wrapper.setProps({ draft: draft() })
expect(pallet(wrapper).text()).toContain('PALLET-1')
expect(wrapper.get('[aria-label="第 1 辆车"]').text()).toContain('删除车辆')
})
it('blocks incomplete server-owned groups and does not convert missing measurements to zero', async () => {
const source = candidate({ suggestedWeightKg: null, suggestedVolumeCbm: null })
source.selectionGroup.complete = false
source.selectionGroup.blockReasons = ['同托货物未全部返回']
const wrapper = render([source])
await select(wrapper)
expect(pallet(wrapper).attributes('draggable')).toBe('false')
expect(pallet(wrapper).get('button').attributes('disabled')).toBeDefined()
expect(pallet(wrapper).text()).toContain('同托货物未全部返回')
expect(wrapper.text()).not.toMatch(/0(?:\.0)?\s*(?:kg|m³)/)
expect(wrapper.emitted('add')).toBeUndefined()
})
it('keeps allocated cars visible across candidate pages and reports page events without pretending to have full orders', async () => {
const source = candidate(), state = draft()
state.sources = [source]; state.containers[0]!.sourceKeys = [source.sourceKey]
const wrapper = render([source], { draft: state, total: 21 })
await select(wrapper)
await wrapper.get('[aria-label="候选货物分页"] button.btn-next').trigger('click')
expect(wrapper.emitted('changePage')).toEqual([[2]])
expect(wrapper.get('[aria-label="托盘明细"]').text()).toContain('请选择左侧订单')
await wrapper.setProps({ page: 2, candidates: [candidate({ orderNo: 'NEXT-PAGE', palletNo: 'NEXT-PALLET' })] })
expect(wrapper.get('[aria-label="待排订单"]').text()).not.toContain('货物分页,同一订单可能分布在多页')
expect(wrapper.get('[aria-label="第 1 辆车"]').text()).toContain('PALLET-1')
expect(wrapper.findAll('[aria-label="车辆装载"] article')).toHaveLength(2)
})
it('restores the chosen order after a same-page loading refresh but discards its old drag reference', async () => {
const source = candidate(), wrapper = render([source]), dataTransfer = transfer()
await select(wrapper); await pallet(wrapper).trigger('dragstart', { dataTransfer })
await wrapper.setProps({ candidates: [] })
await wrapper.setProps({ candidates: [clone(source)] })
expect(pallet(wrapper).text()).toContain('PALLET-1')
await wrapper.get('[aria-label="第 2 辆车"]').trigger('drop', { dataTransfer })
expect(wrapper.emitted('add')).toBeUndefined()
})
it('counts distinct planned groups separately when their display pallet labels are the same', async () => {
const first = candidate({ selection: { ...candidate().selection, warehousePalletId: null } })
const second = candidate({ sourceKey: 'source-2', selection: { ...first.selection, planPalletId: '12' } })
const wrapper = render([first, second])
const order = wrapper.get('[aria-label="待排订单"] tbody tr')
expect(order.findAll('td')[3]!.text()).toBe('2')
await select(wrapper)
expect(wrapper.findAll('[aria-label="托盘明细"] tr[draggable]')).toHaveLength(2)
})
it('uses independent vehicle radio groups when two entrypoint dialogs are mounted', () => {
const props = { candidates: [candidate()], draft: draft(), activeContainerId: 'car-1', capacity: 30, disabled: false, warehouseSelected: true, page: 1, size: 20, total: 1 }
const host = mount(defineComponent({ setup: () => () => h('div', [h(SmartDispatchCargoBoard, props), h(SmartDispatchCargoBoard, props)]) }), { global: { plugins: [ElementPlus] } })
const [first, second] = host.findAllComponents(SmartDispatchCargoBoard)
expect(first!.get('input[type="radio"]').attributes('name')).not.toBe(second!.get('input[type="radio"]').attributes('name'))
})
it('accepts an existing dispatch slot as the exact drop target without draft containers', async () => {
const source = candidate()
const wrapper = mount(SmartDispatchCargoBoard, {
props: { candidates: [source], draft: null, activeContainerId: 'PC/1', capacity: 0, disabled: false, warehouseSelected: true, page: 1, size: 20, total: 1 },
slots: { vehicles: ({ dragOver, drop }: { dragOver: (event: DragEvent, id: string) => void; drop: (event: DragEvent, id: string) => void }) => h('section', { 'aria-label': '真实本车清单', onDragover: (event: DragEvent) => dragOver(event, 'PC/1'), onDrop: (event: DragEvent) => drop(event, 'PC/1') }, 'PALLET-EXISTING') },
global: { plugins: [ElementPlus] },
})
expect(wrapper.find('[aria-label="车辆装载"]').exists()).toBe(true)
expect(wrapper.get('[aria-label="车辆装载"]').text()).not.toContain('添加车辆')
expect(wrapper.text()).toContain('PALLET-EXISTING')
await select(wrapper)
const dataTransfer = transfer()
await pallet(wrapper).trigger('dragstart', { dataTransfer })
await wrapper.get('[aria-label="真实本车清单"]').trigger('dragover', { dataTransfer })
await wrapper.get('[aria-label="真实本车清单"]').trigger('drop', { dataTransfer })
expect(wrapper.emitted('add')).toEqual([[source, 'PC/1']])
await pallet(wrapper).trigger('dragstart', { dataTransfer })
await wrapper.setProps({ activeContainerId: 'PC/2' })
await wrapper.get('[aria-label="真实本车清单"]').trigger('drop', { dataTransfer })
expect(wrapper.emitted('add')).toHaveLength(1)
})
})