481 lines
52 KiB
TypeScript
481 lines
52 KiB
TypeScript
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
import ElementPlus, { ElMessage } from 'element-plus'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
import { listPlatformTenants } from '@/modules/iam'
|
|
import type { PlatformProcurement } from '../procurementTypes'
|
|
import { downloadProcurementFile } from '../api/procurementCommands'
|
|
import { submitProcurementCommand } from '../api/procurementCommands'
|
|
import { getProcurementWorkflow } from '../api/procurementWorkflow'
|
|
import type { FleetArrangement } from '../fleetArrangement'
|
|
import type { LocalLooseboardDetail, LocalLooseboardOrder, LocalTransportDetail, LocalTransportOrder } from '../types'
|
|
import {
|
|
createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getOverseasPurchase, getOverseasPurchaseEditor, getUsTruckOrder,
|
|
listLocalLooseboardOrders, listOverseasPurchaseCustomerOptions, listOverseasPurchaseWarehouseOptions,
|
|
listOverseasPurchases, listUsTruckOrders, updateLocalLooseboardOrder, updateOverseasPurchase, updateUsTruckOrder,
|
|
} from '../api/platformOverseasBusiness'
|
|
import PurchaseOrdersView from './PurchaseOrdersView.vue'
|
|
import PurchaseOrderDetailView from './PurchaseOrderDetailView.vue'
|
|
import PurchaseOrderEditView from './PurchaseOrderEditView.vue'
|
|
import TransportOrdersView from './TransportOrdersView.vue'
|
|
import TransportOrderFormView from './TransportOrderFormView.vue'
|
|
|
|
const purchaseListSource = readFileSync(resolve(process.cwd(), 'src/modules/overseas-business/pages/PurchaseOrdersView.vue'), 'utf8')
|
|
const purchaseDetailSource = readFileSync(resolve(process.cwd(), 'src/modules/overseas-business/pages/PurchaseOrderDetailView.vue'), 'utf8')
|
|
|
|
const routing = vi.hoisted(() => ({ push: vi.fn(), route: { params: {} as Record<string, string>, query: {} as Record<string, string> } }))
|
|
vi.mock('vue-router', () => ({ useRoute: () => routing.route, useRouter: () => ({ push: routing.push }) }))
|
|
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
|
|
vi.mock('../api/procurementWorkflow', () => ({ getProcurementWorkflow: vi.fn(), submitProcurementWorkflow: vi.fn() }))
|
|
vi.mock('../api/procurementCommands', async importOriginal => ({
|
|
...await importOriginal<typeof import('../api/procurementCommands')>(),
|
|
downloadProcurementFile: vi.fn(), submitProcurementCommand: vi.fn(),
|
|
}))
|
|
vi.mock('../api/platformOverseasBusiness', () => ({
|
|
listOverseasPurchases: vi.fn(), getOverseasPurchase: vi.fn(), getOverseasPurchaseEditor: vi.fn(), updateOverseasPurchase: vi.fn(), listOverseasPurchaseCustomerOptions: vi.fn(), listOverseasPurchaseWarehouseOptions: vi.fn(),
|
|
listUsTruckOrders: vi.fn(), getUsTruckOrder: vi.fn(), createUsTruckOrder: vi.fn(), updateUsTruckOrder: vi.fn(),
|
|
listLocalLooseboardOrders: vi.fn(), getLocalLooseboardOrder: vi.fn(), createLocalLooseboardOrder: vi.fn(), updateLocalLooseboardOrder: vi.fn(),
|
|
}))
|
|
vi.mock('@/core/unsaved-work/useUnsavedWorkRegistration', () => ({ useUnsavedWorkRegistration: () => ({ captureBaseline: vi.fn() }) }))
|
|
|
|
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
|
|
function memoryStorage(): Storage { const values = new Map<string, string>(); return { get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null, key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value) } }
|
|
|
|
const purchase = {
|
|
id: '9007199254740993', orderNo: 'OP-100', customerId: '10001', customerName: '客户甲', purchaseCategory: '家居', productName: '置物架', productSpecification: '黑色', productUrl: 'https://example.test/product',
|
|
purchaseQuantity: 20, budgetUnitPrice: '12.50', currency: 'USD', purchaseWarehouseId: '20001', purchaseWarehouseName: '洛杉矶采购仓', status: 'RECEIVING' as const, version: 3,
|
|
createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-02T10:00:00', receivingAddress: 'LA', contactName: 'Alice', contactPhone: '10086',
|
|
estimatedArrivalAt: '2026-08-25T12:00:00', remark: '采购备注', createdBy: '7', reviewedBy: '8', reviewRemark: null, reviewedAt: '2026-08-02T09:00:00',
|
|
histories: [{ operationType: 'UPDATE', fromStatus: 'WAIT_PURCHASE', toStatus: 'WAIT_PURCHASE', operatorId: 'INTERNAL-OPERATOR-7', changeDetail: '{"remark":"采购备注"}', remark: null, createdAt: '2026-08-02T10:00:00' }], files: [], fees: [],
|
|
}
|
|
const procurementDetail = (): PlatformProcurement => ({
|
|
customerView: { order: { ...purchase, status: 'WAIT_PURCHASE', plannedWarehouseName: '洛杉矶采购仓', actualWarehouseCode: null, actualWarehouseName: null, receivingAddress: '深圳收件地址', contactName: 'Alice', contactPhone: '13800000000' }, budget: { totalAmount: '250.00', currency: 'USD', authorizedAt: null }, purchased: false, purchaseConfirmed: false, purchaseNeedsAttention: false, progress: [{ key: 'CREATED', title: '客户下单', completedAt: purchase.createdAt }], boxes: [], shipments: [], fees: [], exceptionSummaries: [] },
|
|
purchaseFacts: {}, physical: { dispositions: [], receipts: [], packingVersion: null, packingStatus: null, cartons: [] }, transport: { overseasCargo: [], domesticCargo: [], overseasLabels: null, domesticLabels: null, shipments: [] }, fees: [], feeCorrections: [], controls: { notes: [], remaining: [], closures: [], closureItems: [] }, files: [], reviewHistory: purchase.histories, operations: [], actions: [], internalRemark: '采购备注',
|
|
})
|
|
const truck: LocalTransportOrder = { fleetArrangement: null, id: '9007199254740995', customerId: '10002', orderNo: 'TK-200', vehicleType: 'PALLET_15', origin: 'Long Beach', destination: 'Ontario', palletCount: 2, packageCount: 20, receivedPalletCount: 0, allocatedPalletCount: 0, shippedPalletCount: 0, totalWeightKg: '1200.000', totalVolumeCbm: '16.500', budgetAmount: '850.00', currency: 'USD', originContactName: 'Bob', originContactPhone: '10010', destinationContactName: 'Alice', destinationContactPhone: '10020', pickupAt: '2026-08-28T09:30:00', deliveryAt: '2026-08-28T15:45:00', status: 'PENDING_REVIEW', version: 1, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-02T10:00:00' }
|
|
const truckDetail: LocalTransportDetail = { ...truck, originAddress: 'Pier A', destinationAddress: 'Warehouse B', remark: null, createdBy: '7', pallets: [], histories: [] }
|
|
const looseboard: LocalLooseboardOrder = { fleetArrangement: null, id: '9007199254740997', customerId: '10003', orderNo: 'LB-300', palletRequired: 'YES', transportType: 'WAREHOUSE_TO_WAREHOUSE', origin: 'LA', destination: 'ONT', productName: '家居用品', estimatedPalletCount: 2, packageCount: 8, totalWeightKg: '80.5', totalVolumeCbm: '3.2', budgetAmount: '50.00', currency: 'USD', originContactName: '发货人', originContactPhone: '1', destinationContactName: '收货人', destinationContactPhone: '2', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00', status: 'PENDING_ACCEPTANCE', version: 0, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-01T10:00:00' }
|
|
const looseboardDetail: LocalLooseboardDetail = { ...looseboard, originAddress: null, destinationAddress: null, remark: null, createdBy: '7', histories: [] }
|
|
const arrangement: FleetArrangement = { sourceType: 'FLEET_CAPACITY_OFFER', businessType: 'US_TRUCK', businessOrderNo: 'TR-200', bookingNo: 'FB-TEST', tripNo: 'TRIP-TEST', plannedDepartureAt: null, originWarehouseName: '我仓', destinationLabel: 'ONT8', 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: '池来源订单按实际运输履约推进' }
|
|
|
|
function activatePermissions() { const pinia = createPinia(); setActivePinia(pinia); const auth = useAuthStore(); auth.token = 'test-token'; auth.user = { id: '7', name: '测试操作员', account: 'tester', roles: [] }; auth.permissions = ['platform:overseas-business:purchase:list', 'platform:overseas-business:purchase:edit', 'platform:overseas-business:purchase:review', 'platform:overseas-business:truck:list', 'platform:overseas-business:truck:create', 'platform:overseas-business:truck:edit', 'platform:overseas-business:looseboard:list', 'platform:overseas-business:looseboard:create', 'platform:overseas-business:looseboard:edit']; useAccessPreviewStore().initializeAfterLogin(); return pinia }
|
|
function mountPage(component: Parameters<typeof mount>[0], props?: Record<string, unknown>) { const pinia = activatePermissions(); return mount(component, { attachTo: document.body, props, global: { plugins: [pinia, ElementPlus] } }) }
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void
|
|
let reject!: (reason: Error) => void
|
|
const promise = new Promise<T>((done, fail) => { resolve = done; reject = fail })
|
|
return { promise, resolve, reject }
|
|
}
|
|
async function mountPurchaseReviewRequests() {
|
|
const first = deferred<Awaited<ReturnType<typeof listOverseasPurchaseWarehouseOptions>>>()
|
|
const second = deferred<Awaited<ReturnType<typeof listOverseasPurchaseWarehouseOptions>>>()
|
|
vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [
|
|
{ ...purchase, id: '101', orderNo: 'OP-A', status: 'PENDING_REVIEW', procurementDirection: 'OVERSEAS_TO_DOMESTIC' },
|
|
{ ...purchase, id: '102', orderNo: 'OP-B', status: 'PENDING_REVIEW', procurementDirection: 'DOMESTIC_TO_OVERSEAS' },
|
|
], page: 1, size: 20, total: 2, pages: 1 })
|
|
vi.mocked(listOverseasPurchaseWarehouseOptions).mockImplementation(direction => direction === 'OVERSEAS_TO_DOMESTIC' ? first.promise : second.promise)
|
|
const wrapper = mountPage(PurchaseOrdersView)
|
|
await flushPromises()
|
|
const buttons = wrapper.findAll('button').filter(button => button.text() === '审核')
|
|
await buttons[0]!.trigger('click')
|
|
const dialog = wrapper.findComponent(AppDialog)
|
|
const openSecond = async () => {
|
|
dialog.vm.$emit('update:modelValue', false)
|
|
await wrapper.vm.$nextTick()
|
|
await buttons[1]!.trigger('click')
|
|
}
|
|
return { wrapper, dialog, first, second, openSecond }
|
|
}
|
|
enableAutoUnmount(afterEach); afterAll(() => vi.unstubAllGlobals())
|
|
|
|
describe('overseas business mounted pages', () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub); vi.stubGlobal('localStorage', memoryStorage()); vi.stubGlobal('sessionStorage', memoryStorage()); document.body.innerHTML = ''
|
|
routing.push.mockReset(); routing.route.params = {}; routing.route.query = {}
|
|
vi.mocked(downloadProcurementFile).mockReset()
|
|
vi.mocked(getProcurementWorkflow).mockReset().mockResolvedValue({ orderNo: 'OP-100', version: 3, status: 'RECEIVING', receivedQuantity: 5, stockQuantity: 5, reservedQuantity: 0, availableQuantity: 5, rejectionReason: null, photosNeedUpdate: false, receipts: [], outbounds: [] })
|
|
vi.mocked(listOverseasPurchases).mockReset().mockResolvedValue({ records: [purchase], page: 1, size: 20, total: 1, pages: 1 }); vi.mocked(getOverseasPurchase).mockReset().mockResolvedValue(procurementDetail()); vi.mocked(updateOverseasPurchase).mockReset().mockResolvedValue({ ...purchase, version: 4 })
|
|
vi.mocked(getOverseasPurchaseEditor).mockReset().mockResolvedValue({ customerView: { order: { orderNo: purchase.orderNo, customerId: purchase.customerId, productName: purchase.productName, purchaseQuantity: purchase.purchaseQuantity, version: purchase.version } }, internalRemark: purchase.remark })
|
|
vi.mocked(listOverseasPurchaseCustomerOptions).mockReset().mockResolvedValue([{ customerId: '10001', customerName: '客户甲' }]); vi.mocked(listOverseasPurchaseWarehouseOptions).mockReset().mockResolvedValue([{ warehouseId: '20001', warehouseCode: 'LAX', warehouseName: '洛杉矶采购仓', countryCode: 'US' }])
|
|
vi.mocked(listUsTruckOrders).mockReset().mockResolvedValue({ records: [truck], page: 1, size: 20, total: 1, pages: 1 }); vi.mocked(getUsTruckOrder).mockReset().mockResolvedValue(truckDetail); vi.mocked(createUsTruckOrder).mockReset().mockResolvedValue(truckDetail); vi.mocked(updateUsTruckOrder).mockReset().mockResolvedValue(truckDetail)
|
|
vi.mocked(listLocalLooseboardOrders).mockReset().mockResolvedValue({ records: [looseboard], page: 1, size: 20, total: 1, pages: 1 }); vi.mocked(getLocalLooseboardOrder).mockReset().mockResolvedValue(looseboardDetail); vi.mocked(createLocalLooseboardOrder).mockReset().mockResolvedValue(looseboardDetail); vi.mocked(updateLocalLooseboardOrder).mockReset().mockResolvedValue(looseboardDetail)
|
|
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '可选客户', status: 'ACTIVE' } as never], page: 1, size: 200, total: 1, pages: 1 })
|
|
})
|
|
|
|
|
|
it.each(['查看路线'])('opens the shared route map from looseboard %s without requerying orders', async label => {
|
|
const wrapper = mountPage(TransportOrdersView, { domain: 'looseboard' })
|
|
await flushPromises()
|
|
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('modelValue')).toBe(false)
|
|
const requests = vi.mocked(listLocalLooseboardOrders).mock.calls.length
|
|
await wrapper.findAll('button').find(button => button.text() === label)!.trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('modelValue')).toBe(true)
|
|
expect(document.body.textContent).toContain('LA → ONT')
|
|
expect(listLocalLooseboardOrders).toHaveBeenCalledTimes(requests)
|
|
})
|
|
|
|
it.each([false, true])('opens truck route map on create/edit without saving (%s)', async editing => {
|
|
if (editing) { routing.route.params = { orderId: truck.id }; routing.route.query = { customerId: truck.customerId } }
|
|
const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' })
|
|
await flushPromises()
|
|
await wrapper.findAll('button').find(button => button.text() === '查看路线')!.trigger('click')
|
|
await flushPromises()
|
|
expect(wrapper.findComponent({ name: 'TransportRouteDialog' }).props('modelValue')).toBe(true)
|
|
expect(createUsTruckOrder).not.toHaveBeenCalled()
|
|
expect(updateUsTruckOrder).not.toHaveBeenCalled()
|
|
const dialog = wrapper.findComponent({ name: 'TransportRouteDialog' })
|
|
expect(dialog.props('origin')).toEqual({ kind: 'address', label: editing ? 'Long Beach' : '', address: editing ? 'Long Beach Pier A' : '' })
|
|
expect(dialog.props('destinations')).toEqual([{ kind: 'address', label: editing ? 'Ontario' : '', address: editing ? 'Ontario Warehouse B' : '' }])
|
|
})
|
|
|
|
it('opens the associated commercial order using a server-side order number filter', async () => {
|
|
routing.route.query = { orderNo: 'LB-LINKED' }
|
|
const wrapper = mount(TransportOrdersView, { props: { domain: 'looseboard' }, global: { plugins: [ElementPlus] } })
|
|
await flushPromises()
|
|
expect(listLocalLooseboardOrders).toHaveBeenCalledWith(expect.objectContaining({ orderNo: 'LB-LINKED' }))
|
|
expect(wrapper.find('input[placeholder="请输入商业单号"]').element).toHaveProperty('value', 'LB-LINKED')
|
|
})
|
|
|
|
it('opens pool arrangements from the list without another request or exposing edit actions', async () => {
|
|
vi.mocked(listUsTruckOrders).mockResolvedValue({ records: [{ ...truck, fleetArrangement: arrangement, status: 'WAITING_INBOUND' }], page: 1, size: 20, total: 1, pages: 1 })
|
|
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
|
|
expect(wrapper.findAll('.el-table__body-wrapper button').some(b => b.text() === '编辑')).toBe(false)
|
|
await wrapper.findAll('.el-table__body-wrapper button').find(b => b.text() === '承运安排')!.trigger('click'); await flushPromises()
|
|
expect(document.body.textContent).toContain('池来源订单按实际运输履约推进')
|
|
expect(document.body.textContent).toContain('FB-TEST'); expect(document.body.textContent).toContain('— / — / —')
|
|
expect(listUsTruckOrders).toHaveBeenCalledOnce(); expect(getUsTruckOrder).not.toHaveBeenCalled()
|
|
const dialog = wrapper.findComponent(AppDialog)
|
|
await dialog.findAll('.app-dialog__footer button').find(button => button.text() === '关闭')!.trigger('click'); await flushPromises()
|
|
expect(dialog.props('modelValue')).toBe(false)
|
|
})
|
|
it('keeps a pool order read-only even when an old edit deep link is used', async () => {
|
|
routing.route.params = { orderId: 'TR-200' }; routing.route.query = { customerId: truck.customerId }
|
|
vi.mocked(getUsTruckOrder).mockResolvedValue({ ...truckDetail, fleetArrangement: arrangement, status: 'WAITING_INBOUND' })
|
|
const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises()
|
|
expect(wrapper.text()).toContain('池来源订单按实际运输履约推进')
|
|
expect(wrapper.find('form').exists()).toBe(false); expect(wrapper.findAll('button').some(b => b.text() === '保存修改')).toBe(false)
|
|
expect(updateUsTruckOrder).not.toHaveBeenCalled(); expect(getUsTruckOrder).toHaveBeenCalledOnce()
|
|
})
|
|
it('shows declared product and forecast pallets without exposing obsolete container selection', async () => {
|
|
const wrapper = mountPage(TransportOrdersView, { domain: 'looseboard' }); await flushPromises()
|
|
expect(wrapper.text()).toContain('家居用品')
|
|
expect(wrapper.text()).toContain('预计托数')
|
|
expect(wrapper.text()).not.toContain('柜型')
|
|
expect(listLocalLooseboardOrders).toHaveBeenCalledOnce()
|
|
})
|
|
it('shows unknown pool vehicle type as a dash without inventing a capacity', async () => {
|
|
vi.mocked(listUsTruckOrders).mockResolvedValue({ records: [{ ...truck, vehicleType: null, fleetArrangement: arrangement }], total: 1, page: 1, size: 20, pages: 1 })
|
|
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
|
|
const vehicleColumn = wrapper.findAll('.el-table__header-wrapper th').findIndex(header => header.text() === '车辆类型')
|
|
expect(vehicleColumn).toBeGreaterThan(0)
|
|
expect(wrapper.find('.el-table__body-wrapper tbody .el-table__row').findAll('td')[vehicleColumn]!.text()).toBe('—')
|
|
expect(wrapper.find('.el-table__body-wrapper').text()).not.toContain('40HQ')
|
|
})
|
|
it('allows standalone demand editing without inventing pallet service or transport arrangements', async () => {
|
|
routing.route.params = { orderId: looseboard.orderNo }; routing.route.query = { customerId: looseboard.customerId }
|
|
vi.mocked(getLocalLooseboardOrder).mockResolvedValue({ ...looseboardDetail, palletRequired: null, transportType: null })
|
|
const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises()
|
|
expect(wrapper.text()).not.toContain('是否打盘')
|
|
expect(wrapper.text()).not.toContain('运输类型')
|
|
await wrapper.findAll('button').find(button => button.text().includes('保存修改'))!.trigger('click'); await flushPromises()
|
|
expect(updateLocalLooseboardOrder).toHaveBeenCalledWith(looseboard.orderNo, looseboard.customerId, expect.objectContaining({ productName: '家居用品', estimatedPalletCount: 2, pickupAt: '2026-10-20T09:30:00' }))
|
|
expect(updateLocalLooseboardOrder).toHaveBeenCalledOnce()
|
|
})
|
|
it('keeps all procurement facts in dense groups and business references in the heading', async () => {
|
|
routing.route.params = { purchaseId: 'internal-purchase-route' }; routing.route.query = { customerId: '10001' }
|
|
const wrapper = mountPage(PurchaseOrderDetailView)
|
|
await flushPromises()
|
|
expect(wrapper.get('.density-detail-title').text()).toBe('OP-100')
|
|
expect(wrapper.text()).toContain('洛杉矶采购仓')
|
|
expect(wrapper.findAll('.density-fields')).toHaveLength(2)
|
|
expect(wrapper.text()).toContain('预算单价')
|
|
expect(wrapper.text()).toContain('收货记录')
|
|
expect(wrapper.text()).toContain('出库单')
|
|
expect(wrapper.text()).toContain('采购库存')
|
|
expect(wrapper.text()).toContain('采购要求')
|
|
expect(wrapper.text()).toContain('采购备注')
|
|
expect(wrapper.text()).not.toContain('两段实际交寄')
|
|
expect(wrapper.text()).not.toContain('internal-purchase-route')
|
|
await wrapper.get('.density-back').trigger('click')
|
|
expect(routing.push).toHaveBeenCalledWith({ name: 'overseas-business-purchases' })
|
|
})
|
|
|
|
it('downloads procurement files with list access and prevents duplicate pending downloads', async () => {
|
|
routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }
|
|
const result = procurementDetail()
|
|
result.files = [{ id: '9007199254741000', fileId: '9007199254741001', category: 'LABEL', fileName: 'label.pdf', contentType: 'application/pdf', sizeBytes: 123, createdAt: '2026-10-10T10:00:00' }]
|
|
vi.mocked(getOverseasPurchase).mockResolvedValue(result)
|
|
vi.mocked(getProcurementWorkflow).mockResolvedValue({ orderNo: 'OP-100', version: 3, status: 'RECEIVING', receivedQuantity: 5, stockQuantity: 5, reservedQuantity: 0, availableQuantity: 5, rejectionReason: null, photosNeedUpdate: false, receipts: [{ id: '1', quantity: 5, fileIds: ['9007199254741001'], receivedAt: '2026-10-10T10:00:00' }], outbounds: [] })
|
|
let rejectDownload!: (error: Error) => void
|
|
vi.mocked(downloadProcurementFile).mockImplementation(() => new Promise((_resolve, reject) => { rejectDownload = reject }))
|
|
const wrapper = mountPage(PurchaseOrderDetailView)
|
|
useAuthStore().permissions = ['platform:overseas-business:purchase:list']
|
|
await flushPromises()
|
|
const button = wrapper.findAll('.el-table__body-wrapper button').find(item => item.text() === 'label.pdf')
|
|
expect(button).toBeDefined()
|
|
await button!.trigger('click'); await flushPromises()
|
|
expect(downloadProcurementFile).toHaveBeenCalledWith('OP-100', '10001', '9007199254741001')
|
|
expect(button!.attributes('disabled')).toBeDefined()
|
|
await button!.trigger('click')
|
|
expect(downloadProcurementFile).toHaveBeenCalledTimes(1)
|
|
rejectDownload(new Error('TEST download unavailable')); await flushPromises()
|
|
expect(button!.attributes('disabled')).toBeUndefined()
|
|
expect(document.body.textContent).toContain('文件下载失败')
|
|
})
|
|
|
|
it('uses the shared overseas fees panel for the exact procurement customer and document', async () => {
|
|
routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }
|
|
const wrapper = mountPage(PurchaseOrderDetailView); await flushPromises()
|
|
expect(wrapper.findComponent({ name: 'BusinessDocumentFees' }).props()).toEqual(expect.objectContaining({ businessType: 'OVERSEAS_PROCUREMENT', businessNo: 'OP-100', customerId: '10001', businessId: '9007199254740993' }))
|
|
expect(wrapper.text()).not.toContain('预估不入池')
|
|
})
|
|
|
|
it('mounts procurement list with customer candidates and real data', async () => { const wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(listOverseasPurchaseCustomerOptions).toHaveBeenCalled(); expect(listOverseasPurchases).toHaveBeenCalledWith({ page: 1, size: 20, customerId: '', orderNo: '', purchaseCategory: '', productName: '', status: '' }); expect(wrapper.text()).toContain('OP-100'); expect(wrapper.text()).toContain('收货中'); expect(wrapper.text()).toContain('2026-08-01 10:00') })
|
|
it.each([
|
|
{ status: 'REJECTED' as const, label: '已拒绝' },
|
|
{ status: 'CANCELLED' as const, label: '已取消' },
|
|
])('filters procurement orders by $status and resets to all statuses', async ({ status, label }) => {
|
|
vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [{ ...purchase, status }], page: 1, size: 20, total: 1, pages: 1 })
|
|
const wrapper = mountPage(PurchaseOrdersView)
|
|
await flushPromises()
|
|
const statusSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find(select => select.props('placeholder') === '全部状态')!
|
|
expect(statusSelect.findAllComponents({ name: 'ElOption' }).map(option => ({ value: option.props('value'), label: option.props('label') }))).toEqual(expect.arrayContaining([
|
|
{ value: 'REJECTED', label: '已拒绝' }, { value: 'CANCELLED', label: '已取消' },
|
|
]))
|
|
statusSelect.vm.$emit('update:modelValue', status)
|
|
await wrapper.vm.$nextTick()
|
|
await wrapper.findAll('button').find(button => button.text() === '查询')!.trigger('click')
|
|
await flushPromises()
|
|
expect(listOverseasPurchases).toHaveBeenLastCalledWith(expect.objectContaining({ status }))
|
|
expect(wrapper.find('.el-table__body .el-tag').text()).toBe(label)
|
|
await wrapper.findAll('button').find(button => button.text() === '重置')!.trigger('click')
|
|
await flushPromises()
|
|
expect(listOverseasPurchases).toHaveBeenLastCalledWith(expect.objectContaining({ status: '' }))
|
|
})
|
|
it('reviews pending procurement from the list, submits rejection as remark, and omits edit', async () => {
|
|
vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [{ ...purchase, status: 'PENDING_REVIEW' }], page: 1, size: 20, total: 1, pages: 1 })
|
|
const wrapper = mountPage(PurchaseOrdersView); await flushPromises()
|
|
expect(wrapper.text()).not.toContain('编辑')
|
|
await wrapper.findAll('button').find(button => button.text() === '审核')!.trigger('click'); await flushPromises()
|
|
const dialog = wrapper.findComponent(AppDialog), form = wrapper.findComponent({ name: 'ElForm' }).props('model') as { decision: string; remark: string }
|
|
form.decision = 'REJECT'; form.remark = '商品型号与要求不符'; dialog.vm.$emit('confirm'); await flushPromises()
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'review', expect.objectContaining({ version: 3, decision: 'REJECT', remark: '商品型号与要求不符' }))
|
|
expect(submitProcurementCommand).not.toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.anything(), expect.objectContaining({ reason: expect.anything() }))
|
|
})
|
|
it('keeps the current procurement warehouses when an earlier order resolves after the new order', async () => {
|
|
const { dialog, first, second, openSecond } = await mountPurchaseReviewRequests()
|
|
await openSecond()
|
|
second.resolve([{ warehouseId: 'W-B', warehouseCode: 'B', warehouseName: 'B 仓', countryCode: 'CN' }])
|
|
await flushPromises()
|
|
first.resolve([{ warehouseId: 'W-A', warehouseCode: 'A', warehouseName: 'A 仓', countryCode: 'US' }])
|
|
await flushPromises()
|
|
expect(dialog.findAllComponents({ name: 'ElOption' }).map(option => option.props('value'))).toEqual(['W-B'])
|
|
const form = dialog.findComponent({ name: 'ElForm' }).props('model') as { purchaseWarehouseId: string }
|
|
form.purchaseWarehouseId = 'W-B'
|
|
dialog.vm.$emit('confirm'); await flushPromises()
|
|
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-B', purchase.customerId, 'review', expect.objectContaining({ purchaseWarehouseId: 'W-B', version: purchase.version }))
|
|
})
|
|
it.each(['success', 'failure'])('ignores an old procurement warehouse %s while the current request is still loading', async outcome => {
|
|
const notification = vi.spyOn(ElMessage, 'error')
|
|
try {
|
|
const { dialog, first, second, openSecond } = await mountPurchaseReviewRequests()
|
|
await openSecond()
|
|
if (outcome === 'success') first.resolve([{ warehouseId: 'W-A', warehouseCode: 'A', warehouseName: 'A 仓', countryCode: 'US' }])
|
|
else first.reject(new Error('A 仓库查询失败'))
|
|
await flushPromises()
|
|
expect(dialog.props('confirmDisabled')).toBe(true)
|
|
expect(dialog.findComponent({ name: 'ElSelect' }).props('loading')).toBe(true)
|
|
expect(dialog.findAllComponents({ name: 'ElOption' })).toHaveLength(0)
|
|
expect(notification).not.toHaveBeenCalled()
|
|
second.resolve([{ warehouseId: 'W-B', warehouseCode: 'B', warehouseName: 'B 仓', countryCode: 'CN' }])
|
|
await flushPromises()
|
|
expect(dialog.props('confirmDisabled')).toBe(false)
|
|
expect(dialog.findAllComponents({ name: 'ElOption' }).map(option => option.props('value'))).toEqual(['W-B'])
|
|
} finally { notification.mockRestore() }
|
|
})
|
|
it.each(['close', 'unmount', 'account'])('drops late procurement warehouse failures after %s', async action => {
|
|
const notification = vi.spyOn(ElMessage, 'error')
|
|
try {
|
|
const { wrapper, dialog, first } = await mountPurchaseReviewRequests()
|
|
if (action === 'close') dialog.vm.$emit('update:modelValue', false)
|
|
else if (action === 'unmount') wrapper.unmount()
|
|
else { const auth = useAuthStore(); auth.user = { ...auth.user!, id: 'other-operator' } }
|
|
await flushPromises()
|
|
first.reject(new Error('已离开的订单查询失败'))
|
|
await flushPromises()
|
|
expect(notification).not.toHaveBeenCalled()
|
|
if (action !== 'unmount') {
|
|
expect(dialog.props('modelValue')).toBe(false)
|
|
expect(dialog.props('confirmDisabled')).toBe(false)
|
|
}
|
|
} finally { notification.mockRestore() }
|
|
})
|
|
it('still reports a current procurement warehouse failure and releases its loading state', async () => {
|
|
const notification = vi.spyOn(ElMessage, 'error')
|
|
try {
|
|
const { dialog, first } = await mountPurchaseReviewRequests()
|
|
first.reject(new Error('当前仓库查询失败'))
|
|
await flushPromises()
|
|
expect(notification).toHaveBeenCalledOnce()
|
|
expect(dialog.props('modelValue')).toBe(true)
|
|
expect(dialog.props('confirmDisabled')).toBe(false)
|
|
expect(dialog.findComponent({ name: 'ElSelect' }).props('loading')).toBe(false)
|
|
} finally { notification.mockRestore() }
|
|
})
|
|
it('keeps long procurement identifiers and descriptive fields out of single-line truncation', async () => {
|
|
const record = { ...purchase, orderNo: 'QLYSXDKH202610100002', customerName: '全量验收下单客户-20261010-长客户名称', productName: 'PROC-PURCHASE-20261010-长商品名称', productSpecification: '多规格描述-完整展示', purchaseCategory: 'OFFICE_SUPPLIES', purchaseWarehouseName: '洛杉矶海外采购收货仓库' }
|
|
vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [record], page: 1, size: 20, total: 1, pages: 1 })
|
|
const wrapper = mountPage(PurchaseOrdersView)
|
|
await flushPromises()
|
|
const cells = wrapper.findAll('.el-table__body-wrapper tbody .el-table__row')[0]!.findAll('td')
|
|
expect(cells[0]!.classes()).toContain('el-table-fixed-column--left')
|
|
const headers = wrapper.findAll('.el-table__header-wrapper th').map(header => header.text())
|
|
expect(headers.slice(0, 3)).toEqual(['操作', '订单编号', '状态'])
|
|
for (const [label, value] of [['订单编号', record.orderNo], ['客户名称', record.customerName], ['采购仓库', record.purchaseWarehouseName], ['采购品类', record.purchaseCategory], ['商品名称', record.productName], ['商品规格', record.productSpecification]]) {
|
|
const index = headers.indexOf(label!)
|
|
expect(index).toBeGreaterThan(0)
|
|
expect(cells[index]!.text()).toBe(value)
|
|
expect(cells[index]!.classes()).not.toContain('data-table__single-line-cell')
|
|
expect(cells[index]!.find('.cell').classes()).not.toContain('el-tooltip')
|
|
}
|
|
})
|
|
it('refreshes the cached procurement list after returning from detail or edit', () => { expect(purchaseListSource).toContain('onActivated'); expect(purchaseListSource).toMatch(/onActivated\(\(\) => \{ if \(activatedOnce\) void load\(\)/) })
|
|
it('refreshes the cached procurement detail after an edit completes', () => { expect(purchaseDetailSource).toContain('onActivated'); expect(purchaseDetailSource).toMatch(/onActivated\(\(\) => \{ if \(activatedOnce\) void load\(\)/) })
|
|
it('paginates procurement results', async () => { vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [purchase], page: 1, size: 20, total: 40, pages: 2 }); const wrapper = mountPage(PurchaseOrdersView); await flushPromises(); await wrapper.find('button[aria-label="第 2 页"]').trigger('click'); await flushPromises(); expect(listOverseasPurchases).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2 })) })
|
|
it('renders procurement empty and traceable 403 states', async () => { vi.mocked(listOverseasPurchases).mockResolvedValueOnce({ records: [], page: 1, size: 20, total: 0, pages: 0 }); let wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(wrapper.text()).toContain('暂无符合筛选条件的海外采购订单'); wrapper.unmount(); vi.mocked(listOverseasPurchases).mockRejectedValue(new ApiResponseError('forbidden', 'A0403', 403, null, 'REQ-403')); wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(wrapper.text()).toContain('当前账号无权访问该数据'); expect(wrapper.text()).toContain('REQ-403') })
|
|
it('mounts procurement detail operation history without exposing internal operator ids', async () => { routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }; const wrapper = mountPage(PurchaseOrderDetailView); await flushPromises(); expect(getOverseasPurchase).toHaveBeenCalledWith('OP-100', '10001'); expect(wrapper.text()).toContain('收货记录'); expect(wrapper.text()).toContain('出库单'); expect(wrapper.text()).not.toContain('INTERNAL-OPERATOR-7') })
|
|
|
|
it('submits only the internal remark and preserves input after a version conflict', async () => {
|
|
routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }
|
|
vi.mocked(updateOverseasPurchase).mockRejectedValue(new ApiResponseError('订单版本冲突,请刷新后重试', 'B0409', 409))
|
|
const wrapper = mountPage(PurchaseOrderEditView); await flushPromises()
|
|
await wrapper.find('textarea').setValue('新备注')
|
|
await wrapper.findAll('button').find((button) => button.text().trim() === '保存')!.trigger('click'); await flushPromises()
|
|
expect(updateOverseasPurchase).toHaveBeenCalledWith('OP-100', '10001', { version: 3, remark: '新备注' })
|
|
expect(document.body.textContent).toContain('订单版本冲突,请刷新后重试')
|
|
expect((wrapper.find('textarea').element as HTMLTextAreaElement).value).toBe('新备注')
|
|
expect(routing.push).not.toHaveBeenCalled()
|
|
})
|
|
it('keeps procurement list usable when customer candidates fail', async () => { vi.mocked(listOverseasPurchaseCustomerOptions).mockRejectedValue(new Error('candidate unavailable')); const wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(listOverseasPurchases).toHaveBeenCalled(); expect(wrapper.text()).toContain('OP-100'); expect(wrapper.text()).not.toContain('candidate unavailable') })
|
|
it('blocks saving until the aggregate context has loaded successfully', async () => {
|
|
routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }
|
|
vi.mocked(getOverseasPurchaseEditor).mockRejectedValue(new Error('unavailable'))
|
|
const wrapper = mountPage(PurchaseOrderEditView); await flushPromises()
|
|
expect(wrapper.text()).toContain('采购订单暂时无法编辑')
|
|
expect(wrapper.find('textarea').exists()).toBe(false)
|
|
expect(updateOverseasPurchase).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('sends only one remark mutation while a save is pending', async () => {
|
|
routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }
|
|
let complete!: (result: Awaited<ReturnType<typeof updateOverseasPurchase>>) => void
|
|
vi.mocked(updateOverseasPurchase).mockImplementation(() => new Promise(resolve => { complete = resolve }))
|
|
const wrapper = mountPage(PurchaseOrderEditView); await flushPromises()
|
|
const button = wrapper.findAll('button').find((item) => item.text().trim() === '保存')!
|
|
await button.trigger('click'); await button.trigger('click')
|
|
expect(updateOverseasPurchase).toHaveBeenCalledTimes(1)
|
|
complete({ ...purchase, version: 4 }); await flushPromises()
|
|
expect(routing.push).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('loads one aggregate editor request without warehouse queries and explicitly clears a remark', async () => {
|
|
routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }
|
|
const wrapper = mountPage(PurchaseOrderEditView); await flushPromises()
|
|
expect(getOverseasPurchaseEditor).toHaveBeenCalledTimes(1)
|
|
expect(listOverseasPurchaseWarehouseOptions).not.toHaveBeenCalled()
|
|
expect(wrapper.text()).toContain('OP-100')
|
|
expect(wrapper.text()).not.toContain('出库明细')
|
|
await wrapper.find('textarea').setValue('')
|
|
await wrapper.findAll('button').find((button) => button.text().trim() === '保存')!.trigger('click'); await flushPromises()
|
|
expect(updateOverseasPurchase).toHaveBeenCalledWith('OP-100', '10001', { version: 3, remark: null })
|
|
expect(routing.push).toHaveBeenCalledWith({ name: 'overseas-business-purchase-detail', params: { purchaseId: 'OP-100' }, query: { customerId: '10001' } })
|
|
})
|
|
it.each(['truck', 'looseboard'] as const)('requires an explicit customer and sends only the unified business payload for %s', async domain => {
|
|
const wrapper = mountPage(TransportOrderFormView, { domain }); await flushPromises()
|
|
const model = wrapper.findComponent({ name: 'ElForm' }).props('model')
|
|
expect(model.totalWeightKg).toBe(0); expect(model.totalVolumeCbm).toBe(0)
|
|
expect(model.budgetAmount).toBeUndefined(); expect(model.currency).toBe('USD')
|
|
const labels = wrapper.findAll('.el-form-item__label').map(item => item.text())
|
|
const routeLabels = domain === 'truck'
|
|
? ['B点取货地', 'C点送达地', 'B点联系人', 'C点联系人', 'B点联系电话', 'C点联系电话', 'B点取货地址', 'C点送达地址']
|
|
: ['起运地', '目的地', '起运地联系人', '目的地联系人', '起运地联系电话', '目的地联系电话', '起运详细地址', '目的详细地址']
|
|
expect(labels.slice(0, domain === 'truck' ? 10 : 9)).toEqual(['客户', ...(domain === 'truck' ? ['车辆类型'] : []), ...routeLabels])
|
|
Object.assign(model, { vehicleType: 'PALLET_15', estimatedPalletCount: domain === 'truck' ? 2 : 0, origin: ' LA ', destination: ' ONT ', originAddress: ' Pickup street ', destinationAddress: ' Delivery street ', originContactName: ' 发货人 ', originContactPhone: ' 1 ', destinationContactName: ' 收货人 ', destinationContactPhone: ' 2 ', pickupAt: '2026-10-20T09:30:45', deliveryAt: '2026-10-20T15:45:12', budgetAmount: domain === 'truck' ? 100 : 0, currency: 'CNY' })
|
|
const submit = wrapper.findAll('button').find(button => button.text() === '提交订单')!
|
|
await submit.trigger('click'); await flushPromises()
|
|
expect(createUsTruckOrder).not.toHaveBeenCalled(); expect(createLocalLooseboardOrder).not.toHaveBeenCalled()
|
|
model.customerId = '10001'
|
|
await submit.trigger('click'); await flushPromises()
|
|
const command = domain === 'truck' ? vi.mocked(createUsTruckOrder) : vi.mocked(createLocalLooseboardOrder)
|
|
expect(command).toHaveBeenCalledOnce()
|
|
const [customer, payload] = command.mock.calls[0]!
|
|
expect(customer).toBe('10001')
|
|
expect(payload).toEqual(expect.objectContaining({ origin: 'LA', destination: 'ONT', originContactName: '发货人', originContactPhone: '1', destinationContactName: '收货人', destinationContactPhone: '2', totalWeightKg: 0, totalVolumeCbm: 0, budgetAmount: domain === 'truck' ? 100 : 0, currency: 'CNY', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00' }))
|
|
if (domain === 'truck') { expect(payload).toMatchObject({ palletCount: 2 }); expect(payload).not.toHaveProperty('packageCount') }
|
|
for (const field of ['customerId', 'tenantId', 'orderNo', 'status', 'transportMode', 'containerType']) expect(payload).not.toHaveProperty(field)
|
|
})
|
|
|
|
it('mounts truck list with enabled create and supported types', async () => { const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises(); expect(listUsTruckOrders).toHaveBeenCalled(); expect(wrapper.text()).toContain('TK-200'); expect(wrapper.text()).toContain('15拖'); expect(wrapper.text()).toContain('2026-08-01 10:00'); expect(wrapper.findAll('button').find((button) => button.text().includes('新增订单'))?.attributes('disabled')).toBeUndefined() })
|
|
it('shows a truck detail entry instead of review and edit and keeps its customer scope', async () => {
|
|
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
|
|
const auth = useAuthStore(); auth.permissions.push('platform:overseas-business:truck:status')
|
|
await wrapper.vm.$nextTick()
|
|
const actions = wrapper.findAll('.el-table__body-wrapper button')
|
|
expect(actions.map(button => button.text())).toContain('详情')
|
|
expect(actions.map(button => button.text())).not.toContain('审核')
|
|
expect(actions.map(button => button.text())).not.toContain('编辑')
|
|
await actions.find(button => button.text() === '详情')!.trigger('click'); await flushPromises()
|
|
expect(routing.push).toHaveBeenCalledWith({ name: 'overseas-business-truck-detail', params: { orderId: 'TK-200' }, query: { customerId: '10002' } })
|
|
})
|
|
it('renders truck load failure without samples', async () => { vi.mocked(listUsTruckOrders).mockRejectedValue(new Error('hidden')); const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises(); expect(wrapper.text()).toContain('本土卡车订单加载失败'); expect(wrapper.text()).not.toContain('TK-200') })
|
|
it('loads and submits pending-review truck edit', async () => { routing.route.params = { orderId: 'TK-200' }; routing.route.query = { customerId: '10002' }; const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(getUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002'); expect(updateUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002', expect.objectContaining({ version: 1, vehicleType: 'PALLET_15', originContactName: 'Bob', destinationContactName: 'Alice', pickupAt: '2026-08-28T09:30:00' })) })
|
|
it('keeps truck edit form and version unchanged after a 409', async () => { routing.route.params = { orderId: 'TK-200' }; routing.route.query = { customerId: '10002' }; vi.mocked(updateUsTruckOrder).mockRejectedValue(new ApiResponseError('订单版本冲突', 'B0409', 409)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await destination.setValue('Ontario-MARKER-EDIT'); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(updateUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002', expect.objectContaining({ version: 1, destination: 'Ontario-MARKER-EDIT' })); expect((destination.element as HTMLInputElement).value).toBe('Ontario-MARKER-EDIT'); expect(document.body.textContent).toContain('订单版本冲突'); expect(routing.push).not.toHaveBeenCalled() })
|
|
it('mounts truck create with explicit customer selector and enabled submit', async () => { const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' }); expect(wrapper.text()).toContain('客户信息'); expect(wrapper.text()).toContain('请选择客户'); expect(wrapper.findAll('button').find((button) => button.text().includes('提交订单'))?.attributes('disabled')).toBeUndefined() })
|
|
it('reuses one idempotency key and preserves truck create input after retries fail', async () => { vi.mocked(createUsTruckOrder).mockRejectedValue(new ApiResponseError('temporary failure', 'S0500', 500)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0].vm.$emit('update:modelValue', '10001'); selects[1].vm.$emit('update:modelValue', 'PALLET_30'); const model = wrapper.findComponent({ name: 'ElForm' }).props('model'); Object.assign(model, { estimatedPalletCount: 2, budgetAmount: 100, originAddress: 'Pickup street', destinationAddress: 'Delivery street' }); const origin = wrapper.find('input[placeholder="城市 / 港口 / 州"]'); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await origin.setValue('LA-MARKER'); await destination.setValue('ONT-MARKER'); for (const label of ['B点联系人', 'C点联系人', 'B点联系电话', 'C点联系电话']) await wrapper.findAll('.el-form-item').find(item => item.text().startsWith(label))!.find('input').setValue(label.includes('电话') ? '+1-555-0100' : '联系人'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总重量'))!.find('input').setValue('100'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总体积'))!.find('input').setValue('2.5'); wrapper.findAllComponents({ name: 'ElDatePicker' })[0].vm.$emit('update:modelValue', '2026-08-31T09:30:00'); wrapper.findAllComponents({ name: 'ElDatePicker' })[1].vm.$emit('update:modelValue', '2026-08-31T15:45:00'); await wrapper.vm.$nextTick(); const submit = wrapper.findAll('button').find((button) => button.text().includes('提交订单')); await submit?.trigger('click'); await flushPromises(); await submit?.trigger('click'); await flushPromises(); expect(createUsTruckOrder).toHaveBeenCalledTimes(2); const keys = vi.mocked(createUsTruckOrder).mock.calls.map((call) => call[1].idempotencyKey); expect(keys[0]).toBeTruthy(); expect(keys[1]).toBe(keys[0]); expect((origin.element as HTMLInputElement).value).toBe('LA-MARKER'); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER'); expect(document.body.textContent).toContain('服务暂时不可用'); expect(routing.push).not.toHaveBeenCalled() })
|
|
it('loads the real looseboard list', async () => { const wrapper = mountPage(TransportOrdersView, { domain: 'looseboard' }); await flushPromises(); expect(listLocalLooseboardOrders).toHaveBeenCalledWith({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' }); expect(wrapper.text()).toContain('LB-300'); expect(wrapper.text()).toContain('待接单'); expect(wrapper.text()).toContain('2026-08-01 10:00'); expect(listUsTruckOrders).not.toHaveBeenCalled() })
|
|
it('loads and submits pending-acceptance looseboard edit with PUT adapter', async () => { routing.route.params = { orderId: 'LB-300' }; routing.route.query = { customerId: '10003' }; const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(getLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003'); expect(updateLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003', expect.objectContaining({ version: 0, estimatedPalletCount: 2 })) })
|
|
it('requires split contacts and both local times, rejecting reversed times', async () => {
|
|
const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises()
|
|
const model = wrapper.findComponent({ name: 'ElForm' }).props('model')
|
|
Object.assign(model, { customerId: '10001', originContactName: 'A', originContactPhone: '1', destinationContactName: 'B', destinationContactPhone: '2', origin: 'LA', destination: 'ONT', productName: '家居用品', estimatedPalletCount: 3, packageCount: 8, totalWeightKg: 80, totalVolumeCbm: 1.8, deliveryAt: '2026-10-20T11:30:00' })
|
|
await wrapper.vm.$nextTick()
|
|
const save = wrapper.findAll('button').find(button => button.text().includes('提交订单'))!
|
|
await save.trigger('click'); await flushPromises()
|
|
expect(createLocalLooseboardOrder).not.toHaveBeenCalled()
|
|
model.pickupAt = '2026-10-20T12:30:00'
|
|
await save.trigger('click'); await flushPromises()
|
|
expect(createLocalLooseboardOrder).not.toHaveBeenCalled()
|
|
model.deliveryAt = '2026-10-20T15:45:00'
|
|
await save.trigger('click'); await flushPromises()
|
|
expect(createLocalLooseboardOrder).toHaveBeenCalledWith('10001', expect.objectContaining({ productName: '家居用品', estimatedPalletCount: 3, packageCount: 8, pickupAt: '2026-10-20T12:30:00', deliveryAt: '2026-10-20T15:45:00' }))
|
|
const payload = vi.mocked(createLocalLooseboardOrder).mock.calls[0]![1]
|
|
expect(payload).not.toHaveProperty('containerType')
|
|
expect(payload).not.toHaveProperty('palletRequired')
|
|
expect(payload).not.toHaveProperty('transportType')
|
|
})
|
|
it('keeps looseboard edit form and version unchanged after a 409', async () => { routing.route.params = { orderId: 'LB-300' }; routing.route.query = { customerId: '10003' }; vi.mocked(updateLocalLooseboardOrder).mockRejectedValue(new ApiResponseError('订单版本冲突', 'B0409', 409)); const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await destination.setValue('ONT-MARKER-EDIT'); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(updateLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003', expect.objectContaining({ version: 0, destination: 'ONT-MARKER-EDIT' })); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER-EDIT'); expect(document.body.textContent).toContain('订单版本冲突'); expect(routing.push).not.toHaveBeenCalled() })
|
|
it('mounts looseboard create without a detail route or pending notice', async () => { const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); expect(wrapper.text()).toContain('客户信息'); expect(wrapper.text()).toContain('请选择客户'); expect(wrapper.text()).not.toContain('接口能力待后端补齐'); expect(wrapper.findAll('button').find((button) => button.text().includes('提交订单'))?.attributes('disabled')).toBeUndefined() })
|
|
it('reuses one idempotency key and preserves looseboard create input after retries fail', async () => { vi.mocked(createLocalLooseboardOrder).mockRejectedValue(new ApiResponseError('temporary failure', 'S0500', 500)); const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0].vm.$emit('update:modelValue', '10001'); for (const label of ['起运地联系人', '目的地联系人', '起运地联系电话', '目的地联系电话']) await wrapper.findAll('.el-form-item').find(item => item.text().startsWith(label))!.find('input').setValue(label.includes('电话') ? '1' : '联系人'); const dates = wrapper.findAllComponents({ name: 'ElDatePicker' }); dates[0].vm.$emit('update:modelValue', '2026-10-20T09:30:00'); dates[1].vm.$emit('update:modelValue', '2026-10-20T15:45:00'); const origin = wrapper.find('input[placeholder="城市 / 港口 / 州"]'); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await origin.setValue('LA-MARKER'); await destination.setValue('ONT-MARKER'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总重量'))!.find('input').setValue('80'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总体积'))!.find('input').setValue('1.8'); await wrapper.vm.$nextTick(); const submit = wrapper.findAll('button').find((button) => button.text().includes('提交订单')); await submit?.trigger('click'); await flushPromises(); await submit?.trigger('click'); await flushPromises(); expect(createLocalLooseboardOrder).toHaveBeenCalledTimes(2); const keys = vi.mocked(createLocalLooseboardOrder).mock.calls.map((call) => call[1].idempotencyKey); expect(keys[0]).toBeTruthy(); expect(keys[1]).toBe(keys[0]); expect((origin.element as HTMLInputElement).value).toBe('LA-MARKER'); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER'); expect(routing.push).not.toHaveBeenCalled() })
|
|
it.each([true, false])('opens only the matching pool order read-only arrangement: %s', matching => {
|
|
routing.route.query = { orderNo: truck.orderNo, fleetSource: 'inquiry', fleetOfferNo: 'FO-1', fleetBookingNo: matching ? arrangement.bookingNo : 'FB-OTHER' }
|
|
vi.mocked(listUsTruckOrders).mockResolvedValue({ records: [{ ...truck, fleetArrangement: arrangement }], page: 1, size: 20, total: 1, pages: 1 })
|
|
return (async () => {
|
|
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
|
|
const dialog = wrapper.findAllComponents(AppDialog).find(item => item.props('title') === '承运安排')!
|
|
expect(dialog.props('modelValue')).toBe(matching); expect(dialog.props('showConfirm')).toBe(false)
|
|
await wrapper.findAll('button').find(item => item.text() === '返回原拼车登记')!.trigger('click')
|
|
expect(routing.push).toHaveBeenCalledWith({ name: 'overseas-master-data-capacity-offer-detail', params: { offerNo: 'FO-1' }, query: { inquiry: matching ? arrangement.bookingNo : 'FB-OTHER' } })
|
|
expect(updateUsTruckOrder).not.toHaveBeenCalled()
|
|
})()
|
|
})
|
|
|
|
})
|
|
|