This commit is contained in:
2026-09-01 15:46:46 +08:00
parent c59608ff2c
commit adb077a714
4 changed files with 67 additions and 63 deletions
@@ -3,16 +3,17 @@ import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ApiResponseError } from '@/shared/api/http'
import TruckAppointmentForm from './TruckAppointmentForm.vue'
import type { SaveTruckAppointmentRequest } from '../types'
import { getOceanBooking, listOceanBookings } from '@/modules/ocean-bookings'
import type { SaveTruckAppointmentRequest, TruckAppointmentResource } from '../types'
import { getOceanBooking } from '@/modules/ocean-bookings'
import { listTruckAppointmentCandidates } from '../api/platformTrucking'
vi.mock('@/modules/suppliers', () => ({
listSuppliers: vi.fn().mockResolvedValue({ records: [{ id: 'carrier-1', name: '平台拖车公司', category: 'TRUCKING', status: 'ACTIVE' }] }),
}))
vi.mock('@/modules/ocean-bookings', () => ({
listOceanBookings: vi.fn(),
getOceanBooking: vi.fn(),
}))
vi.mock('../api/platformTrucking', () => ({ listTruckAppointmentCandidates: vi.fn() }))
const model: SaveTruckAppointmentRequest = {
expectedVersion: 0,
@@ -22,29 +23,28 @@ const model: SaveTruckAppointmentRequest = {
carrierId: null,
plannedLoadingAt: null,
}
const bookings = [
{ id: 'booking-1', bookingNo: 'OB-1', status: 'RELEASED', billOfLadingNo: 'BL-1', warehouseId: 'WH-1', warehouseName: '义乌仓' },
{ id: 'booking-2', bookingNo: 'OB-2', status: 'RELEASED', billOfLadingNo: 'BL-2', warehouseId: 'WH-2', warehouseName: '东莞仓' },
{ id: 'booking-missing-bill', bookingNo: 'OB-3', status: 'RELEASED', billOfLadingNo: null, warehouseId: 'WH-3', warehouseName: '宁波仓' },
const candidates = [
{ oceanBookingId: 'booking-1', oceanBookingNo: 'OB-1', billOfLadingNo: 'BL-1', warehouseId: 'WH-1', warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', requirementId: 'requirement-1', containerType: '40HC', requirementQuantity: 2, containerBookingId: 'container-1', cabinetNo: 'CAB-1', containerNo: 'CBHU8888883', loadingPlanId: 'plan-1', loadingPlanNo: 'LP-1', loadingPlanStatus: 'EDITING', bookingDocumentReady: true },
{ oceanBookingId: 'booking-1', oceanBookingNo: 'OB-1', billOfLadingNo: 'BL-1', warehouseId: 'WH-1', warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', requirementId: 'requirement-1', containerType: '40HC', requirementQuantity: 2, containerBookingId: 'container-pending', cabinetNo: 'CAB-X', containerNo: null, loadingPlanId: null, loadingPlanNo: null, loadingPlanStatus: null, bookingDocumentReady: true },
{ oceanBookingId: 'booking-2', oceanBookingNo: 'OB-2', billOfLadingNo: 'BL-2', warehouseId: 'WH-2', warehouseName: '东莞仓', channelCode: 'SEA', channelName: '海运', requirementId: 'requirement-2', containerType: '40GP', requirementQuantity: 1, containerBookingId: 'container-2', cabinetNo: 'CAB-2', containerNo: 'MSCU6639870', loadingPlanId: null, loadingPlanNo: null, loadingPlanStatus: null, bookingDocumentReady: true },
]
function detail(id: string) {
const booking = bookings.find((item) => item.id === id)!
const candidate = candidates.find((item) => item.oceanBookingId === id)!
return {
...booking,
id, bookingNo: candidate.oceanBookingNo, status: 'RELEASED', billOfLadingNo: candidate.billOfLadingNo, warehouseId: candidate.warehouseId, warehouseName: candidate.warehouseName,
documents: [{ id: `doc-${id}`, documentType: 'BILL_OF_LADING', valid: true }],
containers: id === 'booking-1'
? [
{ containerBookingId: 'container-1', cabinetNo: 'CAB-1', containerNo: 'CBHU8888883' },
{ containerBookingId: 'container-pending', cabinetNo: 'CAB-X', containerNo: null, containerType: '40HC' },
]
: [{ containerBookingId: 'container-2', cabinetNo: 'CAB-2', containerNo: 'MSCU6639870' }],
containers: [],
}
}
function resource(id: string, bookingId: string, containerId: string): TruckAppointmentResource {
const candidate = candidates.find((item) => item.oceanBookingId === bookingId && item.containerBookingId === containerId)!
return { id, status: 'DRAFT', oceanBookingId: bookingId, oceanBookingNo: candidate.oceanBookingNo, containerBookingId: containerId, cabinetNo: candidate.cabinetNo, containerNo: candidate.containerNo, relatedContainerType: candidate.containerType }
}
describe('TruckAppointmentForm booking-only behavior', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(listOceanBookings).mockResolvedValue({ records: bookings } as never)
vi.mocked(listTruckAppointmentCandidates).mockResolvedValue({ records: candidates, page: 1, size: 200, total: candidates.length, pages: 1 })
vi.mocked(getOceanBooking).mockImplementation(async (id) => detail(id) as never)
})
@@ -55,7 +55,8 @@ describe('TruckAppointmentForm booking-only behavior', () => {
selects[0]!.vm.$emit('update:modelValue', 'booking-1')
await flushPromises()
expect(getOceanBooking).toHaveBeenCalledWith('booking-1')
expect(listTruckAppointmentCandidates).toHaveBeenCalledWith({ page: 1, size: 200 })
expect(getOceanBooking).not.toHaveBeenCalled()
const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || ''))
expect(labels.some((label) => label.includes('CAB-1') && label.includes('CBHU8888883'))).toBe(true)
expect(labels.some((label) => label.includes('CAB-X') && label.includes('40HC') && label.includes('待补录'))).toBe(true)
@@ -87,19 +88,18 @@ describe('TruckAppointmentForm booking-only behavior', () => {
wrapper.unmount()
})
it('ignores stale booking details when the user switches selections quickly', async () => {
it('ignores stale current-booking details when a cached edit route changes', async () => {
let resolveFirst!: (value: ReturnType<typeof detail>) => void
let resolveSecond!: (value: ReturnType<typeof detail>) => void
vi.mocked(getOceanBooking).mockImplementation((id) => new Promise((resolve) => {
if (id === 'booking-1') resolveFirst = resolve as typeof resolveFirst
else resolveSecond = resolve as typeof resolveSecond
}) as never)
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } })
const firstResource = resource('appointment-1', 'booking-1', 'container-1')
const secondResource = resource('appointment-2', 'booking-2', 'container-2')
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1', containerBookingId: 'container-1' }, resource: firstResource }, global: { plugins: [ElementPlus] } })
await flushPromises()
const bookingSelect = wrapper.findAllComponents({ name: 'ElSelect' })[0]!
bookingSelect.vm.$emit('update:modelValue', 'booking-1')
await flushPromises()
bookingSelect.vm.$emit('update:modelValue', 'booking-2')
await wrapper.setProps({ modelValue: { ...model, oceanBookingId: 'booking-2', containerBookingId: 'container-2' }, resource: secondResource })
await flushPromises()
resolveSecond(detail('booking-2'))
@@ -108,20 +108,17 @@ describe('TruckAppointmentForm booking-only behavior', () => {
await flushPromises()
const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || ''))
expect(labels.some((label) => label.includes('CAB-2'))).toBe(true)
expect(labels.some((label) => label.includes('CAB-2') && label.includes('不可重选'))).toBe(true)
expect(labels.some((label) => label.includes('CAB-1'))).toBe(false)
expect(wrapper.text()).not.toContain('所选订舱缺少有效的订舱确认文件或提单文件')
const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest
expect(emitted.oceanBookingId).toBe('booking-2')
expect(emitted.billOfLadingNo).toBe('BL-2')
expect(listTruckAppointmentCandidates).not.toHaveBeenCalled()
wrapper.unmount()
})
it('preserves the business code and request trace when booking readiness fails', async () => {
vi.mocked(getOceanBooking).mockRejectedValueOnce(new ApiResponseError('订舱版本冲突', 'A0409', 409, undefined, 'req-booking', 'trace-booking'))
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } })
await flushPromises()
wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'booking-1')
const current = resource('appointment-1', 'booking-1', 'container-1')
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1', containerBookingId: 'container-1' }, resource: current }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('订舱版本冲突')
expect(wrapper.text()).toContain('A0409')
@@ -130,26 +127,13 @@ describe('TruckAppointmentForm booking-only behavior', () => {
wrapper.unmount()
})
it('blocks a released booking that has no bill of lading number', async () => {
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } })
await flushPromises()
wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'booking-missing-bill')
await flushPromises()
expect(wrapper.text()).toContain('所选订舱缺少有效提单号')
const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest
expect(emitted.billOfLadingNo).toBeNull()
wrapper.unmount()
})
it('rejects a booking that becomes unreleased before its detail is loaded', async () => {
it('flags an edit binding whose booking has become unreleased without adding it to create candidates', async () => {
vi.mocked(getOceanBooking).mockResolvedValueOnce({ ...detail('booking-1'), status: 'DRAFT' } as never)
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } })
const current = resource('appointment-1', 'booking-1', 'container-1')
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1', containerBookingId: 'container-1' }, resource: current }, global: { plugins: [ElementPlus] } })
await flushPromises()
wrapper.findAllComponents({ name: 'ElSelect' })[0]!.vm.$emit('update:modelValue', 'booking-1')
await flushPromises()
expect(wrapper.text()).toContain('所选订舱尚未放舱')
const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest
expect(emitted.containerBookingId).toBeNull()
expect(wrapper.text()).toContain('当前绑定订舱已不是放舱状态')
expect(listTruckAppointmentCandidates).not.toHaveBeenCalled()
wrapper.unmount()
})