298 lines
38 KiB
TypeScript
298 lines
38 KiB
TypeScript
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
import ElementPlus 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 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/procurementCommands', async importOriginal => ({
|
|
...await importOriginal<typeof import('../api/procurementCommands')>(),
|
|
downloadProcurementFile: 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: 'WAIT_PACKING' 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', transportType: 'PORT_PICKUP', origin: 'Long Beach', destination: 'Ontario', containerType: '40HQ', palletCount: 2, packageCount: 20, receivedPalletCount: 0, allocatedPalletCount: 0, shippedPalletCount: 0, totalWeightKg: '1200.000', totalVolumeCbm: '16.500', budgetAmount: '850.00', currency: 'USD', contactName: 'Bob', contactInfo: '10010', requestedDate: '2026-08-28', 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', transportMode: 'SHARED_TRUCK', palletRequired: 'YES', transportType: 'WAREHOUSE_TO_WAREHOUSE', origin: 'LA', destination: 'ONT', containerType: '40HQ', packageCount: 8, totalWeightKg: '80.5', totalVolumeCbm: '3.2', budgetAmount: '50.00', currency: 'USD', contactName: null, contactInfo: null, requestedDate: null, 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] } }) }
|
|
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(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('distinguishes unknown pool pallet service from explicit no and yes in actual list cells', async () => {
|
|
const records = ([null, 'NO', 'YES'] as const).map((palletRequired, index) => ({ ...looseboard, id: String(100 + index), orderNo: `LB-NULL-${index}`, palletRequired, containerType: null, fleetArrangement: { ...arrangement, businessType: 'LOCAL_LOOSEBOARD' as const } }))
|
|
vi.mocked(listLocalLooseboardOrders).mockResolvedValue({ records, total: 3, page: 1, size: 20, pages: 1 })
|
|
const wrapper = mountPage(TransportOrdersView, { domain: 'looseboard' }); await flushPromises()
|
|
const rows = wrapper.findAll('.el-table__body-wrapper tbody .el-table__row')
|
|
expect(rows.map(row => row.findAll('td')[3]!.text())).toEqual(['—', '否', '是'])
|
|
expect(listLocalLooseboardOrders).toHaveBeenCalledOnce()
|
|
})
|
|
it('shows an unknown pool container type as a dash without inventing 40HQ', async () => {
|
|
vi.mocked(listUsTruckOrders).mockResolvedValue({ records: [{ ...truck, containerType: null, fleetArrangement: arrangement }], total: 1, page: 1, size: 20, pages: 1 })
|
|
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
|
|
expect(wrapper.find('.el-table__body-wrapper tbody .el-table__row').findAll('td')[7]!.text()).toBe('—')
|
|
expect(wrapper.find('.el-table__body-wrapper').text()).not.toContain('40HQ')
|
|
})
|
|
it('still rejects an ordinary looseboard edit when its required pallet service is absent', async () => {
|
|
routing.route.params = { orderId: looseboard.orderNo }; routing.route.query = { customerId: looseboard.customerId }
|
|
vi.mocked(getLocalLooseboardOrder).mockResolvedValue({ ...looseboardDetail, palletRequired: null, fleetArrangement: null })
|
|
const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises()
|
|
expect(wrapper.findComponent({ name: 'ElForm' }).props('model').palletRequired).toBeNull()
|
|
await wrapper.findAll('button').find(button => button.text().includes('保存修改'))!.trigger('click'); await flushPromises()
|
|
expect(updateLocalLooseboardOrder).not.toHaveBeenCalled()
|
|
expect(document.body.textContent).toContain('请选择是否打盘')
|
|
})
|
|
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.get('[aria-label="采购订单概览"]').text()).toContain('洛杉矶采购仓')
|
|
expect(wrapper.findAll('.density-fields')).toHaveLength(3)
|
|
expect(wrapper.text()).toContain('预算单价')
|
|
expect(wrapper.text()).toContain('两段实际交寄')
|
|
expect(wrapper.text()).not.toContain('库存信息')
|
|
expect(wrapper.findAll('.density-fields .wide')).toHaveLength(2)
|
|
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)
|
|
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() === '下载')
|
|
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('renders estimate holds separately from actual payment without claiming inbound settlement', async () => {
|
|
routing.route.params = { purchaseId: 'OP-100' }; routing.route.query = { customerId: '10001' }
|
|
const result = procurementDetail()
|
|
result.customerView.fees = [{ feeNo: 'FEE-1', purpose: 'INTERNATIONAL_ESTIMATE', name: '国际运费预估', version: 1, active: true, determination: 'POSITIVE', amount: '25.00', currency: 'CNY', originalAmount: '25.00', originalCurrency: 'CNY', exchangeRate: '1', exchangeRateMonth: '2026-10', businessDate: '2026-10-09', parentFeeNo: null, walletStatus: 'FROZEN', sourceLinked: false }]
|
|
vi.mocked(getOverseasPurchase).mockResolvedValue(result)
|
|
const wrapper = mountPage(PurchaseOrderDetailView); await flushPromises()
|
|
expect(wrapper.text()).toContain('已冻结,未扣款')
|
|
expect(wrapper.text()).toContain('预估不入池')
|
|
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('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')
|
|
for (const [index, value] of [record.orderNo, record.customerName, record.purchaseWarehouseName, record.purchaseCategory, record.productName, record.productSpecification].entries()) {
|
|
expect(cells[index + 1]!.text()).toBe(value)
|
|
expect(cells[index + 1]!.classes()).not.toContain('data-table__single-line-cell')
|
|
expect(cells[index + 1]!.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('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('港口提柜'); expect(wrapper.text()).toContain('2026-08-01 10:00'); expect(wrapper.findAll('button').find((button) => button.text().includes('新增订单'))?.attributes('disabled')).toBeUndefined() })
|
|
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, transportType: 'PORT_PICKUP', palletCount: 2 })) })
|
|
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'); wrapper.findComponent({ name: 'ElRadioGroup' }).vm.$emit('update:modelValue', 'PORT_PICKUP'); 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('联系人'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('联系方式'))!.find('input').setValue('13800000000'); 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.findComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-08-31'); 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, transportMode: 'SHARED_TRUCK', palletRequired: 'YES' })) })
|
|
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'); const groups = wrapper.findAllComponents({ name: 'ElRadioGroup' }); groups[0].vm.$emit('update:modelValue', 'SHARED_TRUCK'); groups[1].vm.$emit('update:modelValue', 'YES'); groups[2].vm.$emit('update:modelValue', 'WAREHOUSE_TO_WAREHOUSE'); 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()
|
|
})()
|
|
})
|
|
|
|
})
|