import ElementPlus from 'element-plus' import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAuthStore } from '@/core/auth/auth' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { ApiResponseError } from '@/shared/api/http' import AppDialog from '@/shared/components/AppDialog.vue' import TruckOrderActions from '../components/TruckOrderActions.vue' import { executeUsTruckCommand, getUsTruckOrder } from '../api/platformOverseasBusiness' import { overseasBusinessPermissions as permissions } from '../permissions' import type { LocalTransportDetail } from '../types' import TruckOrderDetailView from './TruckOrderDetailView.vue' const routing = vi.hoisted(() => ({ push: vi.fn(), route: { params: { orderId: 'TR-TEST' }, query: { customerId: '9007199254741999' } as Record } })) vi.mock('vue-router', () => ({ useRoute: () => routing.route, useRouter: () => ({ push: routing.push }) })) vi.mock('../api/platformOverseasBusiness', () => ({ getUsTruckOrder: vi.fn(), executeUsTruckCommand: vi.fn() })) vi.mock('@/core/unsaved-work/useUnsavedWorkRegistration', () => ({ useUnsavedWorkRegistration: () => ({ captureBaseline: vi.fn() }) })) function order(overrides: Partial = {}): LocalTransportDetail { return { id: '9007199254741001', orderNo: 'TR-TEST', customerId: '9007199254741999', fleetArrangement: null, vehicleType: 'PALLET_15', origin: 'LA', originAddress: '起运仓地址', destination: 'ONT', destinationAddress: '目的仓地址', originContactName: '发货人', originContactPhone: '10001', destinationContactName: '收货人', destinationContactPhone: '10002', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00', palletCount: 0, receivedPalletCount: 0, allocatedPalletCount: 0, shippedPalletCount: 0, packageCount: 20, totalWeightKg: '100', totalVolumeCbm: '2', budgetAmount: '500', currency: 'USD', status: 'PENDING_REVIEW', version: 1, remark: '运输备注', createdBy: '7', createdAt: '2026-10-20T08:00:00', updatedAt: '2026-10-20T08:00:00', pallets: [], histories: [], ...overrides, } } function mountDetail(manage = true) { const pinia = createPinia(); setActivePinia(pinia) const auth = useAuthStore(); auth.token = 'test-token'; auth.user = { id: '7', name: '操作员', account: 'tester', roles: [] } auth.permissions = [permissions.truckList, ...(manage ? [permissions.truckStatus] : [])] useAccessPreviewStore().initializeAfterLogin() return mount(TruckOrderDetailView, { global: { plugins: [pinia, ElementPlus], stubs: { teleport: true } } }) } async function click(wrapper: VueWrapper, label: string) { const button = wrapper.findAll('button').find(item => item.text() === label) expect(button, `button ${label}`).toBeDefined() await button!.trigger('click'); await flushPromises() } async function confirm(wrapper: VueWrapper) { wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises() } const formModel = (wrapper: VueWrapper) => wrapper.findComponent(TruckOrderActions).findComponent({ name: 'ElForm' }).props('model') enableAutoUnmount(afterEach) beforeEach(() => { vi.clearAllMocks() vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) routing.route.params = { orderId: 'TR-TEST' }; routing.route.query = { customerId: '9007199254741999' } vi.mocked(getUsTruckOrder).mockResolvedValue(order()) }) afterEach(() => vi.unstubAllGlobals()) describe('truck review and fulfillment', () => { it('groups the business number, single status and actions in the shared detail header', async () => { const wrapper = mountDetail(); await flushPromises() const header = wrapper.find('.detail-page-layout__header') expect(header.find('.density-detail-title').text()).toBe('本土卡车订单 TR-TEST') expect(header.find('.status-badge').text()).toBe('待审核') expect(wrapper.findAll('.status-badge')).toHaveLength(1) expect(header.find('.truck-order-actions').text()).toContain('审核') expect(header.find('.truck-order-actions').text()).toContain('取消订单') await click(wrapper, '刷新订单') expect(getUsTruckOrder).toHaveBeenCalledTimes(2) await click(wrapper, '返回列表') expect(routing.push).toHaveBeenCalledWith({ name: 'overseas-business-trucks' }) }) it('loads customer scoped documents and runs review through receipt, allocation and delivery completion', async () => { let current = order() vi.mocked(executeUsTruckCommand).mockImplementation(async (_no, _customer, operation) => { const next = { ...current, version: current.version + 1 } switch (operation.command) { case 'review': next.status = 'WAITING_INBOUND'; break case 'receive-pallets': next.status = 'WAITING_CONSOLIDATION'; next.palletCount = 2; next.receivedPalletCount = 2 next.pallets = [1, 2].map(sequenceNo => ({ id: `900719925474100${sequenceNo}`, sequenceNo, palletNo: `P-${sequenceNo}`, source: 'PDA_RECEIPT', status: 'RECEIVED', warehousePalletId: null, packageCount: 10, receivedAt: '2026-10-20T10:00:00' })) break case 'allocate-pallets': next.status = 'ALLOCATED'; next.allocatedPalletCount = 2; next.pallets = next.pallets.map(row => ({ ...row, status: 'ALLOCATED' })); break case 'prepare-dispatch': next.status = 'PENDING_DISPATCH'; break case 'dispatch': next.status = 'DISPATCHED'; break case 'start-transit': next.status = 'IN_TRANSIT'; break case 'complete': next.status = 'COMPLETED'; break } next.histories = [...current.histories, { operationType: operation.command === 'review' ? 'REVIEW' : operation.command.toUpperCase().replaceAll('-', '_'), fromStatus: current.status, toStatus: next.status, operatorId: '操作员7', remark: operation.payload.remark || null, createdAt: '2026-10-20T10:00:00' }] current = next; return next }) const wrapper = mountDetail(); await flushPromises() expect(getUsTruckOrder).toHaveBeenCalledWith('TR-TEST', '9007199254741999') expect(wrapper.text()).toContain('起运仓地址'); expect(wrapper.text()).toContain('审核与操作记录') await click(wrapper, '审核'); formModel(wrapper).remark = '资料核对通过'; await confirm(wrapper) expect(wrapper.text()).toContain('登记实收'); expect(wrapper.text()).toContain('资料核对通过') await click(wrapper, '登记实收'); Object.assign(formModel(wrapper), { actualPalletCount: 2, packageCount: 20 }); await confirm(wrapper) await click(wrapper, '分配托盘'); formModel(wrapper).palletIds = current.pallets.map(row => row.id); await confirm(wrapper) for (const label of ['确认待派车', '确认已派车', '确认发运', '确认完成']) { await click(wrapper, label); await confirm(wrapper) } expect(wrapper.find('.truck-order-actions').exists()).toBe(false) expect(current.status).toBe('COMPLETED') expect(vi.mocked(executeUsTruckCommand).mock.calls.map(call => call[2].payload.version)).toEqual([1, 2, 3, 4, 5, 6, 7]) expect(vi.mocked(executeUsTruckCommand).mock.calls[2][2]).toMatchObject({ command: 'allocate-pallets', payload: { palletIds: ['9007199254741001', '9007199254741002'] } }) }) it('requires a rejection reason and displays the resulting rejected state', async () => { const wrapper = mountDetail(); await flushPromises(); await click(wrapper, '审核') formModel(wrapper).decision = 'REJECT'; await confirm(wrapper) expect(executeUsTruckCommand).not.toHaveBeenCalled(); expect(wrapper.text()).toContain('请填写拒绝或取消原因') formModel(wrapper).remark = '取送时间需重新确认' vi.mocked(executeUsTruckCommand).mockResolvedValue(order({ status: 'REJECTED', version: 2 })) await confirm(wrapper) expect(executeUsTruckCommand).toHaveBeenCalledWith('TR-TEST', '9007199254741999', expect.objectContaining({ command: 'review', payload: expect.objectContaining({ decision: 'REJECT', remark: '取送时间需重新确认', version: 1 }) })) expect(wrapper.find('.truck-order-actions').exists()).toBe(false) }) it('preserves one immutable request and retries it after an ambiguous failure, then refreshes the detail', async () => { vi.mocked(executeUsTruckCommand).mockRejectedValueOnce(new ApiResponseError('订单版本冲突', 'A0409', 409, undefined, 'TRUCK-409')).mockResolvedValueOnce(order({ status: 'WAITING_INBOUND', version: 2 })) const wrapper = mountDetail(); await flushPromises(); await click(wrapper, '审核') formModel(wrapper).remark = '核对完成'; await confirm(wrapper) expect(wrapper.text()).toContain('订单版本冲突') expect(wrapper.findComponent({ name: 'ElForm' }).props('disabled')).toBe(true) const original = JSON.parse(JSON.stringify(vi.mocked(executeUsTruckCommand).mock.calls[0][2])) wrapper.findComponent(AppDialog).vm.$emit('update:modelValue', false); await flushPromises() await click(wrapper, '审核'); await confirm(wrapper) expect(vi.mocked(executeUsTruckCommand).mock.calls[1][2]).toEqual(original) expect(wrapper.text()).toContain('登记实收') }) it('locks duplicate submission until the original response arrives', async () => { let finish!: (value: LocalTransportDetail) => void vi.mocked(executeUsTruckCommand).mockImplementation(() => new Promise(resolve => { finish = resolve })) const wrapper = mountDetail(); await flushPromises(); await click(wrapper, '审核'); await confirm(wrapper); await confirm(wrapper) expect(executeUsTruckCommand).toHaveBeenCalledTimes(1) expect(wrapper.findComponent(AppDialog).props('confirmLoading')).toBe(true) for (const button of wrapper.find('.detail-page-layout__header').findAll('button')) expect(button.attributes('disabled')).toBeDefined() finish(order({ status: 'WAITING_INBOUND', version: 2 })); await flushPromises() expect(wrapper.text()).toContain('登记实收') }) it('validates cumulative receipt totals without submitting invalid input', async () => { vi.mocked(getUsTruckOrder).mockResolvedValue(order({ status: 'RECEIVING', palletCount: 3, receivedPalletCount: 2 })) const wrapper = mountDetail(); await flushPromises(); await click(wrapper, '登记实收') formModel(wrapper).actualPalletCount = 1; await confirm(wrapper) expect(executeUsTruckCommand).not.toHaveBeenCalled(); expect(wrapper.text()).toContain('不能少于已实收托数') Object.assign(formModel(wrapper), { actualPalletCount: 3, packageCount: -1 }); await confirm(wrapper) expect(executeUsTruckCommand).not.toHaveBeenCalled() }) it('requires received pallets and permits partial allocation using their string IDs', async () => { const pallet = { id: '9007199254741007', sequenceNo: 1, palletNo: 'P-1', source: 'PDA_RECEIPT', status: 'RECEIVED', warehousePalletId: null, packageCount: 10, receivedAt: null } vi.mocked(getUsTruckOrder).mockResolvedValue(order({ status: 'WAITING_CONSOLIDATION', palletCount: 2, receivedPalletCount: 2, pallets: [pallet] })) const wrapper = mountDetail(); await flushPromises(); await click(wrapper, '分配托盘'); await confirm(wrapper) expect(executeUsTruckCommand).not.toHaveBeenCalled() formModel(wrapper).palletIds = [pallet.id] vi.mocked(executeUsTruckCommand).mockResolvedValue(order({ status: 'PARTIALLY_ALLOCATED', version: 2, allocatedPalletCount: 1, pallets: [{ ...pallet, status: 'ALLOCATED' }] })) await confirm(wrapper) expect(wrapper.text()).toContain('部分分配'); expect(wrapper.text()).not.toContain('确认待派车') expect(executeUsTruckCommand).toHaveBeenCalledWith('TR-TEST', '9007199254741999', expect.objectContaining({ command: 'allocate-pallets', payload: expect.objectContaining({ palletIds: [pallet.id] }) })) }) it.each(['APPROVE', 'REJECT'] as const)('reviews pallet overage with %s', async decision => { vi.mocked(getUsTruckOrder).mockResolvedValue(order({ status: 'PALLET_OVERAGE_REVIEW', palletCount: 2, receivedPalletCount: 3 })) vi.mocked(executeUsTruckCommand).mockResolvedValue(order({ status: 'WAITING_CONSOLIDATION', version: 2 })) const wrapper = mountDetail(); await flushPromises(); await click(wrapper, '超托审核') Object.assign(formModel(wrapper), { decision, remark: '已核对超收货物' }); await confirm(wrapper) expect(executeUsTruckCommand).toHaveBeenCalledWith('TR-TEST', '9007199254741999', expect.objectContaining({ command: 'review-pallet-overage', payload: expect.objectContaining({ decision }) })) }) it('requires cancellation reason and submits only the cancellation status', async () => { const wrapper = mountDetail(); await flushPromises(); await click(wrapper, '取消订单'); await confirm(wrapper) expect(executeUsTruckCommand).not.toHaveBeenCalled() formModel(wrapper).remark = '客户取消需求'; vi.mocked(executeUsTruckCommand).mockResolvedValue(order({ status: 'CANCELLED', version: 2 })); await confirm(wrapper) expect(executeUsTruckCommand).toHaveBeenCalledWith('TR-TEST', '9007199254741999', expect.objectContaining({ command: 'cancel', payload: expect.objectContaining({ status: 'CANCELLED', remark: '客户取消需求' }) })) }) it('keeps list-only users read-only and guards direct attempts to emit submit', async () => { const wrapper = mountDetail(false); await flushPromises() expect(wrapper.findComponent(TruckOrderActions).findAll('button')).toHaveLength(0) wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises() expect(executeUsTruckCommand).not.toHaveBeenCalled() }) it('keeps pool sourced orders on the existing fleet fulfillment path', async () => { vi.mocked(getUsTruckOrder).mockResolvedValue(order({ fleetArrangement: { sourceType: 'FLEET_CAPACITY_OFFER', businessType: 'US_TRUCK', businessOrderNo: 'TR-TEST', bookingNo: 'FB-TEST', tripNo: 'TRIP-TEST', plannedDepartureAt: null, originWarehouseName: 'LA', destinationLabel: 'ONT', deliveryMode: 'SELF_DELIVERY', deliveryInstructions: null, fulfillmentStage: 'WAITING_INBOUND', stageOccurredAt: null, agreedFreightAmount: null, currency: 'USD', incomingNo: null, dispatchNo: null, deliveryNo: null, signedPackages: null, unsignedPackages: null, returnedPackages: null, exceptionSummary: null, readOnlyReason: '按拼车承运实际履约推进', } })) const wrapper = mountDetail(); await flushPromises() expect(wrapper.text()).toContain('按拼车承运实际履约推进') expect(wrapper.findComponent(TruckOrderActions).exists()).toBe(false) expect(executeUsTruckCommand).not.toHaveBeenCalled() }) it('shows a scoped error without requesting detail when customer is missing', async () => { routing.route.query = {}; const wrapper = mountDetail(); await flushPromises() expect(getUsTruckOrder).not.toHaveBeenCalled(); expect(wrapper.text()).toContain('缺少有效客户或订单信息') }) it('retains error metadata and retries a failed detail read', async () => { vi.mocked(getUsTruckOrder).mockRejectedValueOnce(new ApiResponseError('读取失败', 'A0409', 409, undefined, 'DETAIL-409')).mockResolvedValueOnce(order()) const wrapper = mountDetail(); await flushPromises() expect(wrapper.text()).toContain('读取失败'); expect(wrapper.text()).toContain('DETAIL-409') await wrapper.findComponent({ name: 'LoadErrorState' }).vm.$emit('retry'); await flushPromises() expect(wrapper.text()).toContain('TR-TEST'); expect(wrapper.text()).toContain('审核与操作记录') }) })