2026-08-31 21:05:03 +08:00
|
|
|
import ElementPlus from 'element-plus'
|
|
|
|
|
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'
|
|
|
|
|
|
|
|
|
|
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(),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const model: SaveTruckAppointmentRequest = {
|
|
|
|
|
expectedVersion: 0,
|
|
|
|
|
oceanBookingId: null,
|
|
|
|
|
containerBookingId: null,
|
|
|
|
|
loadingPlanId: null,
|
|
|
|
|
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: '宁波仓' },
|
|
|
|
|
]
|
|
|
|
|
function detail(id: string) {
|
|
|
|
|
const booking = bookings.find((item) => item.id === id)!
|
|
|
|
|
return {
|
|
|
|
|
...booking,
|
|
|
|
|
documents: [{ id: `doc-${id}`, documentType: 'BILL_OF_LADING', valid: true }],
|
|
|
|
|
containers: id === 'booking-1'
|
|
|
|
|
? [
|
|
|
|
|
{ containerBookingId: 'container-1', cabinetNo: 'CAB-1', containerNo: 'CBHU8888883' },
|
|
|
|
|
{ containerBookingId: 'container-invalid', cabinetNo: 'CAB-X', containerNo: '错误柜号' },
|
|
|
|
|
]
|
|
|
|
|
: [{ containerBookingId: 'container-2', cabinetNo: 'CAB-2', containerNo: 'MSCU6639870' }],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('TruckAppointmentForm booking-only behavior', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks()
|
|
|
|
|
vi.mocked(listOceanBookings).mockResolvedValue({ records: bookings } as never)
|
|
|
|
|
vi.mocked(getOceanBooking).mockImplementation(async (id) => detail(id) as never)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('selects a released booking, exposes only valid real containers, and never emits customer fields', async () => {
|
|
|
|
|
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
|
|
|
|
|
selects[0]!.vm.$emit('update:modelValue', 'booking-1')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
|
|
|
|
|
expect(getOceanBooking).toHaveBeenCalledWith('booking-1')
|
|
|
|
|
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'))).toBe(false)
|
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest
|
|
|
|
|
expect(emitted.oceanBookingId).toBe('booking-1')
|
|
|
|
|
expect(emitted.billOfLadingNo).toBe('BL-1')
|
|
|
|
|
expect(emitted).not.toHaveProperty('customerId')
|
|
|
|
|
expect(emitted).not.toHaveProperty('customerName')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('clears a selected container when switching to another booking', async () => {
|
|
|
|
|
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } })
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
|
|
|
|
|
selects[0]!.vm.$emit('update:modelValue', 'booking-1')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
selects[1]!.vm.$emit('update:modelValue', 'container-1')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect((wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest).containerBookingId).toBe('container-1')
|
|
|
|
|
|
|
|
|
|
selects[0]!.vm.$emit('update:modelValue', 'booking-2')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest
|
|
|
|
|
expect(emitted.oceanBookingId).toBe('booking-2')
|
|
|
|
|
expect(emitted.containerBookingId).toBeNull()
|
|
|
|
|
const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || ''))
|
|
|
|
|
expect(labels.some((label) => label.includes('CAB-2'))).toBe(true)
|
|
|
|
|
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')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
expect(wrapper.text()).toContain('订舱版本冲突')
|
|
|
|
|
expect(wrapper.text()).toContain('A0409')
|
|
|
|
|
expect(wrapper.text()).toContain('req-booking')
|
|
|
|
|
expect(wrapper.text()).toContain('trace-booking')
|
|
|
|
|
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 () => {
|
|
|
|
|
vi.mocked(getOceanBooking).mockResolvedValueOnce({ ...detail('booking-1'), status: 'DRAFT' } as never)
|
|
|
|
|
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, 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()
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
2026-09-01 10:36:49 +08:00
|
|
|
|
|
|
|
|
it('switches the payable currency between RMB and USD and normalizes an illegal legacy value', async () => {
|
|
|
|
|
const wrapper = mount(TruckAppointmentForm, {
|
|
|
|
|
props: { modelValue: { ...model, payableAmount: '88.00', currency: 'EUR' as never } },
|
|
|
|
|
global: { plugins: [ElementPlus] },
|
|
|
|
|
})
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const currencySelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((select) => select.props('placeholder') === '请选择币种')!
|
|
|
|
|
expect(currencySelect.props('modelValue')).toBe('RMB')
|
|
|
|
|
currencySelect.vm.$emit('update:modelValue', 'USD')
|
|
|
|
|
await flushPromises()
|
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as SaveTruckAppointmentRequest
|
|
|
|
|
expect(emitted.currency).toBe('USD')
|
|
|
|
|
expect(wrapper.text()).toContain('USD')
|
|
|
|
|
expect(emitted).not.toHaveProperty('receivableAmount')
|
|
|
|
|
wrapper.unmount()
|
|
|
|
|
})
|
2026-08-31 21:05:03 +08:00
|
|
|
})
|