From 4ad853711367f03fe6585bed56165de591fd4ac2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 1 Sep 2026 16:35:40 +0800 Subject: [PATCH] test(admin): cover booking-scoped trucking --- .../trucking/api/platformTrucking.test.ts | 10 +++++ .../TruckAppointmentForm.behavior.test.ts | 25 +++++++++++ .../components/TruckAppointmentForm.test.ts | 9 ++-- .../components/TruckAppointmentForm.vue | 8 ++-- .../pages/TruckAppointmentDetailView.test.ts | 8 ++++ .../pages/TruckAppointmentDetailView.vue | 2 +- .../pages/TruckAppointmentsView.test.ts | 45 ++++++++++++++++++- .../trucking/pages/TruckAppointmentsView.vue | 4 +- 8 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/modules/trucking/api/platformTrucking.test.ts b/src/modules/trucking/api/platformTrucking.test.ts index 2da168a6..aa1cc580 100644 --- a/src/modules/trucking/api/platformTrucking.test.ts +++ b/src/modules/trucking/api/platformTrucking.test.ts @@ -41,6 +41,16 @@ describe('truck appointment API', () => { }, { headers: { 'Idempotency-Key': expect.stringContaining('truck-appointment-pickup-9001-') } }) }) + it.each(['SUBMIT', 'DISPATCH', 'PICKUP'] as const)('sends %s without any container field', async (action) => { + vi.mocked(http.post).mockResolvedValue({ data: { id: '9001', status: 'PENDING_DISPATCH' } }) + await performTruckAppointmentAction('9001', action, { expectedVersion: 4, reason: '演示流转' }) + const body = vi.mocked(http.post).mock.calls.at(-1)?.[1] as Record + expect(body).toEqual({ expectedVersion: 4, reason: '演示流转' }) + expect(body).not.toHaveProperty('containerNo') + expect(body).not.toHaveProperty('containerBookingId') + expect(body).not.toHaveProperty('loadingPlanId') + }) + it('drops legacy customer fields instead of forwarding hidden compatibility data', async () => { vi.mocked(http.post).mockResolvedValueOnce({ data: { id: '9001', status: 'DRAFT', attachmentCount: 0 } }) const payload = { carrierId: '200', oceanBookingId: '300', containerBookingId: '400', loadingPlanId: '500', plannedLoadingAt: '2026-08-10T10:00:00', expectedVersion: 0, currency: 'EUR', receivableAmount: '999.00', customerId: 'legacy-customer', customerName: '旧客户快照' } diff --git a/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts b/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts index 00d8f38f..554399d6 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts +++ b/src/modules/trucking/components/TruckAppointmentForm.behavior.test.ts @@ -57,6 +57,7 @@ describe('TruckAppointmentForm booking-only behavior', () => { 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') @@ -65,6 +66,30 @@ describe('TruckAppointmentForm booking-only behavior', () => { wrapper.unmount() }) + it('keeps only the latest candidate response when option reloads overlap', async () => { + let resolveSlow!: (value: Awaited>) => void + let resolveFast!: (value: Awaited>) => 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 } + 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() diff --git a/src/modules/trucking/components/TruckAppointmentForm.test.ts b/src/modules/trucking/components/TruckAppointmentForm.test.ts index e0c06eb4..bbd1b715 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.test.ts +++ b/src/modules/trucking/components/TruckAppointmentForm.test.ts @@ -69,9 +69,10 @@ describe('TruckAppointmentForm interaction contract', () => { }) it('keeps container identity out of the booking-scoped form', () => { - expect(source).not.toContain('柜次号:') - expect(source).not.toContain('集装箱号:') - expect(source).toContain('真实柜次及集装箱号在货柜与预装流程维护') + expect(source).not.toContain('柜次') + expect(source).not.toContain('装柜计划') + expect(source).not.toContain('集装箱号') + expect(source).toContain('货柜资料在货柜与预装流程维护') }) it('keeps the form usable at 390px', () => { @@ -94,7 +95,7 @@ describe('TruckAppointmentForm interaction contract', () => { expect(source).not.toContain('应收拖车费') }) - it('uses four compact desktop columns while retaining two-column relations and wide text fields', () => { + it('keeps the 1920 desktop contract compact without horizontal form overflow', () => { expect(source).toContain('@media (min-width: 1200px)') expect(source).toContain('max-width:25%; flex:0 0 25%') expect(source).toContain('class="relation-column"') diff --git a/src/modules/trucking/components/TruckAppointmentForm.vue b/src/modules/trucking/components/TruckAppointmentForm.vue index b95d1247..c156b93e 100644 --- a/src/modules/trucking/components/TruckAppointmentForm.vue +++ b/src/modules/trucking/components/TruckAppointmentForm.vue @@ -33,7 +33,7 @@ const phoneRule = (label: string) => ({ validator: (_rule: unknown, value: unkno }, trigger: 'blur' }) const rules: FormRules = { oceanBookingId: [{ required: true, message: '请选择已放舱订舱', trigger: 'change' }, { validator: (_rule, value, callback) => { - if (!(bookings.value.some((item) => item.id === String(value)) || (relationLocked.value && selectedBookingFallback.value?.id === String(value)))) return callback(new Error('请选择仍有可预约柜次的已放舱订舱')) + if (!(bookings.value.some((item) => item.id === String(value)) || (relationLocked.value && selectedBookingFallback.value?.id === String(value)))) return callback(new Error('请选择可创建拖车单的已放舱订舱')) if (bookingContainersError.value) return callback(new Error('订舱资料状态加载失败,请重试')) if (!draft.billOfLadingNo?.trim()) return callback(new Error('所选订舱缺少有效提单号')) return callback(bookingDocumentReady.value ? undefined : new Error('所选订舱缺少有效的订舱确认文件或提单文件')) @@ -124,7 +124,7 @@ async function loadOptions() { if (sequence !== optionsLoadSequence) return if (results[0].status === 'fulfilled') carriers.value = results[0].value.records.filter((item) => item.category === 'TRUCKING' && item.status === 'ACTIVE') if (results[1].status === 'fulfilled') candidates.value = results[1].value.records - const failed = ['承运商', '可预约订舱柜次'].filter((_, index) => results[index]?.status === 'rejected') + const failed = ['承运商', '可用订舱'].filter((_, index) => results[index]?.status === 'rejected') if (failed.length) optionsError.value = `${failed.join('、')}选项加载失败` optionsLoading.value = false if (relationLocked.value && draft.oceanBookingId) await loadCurrentBookingContext(String(draft.oceanBookingId)) @@ -140,7 +140,7 @@ async function loadOptions() { } async function validate() { return formRef.value?.validate() } async function submitForm() { if (props.submitting) return; if (await formRef.value?.validate()) emit('submit') } -defineExpose({ validate }) +defineExpose({ validate, loadOptions }) onMounted(() => { void loadOptions() }) watch(() => props.readonly, (readonly) => { if (!readonly && !carriers.value.length) void loadOptions() }) watch(() => props.resource?.id, (id, previous) => { if (id && id !== previous) void loadOptions() }) @@ -148,7 +148,7 @@ watch(() => props.resource?.id, (id, previous) => { if (id && id !== previous) v