Files
admin-web/src/modules/overseas-business/pages/TruckOrderDetailView.test.ts
T
2026-11-03 11:43:50 +08:00

205 lines
14 KiB
TypeScript

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 BusinessDocumentFees from '../components/BusinessDocumentFees.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<string, string> } }))
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> = {}): 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, BusinessDocumentFees: true, ElSelect: true, ElOption: 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) {
const form = wrapper.findComponent(TruckOrderActions).findComponent({ name: 'ElForm' })
if (form.exists() && form.props('model').decision === 'APPROVE') form.props('model').warehouseId = '9'
wrapper.findComponent(TruckOrderActions).findComponent(AppDialog).vm.$emit('confirm'); await flushPromises()
}
const formModel = (wrapper: VueWrapper) => wrapper.findComponent(TruckOrderActions).findComponent({ name: 'ElForm' }).props('model')
vi.mock('@/modules/warehouses/api/platformWarehouses', () => ({ listPlatformWarehouses: vi.fn().mockResolvedValue({ records: [{ id: '9', warehouseCode: 'WH-9', warehouseName: '海外仓' }] }) }))
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('scopes fee operations to the loaded truck document', async () => {
const wrapper = mountDetail(); await flushPromises()
expect(wrapper.findComponent(BusinessDocumentFees).props()).toMatchObject({
businessType: 'US_TRUCK', businessNo: 'TR-TEST', customerId: '9007199254741999', businessId: '9007199254741001',
})
})
it('displays saved pallet counts instead of the unused package count', async () => {
vi.mocked(getUsTruckOrder).mockResolvedValue(order({ palletCount: 3, packageCount: 0 }))
const wrapper = mountDetail(); await flushPromises()
const facts = new Map(wrapper.findAll('.truck-detail-fact').map(fact => [fact.get('dt').text(), fact.get('dd').text()]))
expect(facts.get('托数')).toBe('3')
expect(facts.has('件数')).toBe(false)
})
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()).not.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 moves approval directly to dispatch', 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 = 'PENDING_DISPATCH'; 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('资料核对通过')
expect(wrapper.text()).not.toContain('登记实收'); expect(wrapper.text()).not.toContain('分配托盘')
expect(current.status).toBe('PENDING_DISPATCH')
expect(vi.mocked(executeUsTruckCommand).mock.calls.map(call => call[2].payload.version)).toEqual([1])
})
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 }) }))
})
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: 'PENDING_DISPATCH', version: 2 }))
const wrapper = mountDetail(); await flushPromises(); await click(wrapper, '审核并生成排车')
formModel(wrapper).remark = '核对完成'; await confirm(wrapper)
expect(wrapper.text()).toContain('订单版本冲突')
expect(wrapper.text()).not.toContain('TRUCK-409')
expect(wrapper.text()).not.toContain('请求 ID')
expect(wrapper.text()).not.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(TruckOrderActions).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('uses delivery as the last flow node and omits point aliases', async () => {
vi.mocked(getUsTruckOrder).mockResolvedValue(order({ status: 'COMPLETED' }))
const wrapper = mountDetail(); await flushPromises()
expect(wrapper.findAll('.truck-flow-label').map(node => node.text())).toEqual(['待审核', '排车审核', '取货', '派送中', '送达'])
expect(wrapper.findAll('.truck-flow li.done')).toHaveLength(5)
expect(wrapper.text()).not.toMatch(/B点|C点|B\/C/)
})
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(TruckOrderActions).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: 'PENDING_DISPATCH', version: 2 })); await flushPromises()
expect(wrapper.text()).toContain('排车审核')
})
it('hides obsolete pallet actions after approval and keeps the standalone order read-only', async () => {
vi.mocked(getUsTruckOrder).mockResolvedValue(order({ status: 'PENDING_DISPATCH', version: 2 }))
const wrapper = mountDetail(); await flushPromises()
expect(wrapper.text()).not.toContain('登记实收'); expect(wrapper.text()).not.toContain('分配托盘'); expect(wrapper.text()).not.toContain('超托审核')
})
it('does not offer platform cancellation of a customer truck order', async () => {
const wrapper = mountDetail(); await flushPromises()
expect(wrapper.text()).not.toContain('取消订单')
expect(executeUsTruckCommand).not.toHaveBeenCalled()
})
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(TruckOrderActions).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('审核与操作记录')
})
})