171 lines
9.6 KiB
TypeScript
171 lines
9.6 KiB
TypeScript
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, 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', () => ({
|
|
getOceanBooking: vi.fn(),
|
|
}))
|
|
vi.mock('../api/platformTrucking', () => ({ listTruckAppointmentCandidates: vi.fn() }))
|
|
|
|
const model: SaveTruckAppointmentRequest = {
|
|
expectedVersion: 0,
|
|
oceanBookingId: null,
|
|
carrierId: null,
|
|
plannedLoadingAt: null,
|
|
}
|
|
const candidates = [
|
|
{ oceanBookingId: 'booking-1', oceanBookingNo: 'OB-1', billOfLadingNo: 'BL-1', warehouseId: 'WH-1', warehouseName: '义乌仓', channelCode: 'SEA', channelName: '海运', bookingDocumentReady: true },
|
|
{ oceanBookingId: 'booking-2', oceanBookingNo: 'OB-2', billOfLadingNo: 'BL-2', warehouseId: 'WH-2', warehouseName: '东莞仓', channelCode: 'SEA', channelName: '海运', bookingDocumentReady: true },
|
|
]
|
|
function detail(id: string) {
|
|
const candidate = candidates.find((item) => item.oceanBookingId === id)!
|
|
return {
|
|
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: [],
|
|
}
|
|
}
|
|
function resource(id: string, bookingId: string): TruckAppointmentResource {
|
|
const candidate = candidates.find((item) => item.oceanBookingId === bookingId)!
|
|
return { id, status: 'DRAFT', oceanBookingId: bookingId, oceanBookingNo: candidate.oceanBookingNo, billOfLadingNo: candidate.billOfLadingNo, warehouseId: candidate.warehouseId, warehouseName: candidate.warehouseName }
|
|
}
|
|
|
|
describe('TruckAppointmentForm booking-only behavior', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
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)
|
|
})
|
|
|
|
it('selects only server-approved released bookings and never exposes container identity', 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(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('OB-1') && label.includes('BL-1'))).toBe(true)
|
|
expect(labels.some((label) => label.includes('CAB-'))).toBe(false)
|
|
expect(candidates[0]).not.toHaveProperty('containerBookingId')
|
|
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('keeps only the latest candidate response when option reloads overlap', async () => {
|
|
let resolveSlow!: (value: Awaited<ReturnType<typeof listTruckAppointmentCandidates>>) => void
|
|
let resolveFast!: (value: Awaited<ReturnType<typeof listTruckAppointmentCandidates>>) => void
|
|
vi.mocked(listTruckAppointmentCandidates)
|
|
.mockResolvedValueOnce({ records: candidates, page: 1, size: 200, total: candidates.length, pages: 1 })
|
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveSlow = resolve }))
|
|
.mockImplementationOnce(() => new Promise((resolve) => { resolveFast = resolve }))
|
|
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model } }, global: { plugins: [ElementPlus] } })
|
|
await flushPromises()
|
|
const vm = wrapper.vm as unknown as { loadOptions: () => Promise<void> }
|
|
const slow = vm.loadOptions()
|
|
const fast = vm.loadOptions()
|
|
resolveFast({ records: [candidates[1]!], page: 1, size: 200, total: 1, pages: 1 })
|
|
await fast
|
|
resolveSlow({ records: [candidates[0]!], page: 1, size: 200, total: 1, pages: 1 })
|
|
await slow
|
|
await flushPromises()
|
|
|
|
const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || ''))
|
|
expect(labels.some((label) => label.includes('OB-2'))).toBe(true)
|
|
expect(labels.some((label) => label.includes('OB-1'))).toBe(false)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('replaces frozen booking snapshots when switching the selected 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[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.billOfLadingNo).toBe('BL-2')
|
|
expect(emitted.warehouseId).toBe('WH-2')
|
|
const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || ''))
|
|
expect(labels.some((label) => label.includes('OB-2'))).toBe(true)
|
|
wrapper.unmount()
|
|
})
|
|
|
|
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 firstResource = resource('appointment-1', 'booking-1')
|
|
const secondResource = resource('appointment-2', 'booking-2')
|
|
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1' }, resource: firstResource }, global: { plugins: [ElementPlus] } })
|
|
await flushPromises()
|
|
await wrapper.setProps({ modelValue: { ...model, oceanBookingId: 'booking-2' }, resource: secondResource })
|
|
await flushPromises()
|
|
|
|
resolveSecond(detail('booking-2'))
|
|
await flushPromises()
|
|
resolveFirst({ ...detail('booking-1'), documents: [] })
|
|
await flushPromises()
|
|
|
|
const labels = wrapper.findAllComponents({ name: 'ElOption' }).map((option) => String(option.props('label') || ''))
|
|
expect(labels.some((label) => label.includes('OB-2') && label.includes('不可重选'))).toBe(true)
|
|
expect(labels.some((label) => label.includes('OB-1'))).toBe(false)
|
|
expect(wrapper.text()).not.toContain('所选订舱缺少有效的订舱确认文件或提单文件')
|
|
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 current = resource('appointment-1', 'booking-1')
|
|
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1' }, resource: current }, global: { plugins: [ElementPlus] } })
|
|
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('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 current = resource('appointment-1', 'booking-1')
|
|
const wrapper = mount(TruckAppointmentForm, { props: { modelValue: { ...model, oceanBookingId: 'booking-1' }, resource: current }, global: { plugins: [ElementPlus] } })
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('当前绑定订舱已不是放舱状态')
|
|
expect(listTruckAppointmentCandidates).not.toHaveBeenCalled()
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('offers only RMB for domestic trucking payables', async () => {
|
|
const wrapper = mount(TruckAppointmentForm, {
|
|
props: { modelValue: { ...model, payableAmount: '88.00', currency: 'RMB' } },
|
|
global: { plugins: [ElementPlus] },
|
|
})
|
|
await flushPromises()
|
|
const currencySelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((select) => select.props('placeholder') === '请选择币种')!
|
|
expect(currencySelect.props('modelValue')).toBe('RMB')
|
|
expect(currencySelect.findAllComponents({ name: 'ElOption' }).map(option => option.props('value'))).toEqual(['RMB'])
|
|
wrapper.unmount()
|
|
})
|
|
})
|