87 lines
6.9 KiB
TypeScript
87 lines
6.9 KiB
TypeScript
import ElementPlus, { ElInputNumber } from 'element-plus'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { beforeEach, expect, it, vi } from 'vitest'
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
import DispatchAppointmentAllocationsDialog from './DispatchAppointmentAllocationsDialog.vue'
|
|
import * as api from '../api/dispatches'
|
|
import type { AppointmentAllocations } from '../dispatchContracts'
|
|
import { command } from '../dispatchTestFixtures'
|
|
|
|
const access = vi.hoisted(() => ({ can: true }))
|
|
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => access.can }) }))
|
|
vi.mock('../api/dispatches', () => ({ getDispatchAppointmentAllocations: vi.fn(), saveDispatchAppointmentAllocations: vi.fn() }))
|
|
function facts(): AppointmentAllocations {
|
|
return { dispatchNo: 'PC/1', recordVersion: '0', edit: { allowed: true, blockCode: null, blockReason: null }, items: ['11', '12'].map((id, index) => ({
|
|
dispatchItemId: id, planPalletId: `${index + 21}`, customerId: '101', customerName: '客户甲', palletNo: `PALLET-${index + 1}`, allocations: [],
|
|
sources: [{ sourceKey: 'ORDER:1', orderNo: 'WC-1', inboundCode: 'IN-1', destinationKey: 'FBA:ACY2', packageCount: 10, blockReasons: [],
|
|
options: [{ batchKey: 'FBA_APPOINTMENT:9', batchNo: 'YY-9', recordVersion: '0', availablePackages: 20, confirmationNo: 'CONF-9', appointmentAt: '2026-10-26T10:00:00', appointmentEndAt: null, timezone: 'America/New_York' }] }],
|
|
})) }
|
|
}
|
|
async function open() {
|
|
const wrapper = mount(DispatchAppointmentAllocationsDialog, { props: { modelValue: true, dispatchNo: 'PC/1' }, global: { plugins: [ElementPlus] } })
|
|
await flushPromises(); return wrapper
|
|
}
|
|
beforeEach(() => {
|
|
vi.clearAllMocks(); access.can = true
|
|
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
|
|
vi.mocked(api.getDispatchAppointmentAllocations).mockResolvedValue(facts())
|
|
vi.mocked(api.saveDispatchAppointmentAllocations).mockResolvedValue(command('EDIT'))
|
|
})
|
|
it('links both pallets to the confirmed appointment on the original dispatch with explicit version zero', async () => {
|
|
const wrapper = await open()
|
|
expect(wrapper.text()).toContain('WC-1'); expect(wrapper.text()).toContain('IN-1'); expect(wrapper.text()).toContain('America/New_York')
|
|
expect(wrapper.text()).not.toContain('ORDER:1')
|
|
const dialog = wrapper.findComponent(AppDialog)
|
|
expect(dialog.props('confirmDisabled')).toBe(false)
|
|
dialog.vm.$emit('confirm'); dialog.vm.$emit('confirm'); await flushPromises()
|
|
expect(api.saveDispatchAppointmentAllocations).toHaveBeenCalledTimes(1)
|
|
expect(api.saveDispatchAppointmentAllocations).toHaveBeenCalledWith('PC/1', expect.objectContaining({ recordVersion: '0', dispatchItemIds: ['11', '12'],
|
|
batchAllocations: [expect.objectContaining({ planPalletId: '21', packageCount: 10, recordVersion: '0' }), expect.objectContaining({ planPalletId: '22', packageCount: 10, recordVersion: '0' })] }))
|
|
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
|
|
})
|
|
it('counts a shared source quota once across pallets and rejects incomplete allocation', async () => {
|
|
const value = facts(); value.items.forEach(item => { item.sources[0]!.options[0]!.availablePackages = 15 })
|
|
vi.mocked(api.getDispatchAppointmentAllocations).mockResolvedValue(value)
|
|
const wrapper = await open(); const dialog = wrapper.findComponent(AppDialog)
|
|
expect(dialog.props('confirmDisabled')).toBe(true); expect(wrapper.text()).toContain('本票剩余可排量不足')
|
|
wrapper.findAllComponents(ElInputNumber)[0]!.vm.$emit('update:modelValue', 5); await flushPromises()
|
|
expect(dialog.props('confirmDisabled')).toBe(true); expect(wrapper.text()).toContain('须完整分配 10 箱')
|
|
dialog.vm.$emit('confirm'); await flushPromises(); expect(api.saveDispatchAppointmentAllocations).not.toHaveBeenCalled(); wrapper.unmount()
|
|
})
|
|
it('preserves edited quantities after rejection and retries with current versions and a new command key', async () => {
|
|
const value = facts(); value.items = value.items.slice(0, 1)
|
|
value.items[0]!.sources[0]!.options.push({ ...value.items[0]!.sources[0]!.options[0]!, batchKey: 'FBA_APPOINTMENT:10', batchNo: 'YY-10' })
|
|
vi.mocked(api.getDispatchAppointmentAllocations).mockResolvedValue(value)
|
|
const wrapper = await open(); const inputs = wrapper.findAllComponents(ElInputNumber)
|
|
inputs[0]!.vm.$emit('update:modelValue', 6); inputs[1]!.vm.$emit('update:modelValue', 4); await flushPromises()
|
|
const latest = structuredClone(value); latest.recordVersion = '2'; latest.items[0]!.sources[0]!.options.forEach(option => { option.recordVersion = '3' })
|
|
vi.mocked(api.saveDispatchAppointmentAllocations).mockRejectedValueOnce(new Error('版本已变化'))
|
|
vi.mocked(api.getDispatchAppointmentAllocations).mockResolvedValue(latest)
|
|
wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises()
|
|
expect(wrapper.findAllComponents(ElInputNumber).map(input => input.props('modelValue'))).toEqual([6, 4])
|
|
expect(wrapper.emitted('saved')).toBeUndefined()
|
|
wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises()
|
|
const calls = vi.mocked(api.saveDispatchAppointmentAllocations).mock.calls
|
|
expect(calls).toHaveLength(2); expect(calls[1]![1].idempotencyKey).not.toBe(calls[0]![1].idempotencyKey)
|
|
expect(calls[1]![1]).toMatchObject({ recordVersion: '2', batchAllocations: [{ recordVersion: '3', packageCount: 6 }, { recordVersion: '3', packageCount: 4 }] })
|
|
wrapper.unmount()
|
|
})
|
|
it('retains an unavailable former choice after a failed save until the user clears it', async () => {
|
|
const wrapper = await open()
|
|
vi.mocked(api.saveDispatchAppointmentAllocations).mockRejectedValueOnce(new Error('预约已变化'))
|
|
const latest = facts(); latest.items[0]!.sources[0]!.options = []
|
|
vi.mocked(api.getDispatchAppointmentAllocations).mockResolvedValue(latest)
|
|
wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises()
|
|
expect(wrapper.text()).toContain('已失效'); expect(wrapper.findAllComponents(ElInputNumber)[0]!.props('modelValue')).toBe(10)
|
|
expect(wrapper.findComponent(AppDialog).props('confirmDisabled')).toBe(true); wrapper.unmount()
|
|
})
|
|
it.each(['permission', 'gate', 'unconfirmed'] as const)('does not save when blocked by %s', async mode => {
|
|
const value = facts()
|
|
if (mode === 'permission') access.can = false
|
|
if (mode === 'gate') value.edit = { allowed: false, blockCode: 'NOT_ELIGIBLE', blockReason: '本车已有真实扫码' }
|
|
if (mode === 'unconfirmed') value.items[0]!.sources[0] = { ...value.items[0]!.sources[0]!, options: [], blockReasons: ['请先确认送仓时间'] }
|
|
vi.mocked(api.getDispatchAppointmentAllocations).mockResolvedValue(value)
|
|
const wrapper = await open(); expect(wrapper.findComponent(AppDialog).props('confirmDisabled')).toBe(true)
|
|
wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises(); expect(api.saveDispatchAppointmentAllocations).not.toHaveBeenCalled(); wrapper.unmount()
|
|
})
|