2026-10-10 02:19:39 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-20 18:02:52 +08:00
import ElementPlus from 'element-plus'
2026-09-14 10:02:39 +08:00
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
2026-08-20 18:02:52 +08:00
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'
2026-10-10 02:19:39 +08:00
import type { PlatformProcurement } from '../procurementTypes'
2026-10-10 08:11:13 +08:00
import { downloadProcurementFile } from '../api/procurementCommands'
2026-10-30 11:19:57 +08:00
import { submitProcurementCommand } from '../api/procurementCommands'
2026-10-27 17:39:19 +08:00
import { getProcurementWorkflow } from '../api/procurementWorkflow'
2026-10-07 16:46:57 +08:00
import type { FleetArrangement } from '../fleetArrangement'
2026-10-10 02:19:39 +08:00
import type { LocalLooseboardDetail , LocalLooseboardOrder , LocalTransportDetail , LocalTransportOrder } from '../types'
2026-08-20 18:02:52 +08:00
import {
2026-10-10 02:19:39 +08:00
createLocalLooseboardOrder , createUsTruckOrder , getLocalLooseboardOrder , getOverseasPurchase , getOverseasPurchaseEditor , getUsTruckOrder ,
2026-08-20 18:02:52 +08:00
listLocalLooseboardOrders , listOverseasPurchaseCustomerOptions , listOverseasPurchaseWarehouseOptions ,
2026-10-10 02:19:39 +08:00
listOverseasPurchases , listUsTruckOrders , updateLocalLooseboardOrder , updateOverseasPurchase , updateUsTruckOrder ,
2026-08-20 18:02:52 +08:00
} 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'
2026-09-14 10:02:39 +08:00
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' )
2026-08-20 18:02:52 +08:00
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 () }))
2026-10-27 17:39:19 +08:00
vi . mock ( '../api/procurementWorkflow' , () => ({ getProcurementWorkflow : vi.fn (), submitProcurementWorkflow : vi.fn () }))
2026-10-10 08:11:13 +08:00
vi . mock ( '../api/procurementCommands' , async importOriginal => ({
... await importOriginal < typeof import ( '../api/procurementCommands' ) >(),
2026-10-30 11:19:57 +08:00
downloadProcurementFile : vi.fn (), submitProcurementCommand : vi.fn (),
2026-10-10 08:11:13 +08:00
}))
2026-08-20 18:02:52 +08:00
vi . mock ( '../api/platformOverseasBusiness' , () => ({
2026-10-10 02:19:39 +08:00
listOverseasPurchases : vi.fn (), getOverseasPurchase : vi.fn (), getOverseasPurchaseEditor : vi.fn (), updateOverseasPurchase : vi.fn (), listOverseasPurchaseCustomerOptions : vi.fn (), listOverseasPurchaseWarehouseOptions : vi.fn (),
2026-08-20 18:02:52 +08:00
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 ) } }
2026-10-10 02:19:39 +08:00
const purchase = {
2026-08-20 18:02:52 +08:00
id : '9007199254740993' , orderNo : 'OP-100' , customerId : '10001' , customerName : '客户甲' , purchaseCategory : '家居' , productName : '置物架' , productSpecification : '黑色' , productUrl : 'https://example.test/product' ,
2026-10-27 17:39:19 +08:00
purchaseQuantity : 20 , budgetUnitPrice : '12.50' , currency : 'USD' , purchaseWarehouseId : '20001' , purchaseWarehouseName : '洛杉矶采购仓' , status : 'RECEIVING' as const , version : 3 ,
2026-10-10 02:19:39 +08:00
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 : [],
2026-08-20 18:02:52 +08:00
}
2026-10-10 02:19:39 +08:00
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 : '采购备注' ,
})
2026-10-19 14:38:47 +08:00
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' }
2026-08-20 18:02:52 +08:00
const truckDetail : LocalTransportDetail = { ... truck , originAddress : 'Pier A' , destinationAddress : 'Warehouse B' , remark : null , createdBy : '7' , pallets : [], histories : [] }
2026-10-19 15:11:15 +08:00
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' }
2026-08-20 18:02:52 +08:00
const looseboardDetail : LocalLooseboardDetail = { ... looseboard , originAddress : null , destinationAddress : null , remark : null , createdBy : '7' , histories : [] }
2026-10-07 16:46:57 +08:00
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 : '池来源订单按实际运输履约推进' }
2026-08-20 18:02:52 +08:00
2026-10-10 02:19:39 +08:00
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 }
2026-08-20 18:02:52 +08:00
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 ] } }) }
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 = {}
2026-10-10 08:11:13 +08:00
vi . mocked ( downloadProcurementFile ). mockReset ()
2026-10-30 11:19:57 +08:00
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 : [] })
2026-10-10 02:19:39 +08:00
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 })
2026-08-20 18:02:52 +08:00
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 })
})
2026-09-15 09:58:15 +08:00
2026-10-09 17:36:32 +08:00
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' : '' }])
})
2026-10-09 16:11:59 +08:00
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' )
})
2026-10-07 16:46:57 +08:00
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 ()
2026-10-07 18:21:03 +08:00
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 )
2026-10-07 16:46:57 +08:00
})
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 ()
})
2026-10-19 15:11:15 +08:00
it ( 'shows declared product and forecast pallets without exposing obsolete container selection' , async () => {
2026-10-07 19:44:42 +08:00
const wrapper = mountPage ( TransportOrdersView , { domain : 'looseboard' }); await flushPromises ()
2026-10-19 15:11:15 +08:00
expect ( wrapper . text ()). toContain ( '家居用品' )
expect ( wrapper . text ()). toContain ( '预计托数' )
expect ( wrapper . text ()). not . toContain ( '柜型' )
2026-10-07 19:44:42 +08:00
expect ( listLocalLooseboardOrders ). toHaveBeenCalledOnce ()
})
2026-10-19 14:38:47 +08:00
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 })
2026-10-07 19:44:42 +08:00
const wrapper = mountPage ( TransportOrdersView , { domain : 'truck' }); await flushPromises ()
2026-10-28 10:32:33 +08:00
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 ( '—' )
2026-10-07 19:44:42 +08:00
expect ( wrapper . find ( '.el-table__body-wrapper' ). text ()). not . toContain ( '40HQ' )
})
2026-10-19 15:11:15 +08:00
it ( 'allows standalone demand editing without inventing pallet service or transport arrangements' , async () => {
2026-10-07 19:44:42 +08:00
routing . route . params = { orderId : looseboard.orderNo }; routing . route . query = { customerId : looseboard.customerId }
2026-10-19 15:11:15 +08:00
vi . mocked ( getLocalLooseboardOrder ). mockResolvedValue ({ ... looseboardDetail , palletRequired : null , transportType : null })
2026-10-07 19:44:42 +08:00
const wrapper = mountPage ( TransportOrderFormView , { domain : 'looseboard' }); await flushPromises ()
2026-10-19 15:11:15 +08:00
expect ( wrapper . text ()). not . toContain ( '是否打盘' )
expect ( wrapper . text ()). not . toContain ( '运输类型' )
2026-10-07 19:44:42 +08:00
await wrapper . findAll ( 'button' ). find ( button => button . text (). includes ( '保存修改' )) ! . trigger ( 'click' ); await flushPromises ()
2026-10-19 15:11:15 +08:00
expect ( updateLocalLooseboardOrder ). toHaveBeenCalledWith ( looseboard . orderNo , looseboard . customerId , expect . objectContaining ({ productName : '家居用品' , estimatedPalletCount : 2 , pickupAt : '2026-10-20T09:30:00' }))
expect ( updateLocalLooseboardOrder ). toHaveBeenCalledOnce ()
2026-10-07 19:44:42 +08:00
})
2026-09-15 09:58:15 +08:00
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' )
2026-10-27 17:39:19 +08:00
expect ( wrapper . text ()). toContain ( '洛杉矶采购仓' )
expect ( wrapper . findAll ( '.density-fields' )). toHaveLength ( 2 )
2026-10-10 02:19:39 +08:00
expect ( wrapper . text ()). toContain ( '预算单价' )
2026-10-27 17:39:19 +08:00
expect ( wrapper . text ()). toContain ( '收货记录' )
expect ( wrapper . text ()). toContain ( '出库单' )
expect ( wrapper . text ()). toContain ( '采购库存' )
2026-10-30 11:19:57 +08:00
expect ( wrapper . text ()). toContain ( '采购要求' )
expect ( wrapper . text ()). toContain ( '采购备注' )
2026-10-27 17:39:19 +08:00
expect ( wrapper . text ()). not . toContain ( '两段实际交寄' )
2026-09-15 09:58:15 +08:00
expect ( wrapper . text ()). not . toContain ( 'internal-purchase-route' )
await wrapper . get ( '.density-back' ). trigger ( 'click' )
expect ( routing . push ). toHaveBeenCalledWith ({ name : 'overseas-business-purchases' })
})
2026-10-10 08:11:13 +08:00
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 )
2026-10-30 11:19:57 +08:00
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 : [] })
2026-10-10 08:11:13 +08:00
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 ()
2026-10-27 17:39:19 +08:00
const button = wrapper . findAll ( '.el-table__body-wrapper button' ). find ( item => item . text () === 'label.pdf' )
2026-10-10 08:11:13 +08:00
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 ( '文件下载失败' )
})
2026-10-27 17:39:19 +08:00
it ( 'uses the shared overseas fees panel for the exact procurement customer and document' , async () => {
2026-09-15 09:58:15 +08:00
routing . route . params = { purchaseId : 'OP-100' }; routing . route . query = { customerId : '10001' }
2026-10-10 02:19:39 +08:00
const wrapper = mountPage ( PurchaseOrderDetailView ); await flushPromises ()
2026-10-27 17:39:19 +08:00
expect ( wrapper . findComponent ({ name : 'BusinessDocumentFees' }). props ()). toEqual ( expect . objectContaining ({ businessType : 'OVERSEAS_PROCUREMENT' , businessNo : 'OP-100' , customerId : '10001' , businessId : '9007199254740993' }))
expect ( wrapper . text ()). not . toContain ( '预估不入池' )
2026-09-15 09:58:15 +08:00
})
2026-10-27 17:39:19 +08:00
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' ) })
2026-10-30 11:19:57 +08:00
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 () }))
})
2026-10-10 19:52:33 +08:00
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' )
2026-10-28 10:32:33 +08:00
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' )
2026-10-10 19:52:33 +08:00
}
})
2026-09-14 10:02:39 +08:00
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\(\)/ ) })
2026-08-20 18:02:52 +08:00
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' ) })
2026-10-27 17:39:19 +08:00
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' ) })
2026-10-10 02:19:39 +08:00
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 ()
})
2026-08-24 15:41:36 +08:00
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' ) })
2026-10-10 02:19:39 +08:00
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' } })
})
2026-10-19 15:50:39 +08:00
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 ())
2026-10-27 17:39:19 +08:00
const routeLabels = domain === 'truck'
? [ 'B点取货地' , 'C点送达地' , 'B点联系人' , 'C点联系人' , 'B点联系电话' , 'C点联系电话' , 'B点取货地址' , 'C点送达地址' ]
: [ '起运地' , '目的地' , '起运地联系人' , '目的地联系人' , '起运地联系电话' , '目的地联系电话' , '起运详细地址' , '目的详细地址' ]
expect ( labels . slice ( 0 , domain === 'truck' ? 10 : 9 )). toEqual ([ '客户' , ...( domain === 'truck' ? [ '车辆类型' ] : []), ... routeLabels ])
2026-10-19 15:50:39 +08:00
Object . assign ( model , { vehicleType : 'PALLET_15' , origin : ' LA ' , destination : ' ONT ' , originContactName : ' 发货人 ' , originContactPhone : ' 1 ' , destinationContactName : ' 收货人 ' , destinationContactPhone : ' 2 ' , pickupAt : '2026-10-20T09:30:45' , deliveryAt : '2026-10-20T15:45:12' , budgetAmount : 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 : 0 , currency : 'CNY' , pickupAt : '2026-10-20T09:30:00' , deliveryAt : '2026-10-20T15:45:00' }))
for ( const field of [ 'customerId' , 'tenantId' , 'orderNo' , 'status' , 'transportMode' , 'containerType' ]) expect ( payload ). not . toHaveProperty ( field )
})
2026-10-19 14:38:47 +08:00
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 () })
2026-10-20 11:54:36 +08:00
it ( 'opens the requested truck review from the list using 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 review = wrapper . findAll ( 'button' ). find ( button => button . text () === '审核' )
expect ( review ). toBeDefined (); await review ! . trigger ( 'click' ); await flushPromises ()
expect ( routing . push ). toHaveBeenCalledWith ({ name : 'overseas-business-truck-detail' , params : { orderId : 'TK-200' }, query : { customerId : '10002' } })
})
2026-08-20 18:02:52 +08:00
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' ) })
2026-10-19 14:38:47 +08:00
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' })) })
2026-08-24 15:41:36 +08: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 () })
2026-08-20 18:02:52 +08:00
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 () })
2026-10-27 17:39:19 +08:00
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 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 () })
2026-10-09 16:11:59 +08:00
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 () })
2026-10-19 15:11:15 +08:00
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' )
2026-10-19 15:50:39 +08:00
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' })
2026-10-19 15:11:15 +08: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' )
})
2026-10-19 15:50:39 +08:00
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 () })
2026-08-20 18:02:52 +08:00
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 () })
2026-10-19 15:50:39 +08:00
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 () })
2026-10-11 15:33:09 +08:00
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 ()
})()
})
2026-08-20 18:02:52 +08:00
})