Files

396 lines
30 KiB
TypeScript
Raw Permalink Normal View History

import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios'
import { mkdirSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { createPinia, setActivePinia } from 'pinia'
import { createMemoryHistory, createRouter } from 'vue-router'
import { defineComponent, nextTick } from 'vue'
import { mount, type VueWrapper } from '@vue/test-utils'
import ElementPlus from 'element-plus'
import { http, ApiResponseError } from '@/shared/api/http'
2026-09-29 18:47:41 +08:00
import { refreshAuthenticatedSession } from '@/shared/api/http'
import { useAuthStore } from '@/core/auth/auth'
import { listPlatformTenants } from '@/modules/iam/api/platformIam'
import {
changeExchangeRateStatus, changeQuoteStatus, changeVehicleStatus, createExchangeRate, createQuote, createVehicle,
exportExchangeRates, exportQuotes, exportVehicles, getExchangeRate, getQuote, getVehicle, listExchangeRates,
listQuoteWarehouseOptions, listQuotes, listVehicles, updateExchangeRate, updateQuote, updateVehicle,
} from '@/modules/overseas-master-data/api/overseasMasterData'
import {
createLocalLooseboardOrder, createUsTruckOrder, getLocalLooseboardOrder, getUsTruckOrder,
listLocalLooseboardOrders, listOverseasPurchaseCustomerOptions, listOverseasPurchases,
listOverseasPurchaseWarehouseOptions, listUsTruckOrders, updateLocalLooseboardOrder, updateUsTruckOrder,
} from '@/modules/overseas-business/api/platformOverseasBusiness'
import { listLastMileOrders, getLastMileOrder } from '@/modules/overseas-warehouse/api/lastMileOrders'
import { listInboundOrders } from '@/modules/overseas-warehouse/api/inboundOrders'
import { getInboundPlan } from '@/modules/overseas-warehouse/api/inboundPlans'
import { listDispatchCandidates, listDispatchDrafts, listDispatches } from '@/modules/overseas-warehouse/api/dispatches'
import { getInventorySummary, listInventoryBalances, listInventoryLedger, listInventoryPositions } from '@/modules/overseas-warehouse/api/inventory'
import { createPallet, getPallet, listCustomerWarehouses, listPallets, updatePallet } from '@/modules/overseas-warehouse/api/pallets'
import { listRelabelOrders } from '@/modules/overseas-warehouse/api/relabelOrders'
import { getCarrierScheduleQuery, listCarrierScheduleQueries } from '@/modules/port-schedules/api/platformCarrierScheduleQueries'
import OverseasQuotesView from '@/modules/overseas-master-data/pages/OverseasQuotesView.vue'
import PurchaseOrdersView from '@/modules/overseas-business/pages/PurchaseOrdersView.vue'
import TransportOrdersView from '@/modules/overseas-business/pages/TransportOrdersView.vue'
import LastMileOrdersView from '@/modules/overseas-warehouse/pages/LastMileOrdersView.vue'
import InboundOrdersView from '@/modules/overseas-warehouse/pages/InboundOrdersView.vue'
import InventoryView from '@/modules/overseas-warehouse/pages/InventoryView.vue'
import DispatchesView from '@/modules/overseas-warehouse/pages/DispatchesView.vue'
import RelabelOrdersView from '@/modules/overseas-warehouse/pages/RelabelOrdersView.vue'
import OutboundsView from '@/modules/overseas-warehouse/pages/OutboundsView.vue'
import DeliveryOrdersView from '@/modules/overseas-warehouse/pages/DeliveryOrdersView.vue'
import ExceptionOrdersView from '@/modules/overseas-warehouse/pages/ExceptionOrdersView.vue'
import CustomerVisitsView from '@/modules/overseas-warehouse/pages/CustomerVisitsView.vue'
import InventoryTransactionsView from '@/modules/overseas-warehouse/pages/InventoryTransactionsView.vue'
import PalletsView from '@/modules/overseas-warehouse/pages/PalletsView.vue'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
2026-09-29 18:47:41 +08:00
type Region = 'unified'
interface TraceStep { region: Region; method: string; path: string; params: unknown; body: unknown; httpStatus: number; code?: string; requestId?: string; traceId?: string; authorization: boolean }
interface CoverageGap { area: string; reason: string }
const enabled = process.env.ADMIN_PHASE4_LIVE === '1'
const marker = `ADMIN-FRONT-P4-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}`
const traces: TraceStep[] = []
const coverageGaps: CoverageGap[] = []
const createdResources: Record<string, unknown> = {}
const wrappers: VueWrapper[] = []
function redact(value: unknown): unknown {
if (Array.isArray(value)) return value.map(redact)
if (!value || typeof value !== 'object') return value
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([key, item]) => [key, /password|token|authorization/i.test(key) ? '[REDACTED]' : redact(item)]))
}
function parsed(value: unknown) {
if (typeof value !== 'string') return value
try { return JSON.parse(value) } catch { return value }
}
function envelope(value: unknown) {
const body = parsed(value) as { code?: unknown; requestId?: unknown; traceId?: unknown } | null
return body && typeof body === 'object' ? body : null
}
function installTracingAdapter(client: AxiosInstance, region: Region) {
const nodeAdapter = axios.getAdapter('http')
client.defaults.adapter = async (config: InternalAxiosRequestConfig) => {
try {
const response = await nodeAdapter(config)
const body = envelope(response.data)
traces.push({ region, method: String(config.method || 'GET').toUpperCase(), path: String(config.url || ''), params: redact(config.params), body: redact(parsed(config.data)), httpStatus: response.status, code: body?.code == null ? undefined : String(body.code), requestId: body?.requestId == null ? undefined : String(body.requestId), traceId: body?.traceId == null ? undefined : String(body.traceId), authorization: Boolean(config.headers?.Authorization) })
return response
} catch (error) {
if (axios.isAxiosError(error)) {
const body = envelope(error.response?.data)
traces.push({ region, method: String(config.method || 'GET').toUpperCase(), path: String(config.url || ''), params: redact(config.params), body: redact(parsed(config.data)), httpStatus: error.response?.status || 0, code: body?.code == null ? undefined : String(body.code), requestId: body?.requestId == null ? undefined : String(body.requestId), traceId: body?.traceId == null ? undefined : String(body.traceId), authorization: Boolean(config.headers?.Authorization) })
}
throw error
}
}
}
function gap(area: string, reason: string) { coverageGaps.push({ area, reason }) }
function numericId(value: unknown) { return /^[1-9]\d*$/.test(String(value ?? '')) }
function expectPage(value: { records: unknown[]; page: number; size: number; total: number; pages: number }) {
expect(Array.isArray(value.records)).toBe(true); expect(value.page).toBeGreaterThanOrEqual(1); expect(value.size).toBeGreaterThan(0); expect(value.total).toBeGreaterThanOrEqual(value.records.length); expect(value.pages).toBeGreaterThanOrEqual(0)
}
async function expectConflict(action: () => Promise<unknown>) {
await expect(action()).rejects.toSatisfy((error: unknown) => {
if (error instanceof ApiResponseError) return error.code.endsWith('409')
if (!axios.isAxiosError(error)) return false
const response = parsed(error.response?.data) as { code?: unknown } | undefined
return error.response?.status === 409 || String(response?.code || '').endsWith('409')
})
}
function futureDate(days: number) { return new Date(Date.now() + days * 86_400_000).toISOString().slice(0, 10) }
async function masterDataLifecycle() {
const options = await listQuoteWarehouseOptions()
const quotePage = await listQuotes({ page: 1, size: 20 })
expectPage(quotePage)
if (options.length) {
const quote = await createQuote({ name: `${marker} 报价`, warehouseId: options[0].warehouseId, providerName: `${marker} Provider`, currency: 'USD', effectiveStart: '2098-01-01', effectiveEnd: '2098-12-31', versionCode: marker.slice(-32), warehouseFees: [{ item: 'Storage QA', billingUnit: 'PALLET', unitPrice: 1.25, minimumCharge: 2.5, remark: marker }], transitPrices: [], remark: marker })
const id = quote.summary.id
createdResources.quote = id
let detail = await getQuote(id)
const stale = detail.summary.recordVersion
detail = await updateQuote(id, { recordVersion: stale, name: `${marker} 报价-编辑`, providerName: detail.summary.providerName, currency: detail.summary.currency, effectiveStart: detail.summary.effectiveStart, effectiveEnd: detail.summary.effectiveEnd || '2098-12-31', warehouseFees: detail.warehouseFees, transitPrices: detail.transitPrices, remark: `${marker} edited` })
await expectConflict(() => updateQuote(id, { recordVersion: stale, name: 'STALE', providerName: detail.summary.providerName, currency: detail.summary.currency, effectiveStart: detail.summary.effectiveStart, effectiveEnd: detail.summary.effectiveEnd || '2098-12-31', warehouseFees: detail.warehouseFees, transitPrices: detail.transitPrices, remark: marker }))
detail = await changeQuoteStatus(id, 'ACTIVE', detail.summary.recordVersion)
expect(detail.summary.status).toBe('ACTIVE')
expect((await exportQuotes({ keyword: marker })).size).toBeGreaterThan(0)
detail = await changeQuoteStatus(id, 'INACTIVE', detail.summary.recordVersion)
expect(detail.summary.status).toBe('INACTIVE')
} else gap('海外报价', 'warehouse-options 为空,未猜测仓库 ID,跳过报价写生命周期')
let exchangeMonth = ''
for (let index = 0; index < 24 && !exchangeMonth; index += 1) {
const month = `${2097 + Math.floor(index / 12)}-${String(index % 12 + 1).padStart(2, '0')}`
const page = await listExchangeRates({ page: 1, size: 20, monthFrom: month, monthTo: month })
if (!page.total) exchangeMonth = month
}
if (!exchangeMonth) throw new Error('安全汇率月份池已占满')
let rate = await createExchangeRate({ month: exchangeMonth, usdToCnyAverage: 7.123456, remark: marker })
createdResources.exchangeRate = exchangeMonth
const staleRate = rate.recordVersion
rate = await updateExchangeRate(exchangeMonth, { recordVersion: staleRate, usdToCnyAverage: 7.234567, remark: `${marker} edited` })
await expectConflict(() => updateExchangeRate(exchangeMonth, { recordVersion: staleRate, usdToCnyAverage: 7.9, remark: 'STALE' }))
rate = await getExchangeRate(exchangeMonth)
rate = await changeExchangeRateStatus(exchangeMonth, 'INACTIVE', rate.recordVersion, `${marker} cleanup`)
expect(rate.status).toBe('INACTIVE')
expect((await exportExchangeRates({ monthFrom: exchangeMonth, monthTo: exchangeMonth })).size).toBeGreaterThan(0)
let vehicle = await createVehicle({ plateNumber: `QA-${marker.slice(-18)}`, model: 'Phase4 Front QA', fuelConsumptionPer100Km: 18.5, mileageKm: 100, driverOrDispatchCompany: marker, phone: '+1-213-555-0188', region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', remark: marker })
createdResources.vehicle = vehicle.id
const staleVehicle = vehicle.recordVersion
vehicle = await updateVehicle(vehicle.id, { recordVersion: staleVehicle, plateNumber: vehicle.plateNumber, model: 'Phase4 Front QA Edited', fuelConsumptionPer100Km: 18.75, mileageKm: 101, driverOrDispatchCompany: marker, phone: vehicle.phone, region: vehicle.region, ownership: vehicle.ownership, remark: `${marker} edited` })
await expectConflict(() => updateVehicle(vehicle.id, { recordVersion: staleVehicle, plateNumber: vehicle.plateNumber, model: 'STALE', fuelConsumptionPer100Km: 19, mileageKm: 102, driverOrDispatchCompany: marker, phone: vehicle.phone, region: vehicle.region, ownership: vehicle.ownership, remark: 'STALE' }))
vehicle = await getVehicle(vehicle.id)
vehicle = await changeVehicleStatus(vehicle.id, 'INACTIVE', vehicle.recordVersion)
expect(vehicle.status).toBe('INACTIVE')
expect((await exportVehicles({ keyword: vehicle.plateNumber })).size).toBeGreaterThan(0)
expectPage(await listVehicles({ page: 1, size: 20, keyword: marker }))
}
async function cleanupTransport(path: string, customerId: string, version: number) {
2026-09-29 18:47:41 +08:00
await http.patch(`${path}/status`, { version, status: 'CANCELLED', remark: `${marker} cleanup` }, { params: { customerId } })
}
async function overseasBusinessLifecycle() {
const customers = (await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })).records.filter(item => item.status === 'ACTIVE')
const active = customers.find(item => numericId(item.id))
if (!active) { gap('卡车/散板', 'IAM 无 ACTIVE 客户'); return }
const customerId = String(active.id)
expect(Array.isArray(await listOverseasPurchaseCustomerOptions())).toBe(true)
expect(Array.isArray(await listOverseasPurchaseWarehouseOptions())).toBe(true)
expectPage(await listOverseasPurchases({ page: 1, size: 20 }))
const common = { origin: `${marker}-ORIGIN`, originAddress: 'Long Beach', destination: `${marker}-DEST`, destinationAddress: 'Ontario', containerType: '40HQ', packageCount: 2, totalWeightKg: 100, totalVolumeCbm: 2.5, budgetAmount: 88, currency: 'USD', contactName: '四期前端验收', contactInfo: '13800000000', requestedDate: futureDate(7), remark: marker }
let truck = await createUsTruckOrder(customerId, { ...common, transportType: 'PORT_PICKUP', palletCount: 1, idempotencyKey: `${marker}-truck` })
createdResources.truck = truck.orderNo
try {
truck = await getUsTruckOrder(truck.orderNo, customerId)
const stale = truck.version
truck = await updateUsTruckOrder(truck.orderNo, customerId, { ...common, transportType: 'PORT_PICKUP', palletCount: 1, destination: `${marker}-DEST-EDIT`, version: stale })
await expectConflict(() => updateUsTruckOrder(truck.orderNo, customerId, { ...common, transportType: 'PORT_PICKUP', palletCount: 1, destination: 'STALE', version: stale }))
} finally {
truck = await getUsTruckOrder(truck.orderNo, customerId)
if (truck.status === 'PENDING_REVIEW') await cleanupTransport(`/platform/v1/last-leg/us-truck-orders/${encodeURIComponent(truck.orderNo)}`, customerId, truck.version)
}
expectPage(await listUsTruckOrders({ page: 1, size: 20, customerId }))
let loose = await createLocalLooseboardOrder(customerId, { transportMode: 'SHARED_TRUCK', palletRequired: 'YES', transportType: 'WAREHOUSE_TO_WAREHOUSE', ...common, idempotencyKey: `${marker}-looseboard` })
createdResources.looseboard = loose.orderNo
try {
loose = await getLocalLooseboardOrder(loose.orderNo, customerId)
const stale = loose.version
loose = await updateLocalLooseboardOrder(loose.orderNo, customerId, { transportMode: loose.transportMode, palletRequired: loose.palletRequired, transportType: loose.transportType, ...common, destination: `${marker}-DEST-EDIT`, version: stale })
await expectConflict(() => updateLocalLooseboardOrder(loose.orderNo, customerId, { transportMode: loose.transportMode, palletRequired: loose.palletRequired, transportType: loose.transportType, ...common, destination: 'STALE', version: stale }))
} finally {
loose = await getLocalLooseboardOrder(loose.orderNo, customerId)
if (loose.status === 'PENDING_ACCEPTANCE') await cleanupTransport(`/platform/v1/last-leg/local-looseboard-orders/${encodeURIComponent(loose.orderNo)}`, customerId, loose.version)
}
expectPage(await listLocalLooseboardOrders({ page: 1, size: 20, customerId }))
}
async function overseasWarehouseCoverage() {
const lastMile = await listLastMileOrders({ page: 1, size: 20 }); expectPage(lastMile)
if (lastMile.records[0]) await getLastMileOrder(String(lastMile.records[0].id))
const inbound = await listInboundOrders({ page: 1, size: 20 }); expectPage(inbound)
if (inbound.records[0]) await getInboundPlan(inbound.records[0].planNo, inbound.records[0].customerId)
expectPage(await listDispatches({ page: 1, size: 20 }))
expect(Array.isArray(await listDispatchCandidates({}))).toBe(true)
expect(Array.isArray(await listDispatchDrafts())).toBe(true)
const inventoryQuery = { page: 1, size: 20 }
const balances = await listInventoryBalances(inventoryQuery); expectPage(balances)
await getInventorySummary(inventoryQuery)
await listInventoryLedger({ page: 1, size: 20 })
if (balances.records[0]) await listInventoryPositions(String(balances.records[0].id), 1, 20)
expectPage(await listRelabelOrders({ page: 1, size: 20 }))
const warehouses = (await listCustomerWarehouses()).records.filter(item => item.status === 'ACTIVE')
if (!warehouses.length) { gap('公共托盘', '实际海外仓为空,跳过前端托盘写生命周期'); return }
const warehouse = warehouses[0]
const payload = { palletCode: `QA-${marker.slice(-20)}`, palletType: 'STANDARD', fbaWarehouseCode: 'ONT8', capacityCbm: 2.5, unitPrice: 10, status: 'IDLE', remark: marker }
const palletId = await createPallet(warehouse.id, payload)
createdResources.pallet = palletId
try {
const pallet = await getPallet(warehouse.id, palletId)
await updatePallet(warehouse.id, palletId, { ...payload, capacityCbm: 2.75, remark: `${marker} edited`, version: pallet.version })
await expectConflict(() => updatePallet(warehouse.id, palletId, { ...payload, capacityCbm: 3, remark: 'STALE', version: pallet.version }))
expectPage(await listPallets(warehouse.id, { page: 1, size: 20, keyword: payload.palletCode }))
}
finally {
const current = await getPallet(warehouse.id, palletId)
if (current.status !== 'SCRAPPED') await updatePallet(warehouse.id, palletId, { ...payload, status: 'SCRAPPED', remark: `${marker} cleanup`, version: current.version })
}
gap('FBA预约/智能预排/换标新增', '没有本轮 marker 入仓计划和托盘候选;为避免操作历史数据,本轮只自动覆盖列表、详情、候选和草稿查询')
}
async function carrierCoverage() {
const page = await listCarrierScheduleQueries({ page: 1, size: 20 })
expectPage(page)
if (page.records[0]) await getCarrierScheduleQuery(page.records[0].query.id)
else gap('船节点人工补录', '环境无真实查询任务;未创建或修改历史任务')
}
async function waitForTrace(method: string, path: string, start: number) {
const deadline = Date.now() + 20_000
while (Date.now() < deadline) {
if (traces.slice(start).some(item => item.method === method && item.path.startsWith(path))) return
await new Promise(resolve => setTimeout(resolve, 50))
}
throw new Error(`页面未发出 ${method} ${path}`)
}
async function mountPage(component: Parameters<typeof mount>[0], method: string, path: string, props?: Record<string, unknown>) {
const start = traces.length
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/:pathMatch(.*)*', component: defineComponent({ template: '<div />' }) }] })
await router.push('/'); await router.isReady()
const wrapper = mount(component, { attachTo: document.body, props, global: { plugins: [ElementPlus, router, setActivePinia(createPinia())] } })
wrappers.push(wrapper)
await waitForTrace(method, path, start)
await nextTick()
expect(wrapper.text()).not.toContain('服务暂时不可用')
return wrapper
}
async function exerciseListSearch(
wrapper: VueWrapper,
path: string,
model: Record<string, unknown>,
expectedParams: Record<string, unknown>,
) {
const toolbar = wrapper.findComponent(QueryToolbar)
expect(toolbar.exists()).toBe(true)
toolbar.vm.$emit('update:modelValue', { ...model })
await nextTick()
let start = traces.length
toolbar.vm.$emit('search', { ...model })
await waitForTrace('GET', path, start)
const searchRequests = traces.slice(start).filter(item => item.method === 'GET' && item.path === path)
expect(searchRequests).toHaveLength(1)
expect(searchRequests[0]?.httpStatus).toBe(200)
expect(searchRequests[0]?.params).toMatchObject(expectedParams)
expect(wrapper.text()).not.toContain('加载失败')
start = traces.length
;(toolbar.vm as unknown as { reset(): void }).reset()
await waitForTrace('GET', path, start)
const resetRequests = traces.slice(start).filter(item => item.method === 'GET' && item.path === path)
expect(resetRequests).toHaveLength(1)
expect(resetRequests[0]?.httpStatus).toBe(200)
const resetParams = resetRequests[0]?.params as Record<string, unknown> | undefined
for (const key of Object.keys(expectedParams)) expect(resetParams?.[key] == null || resetParams[key] === '').toBe(true)
}
async function pageRequestCoverage() {
const quotes = await mountPage(OverseasQuotesView, 'GET', '/platform/v1/last-leg/master-data/overseas-quotes')
await exerciseListSearch(quotes, '/platform/v1/last-leg/master-data/overseas-quotes',
{ keyword: `${marker}-NO-MATCH`, warehouseId: '', providerName: `${marker}-PROVIDER`, currency: 'USD', status: 'INACTIVE' },
{ keyword: `${marker}-NO-MATCH`, providerName: `${marker}-PROVIDER`, currency: 'USD', status: 'INACTIVE' })
const purchases = await mountPage(PurchaseOrdersView, 'GET', '/platform/v1/last-leg/overseas-procurements')
await exerciseListSearch(purchases, '/platform/v1/last-leg/overseas-procurements',
{ orderNo: `${marker}-NO-MATCH`, customerId: '', purchaseCategory: `${marker}-CATEGORY`, productName: `${marker}-PRODUCT` },
{ orderNo: `${marker}-NO-MATCH`, purchaseCategory: `${marker}-CATEGORY`, productName: `${marker}-PRODUCT` })
const trucks = await mountPage(TransportOrdersView, 'GET', '/platform/v1/last-leg/us-truck-orders', { domain: 'truck' })
await exerciseListSearch(trucks, '/platform/v1/last-leg/us-truck-orders',
{ status: 'CANCELLED', routeKeyword: `${marker}-NO-MATCH` },
{ status: 'CANCELLED', routeKeyword: `${marker}-NO-MATCH` })
const looseboards = await mountPage(TransportOrdersView, 'GET', '/platform/v1/last-leg/local-looseboard-orders', { domain: 'looseboard' })
await exerciseListSearch(looseboards, '/platform/v1/last-leg/local-looseboard-orders',
{ status: 'CANCELLED', routeKeyword: `${marker}-NO-MATCH` },
{ status: 'CANCELLED', routeKeyword: `${marker}-NO-MATCH` })
const lastMile = await mountPage(LastMileOrdersView, 'GET', '/platform/v1/last-leg/orders')
await exerciseListSearch(lastMile, '/platform/v1/last-leg/orders',
{ orderNo: `${marker}-ORDER`, inboundCode: `${marker}-INBOUND`, lxCode: `${marker}-LX`, orderType: 'FIRST_LEG', warehouse: `${marker}-WAREHOUSE`, sourceOrderNo: `${marker}-SOURCE`, containerNo: `${marker}-CONTAINER`, transferWarehouseCode: `${marker}-TRANSFER`, status: 'DELIVERED', abnormal: 'true', detained: 'false', createdAt: ['2098-01-01', '2098-01-02'] },
{ orderNo: `${marker}-ORDER`, inboundCode: `${marker}-INBOUND`, lxCode: `${marker}-LX`, orderType: 'FIRST_LEG', warehouse: `${marker}-WAREHOUSE`, sourceOrderNo: `${marker}-SOURCE`, containerNo: `${marker}-CONTAINER`, transferWarehouseCode: `${marker}-TRANSFER`, currentStatus: 'DELIVERED', abnormal: true, detained: false, createdFrom: '2098-01-01T00:00:00', createdTo: '2098-01-02T23:59:59' })
let start = traces.length
await lastMile.findAll('.el-radio-button').find(item => item.text() === '待分配')!.trigger('click')
await waitForTrace('GET', '/platform/v1/last-leg/orders', start)
start = traces.length
await lastMile.findAll('.el-radio-button').find(item => item.text().startsWith('柜子'))!.trigger('click')
await waitForTrace('GET', '/platform/v1/last-leg/orders/container-summaries', start)
start = traces.length
await lastMile.findAll('.el-radio-button').find(item => item.text() === '全部')!.trigger('click')
await waitForTrace('GET', '/platform/v1/last-leg/orders', start)
const inbounds = await mountPage(InboundOrdersView, 'GET', '/platform/v1/last-leg/inbound-plans')
await exerciseListSearch(inbounds, '/platform/v1/last-leg/inbound-plans',
{ orderNo: `${marker}-PLAN`, inboundCode: `${marker}-INBOUND`, lxCode: `${marker}-LX`, status: 'COMPLETED', warehouse: `${marker}-WAREHOUSE`, fbaWarehouse: `${marker}-FBA`, appointmentStatus: 'CANCELLED', dispatchStatus: 'SCHEDULED', inboundDate: ['2098-01-01', '2098-01-02'] },
{ planNo: `${marker}-PLAN`, inboundCode: `${marker}-INBOUND`, lxCode: `${marker}-LX`, status: 'COMPLETED', warehouse: `${marker}-WAREHOUSE`, fbaWarehouseCode: `${marker}-FBA`, appointmentStatus: 'CANCELLED', dispatchStatus: 'SCHEDULED', createdFrom: '2098-01-01T00:00:00', createdTo: '2098-01-02T23:59:59' })
const inventory = await mountPage(InventoryView, 'GET', '/platform/v1/last-leg/inventory/balances')
await exerciseListSearch(inventory, '/platform/v1/last-leg/inventory/balances',
{ warehouseId: '', owner: `${marker}-OWNER`, sku: `${marker}-SKU`, productName: `${marker}-PRODUCT`, batchNo: `${marker}-BATCH`, location: `${marker}-LOCATION`, status: 'AVAILABLE' },
{ customerName: `${marker}-OWNER`, skuCode: `${marker}-SKU`, productName: `${marker}-PRODUCT`, batchNo: `${marker}-BATCH`, warehouseLocationCode: `${marker}-LOCATION`, inventoryStatus: 'AVAILABLE' })
const dispatches = await mountPage(DispatchesView, 'GET', '/platform/v1/last-leg/dispatches')
await exerciseListSearch(dispatches, '/platform/v1/last-leg/dispatches',
{ dispatchNo: `${marker}-DISPATCH`, vehiclePlate: `${marker}-PLATE`, status: 'CANCELLED', fbaAppointmentNo: `${marker}-APPOINTMENT` },
{ dispatchNo: `${marker}-DISPATCH`, vehiclePlate: `${marker}-PLATE`, status: 'CANCELLED', fbaAppointmentNo: `${marker}-APPOINTMENT` })
const relabels = await mountPage(RelabelOrdersView, 'GET', '/platform/v1/last-leg/relabel-applications')
await exerciseListSearch(relabels, '/platform/v1/last-leg/relabel-applications',
{ relabelNo: `${marker}-RELABEL`, sku: `${marker}-SKU`, status: 'CANCELLED' },
{ relabelNo: `${marker}-RELABEL`, skuCode: `${marker}-SKU`, status: 'CANCELLED' })
const outbounds = await mountPage(OutboundsView, 'GET', '/platform/v1/last-leg/outbound-orders')
await exerciseListSearch(outbounds, '/platform/v1/last-leg/outbound-orders',
{ documentNo: `${marker}-OUTBOUND`, lxCode: `${marker}-LX`, warehouseId: `${marker}-WAREHOUSE`, status: 'OUTBOUNDED' },
{ documentNo: `${marker}-OUTBOUND`, lxCode: `${marker}-LX`, warehouseId: `${marker}-WAREHOUSE`, status: 'OUTBOUNDED' })
const deliveries = await mountPage(DeliveryOrdersView, 'GET', '/platform/v1/last-leg/delivery-orders')
await exerciseListSearch(deliveries, '/platform/v1/last-leg/delivery-orders',
{ documentNo: `${marker}-DELIVERY`, dispatchNo: `${marker}-DISPATCH`, waybillNo: `${marker}-WAYBILL`, warehouseId: '', status: 'CANCELLED', vehiclePlate: `${marker}-PLATE`, driverName: `${marker}-DRIVER`, deliveryMethod: 'EXPRESS', plannedAt: ['2098-01-01', '2098-01-02'] },
{ documentNo: `${marker}-DELIVERY`, dispatchNo: `${marker}-DISPATCH`, waybillNo: `${marker}-WAYBILL`, status: 'CANCELLED', vehiclePlate: `${marker}-PLATE`, driverName: `${marker}-DRIVER`, deliveryMethod: 'EXPRESS', plannedFrom: '2098-01-01T00:00:00', plannedTo: '2098-01-02T23:59:59' })
const exceptions = await mountPage(ExceptionOrdersView, 'GET', '/platform/v1/last-leg/exception-orders')
await exerciseListSearch(exceptions, '/platform/v1/last-leg/exception-orders',
{ keyword: `${marker}-EXCEPTION`, exceptionType: 'OTHER', status: 'COMPLETED' },
{ keyword: `${marker}-EXCEPTION`, exceptionType: 'OTHER', status: 'COMPLETED' })
const visits = await mountPage(CustomerVisitsView, 'GET', '/platform/v1/last-leg/customer-visits')
await exerciseListSearch(visits, '/platform/v1/last-leg/customer-visits',
{ keyword: `${marker}-VISIT`, status: 'COMPLETED', completedAt: ['2098-01-01', '2098-01-02'] },
{ keyword: `${marker}-VISIT`, status: 'COMPLETED', completedFrom: '2098-01-01T00:00:00', completedTo: '2098-01-02T23:59:59' })
const ledger = await mountPage(InventoryTransactionsView, 'GET', '/platform/v1/last-leg/inventory/ledger')
await exerciseListSearch(ledger, '/platform/v1/last-leg/inventory/ledger',
{ occurredDate: ['2098-01-01', '2098-01-02'], customerName: `${marker}-CUSTOMER`, warehouse: `${marker}-WAREHOUSE`, sku: `${marker}-SKU`, businessType: 'ADJUST', businessOrderNo: `${marker}-BUSINESS`, inboundCode: `${marker}-INBOUND`, inboundOrderNo: `${marker}-INBOUND-ORDER`, outboundOrderNo: `${marker}-OUTBOUND` },
{ customerName: `${marker}-CUSTOMER`, destinationWarehouseCode: `${marker}-WAREHOUSE`, skuCode: `${marker}-SKU`, businessType: 'ADJUST', businessOrderNo: `${marker}-BUSINESS`, inboundCode: `${marker}-INBOUND`, inboundOrderNo: `${marker}-INBOUND-ORDER`, outboundOrderNo: `${marker}-OUTBOUND`, occurredFrom: '2098-01-01T00:00:00', occurredTo: '2098-01-02T23:59:59' })
2026-09-29 18:47:41 +08:00
const pallets = await mountPage(PalletsView, 'GET', '/platform/v1/overseas-warehouse-resources/pallets')
await exerciseListSearch(pallets, '/platform/v1/overseas-warehouse-resources/pallets',
{ warehouseId: '', code: `${marker}-PALLET`, type: 'WOOD', status: 'IDLE' },
{ keyword: `${marker}-PALLET`, palletType: 'WOOD', status: 'IDLE' })
}
2026-09-29 18:47:41 +08:00
describe.skipIf(!enabled).sequential('phase4 test frontend acceptance', () => {
beforeAll(async () => {
Object.defineProperty(window, 'matchMedia', { writable: true, value: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }) })
globalThis.ResizeObserver = class { observe() {}; unobserve() {}; disconnect() {} }
2026-09-29 18:47:41 +08:00
installTracingAdapter(http, 'unified')
const pinia = createPinia(); setActivePinia(pinia)
const auth = useAuthStore(pinia)
await auth.login(process.env.ADMIN_ACCEPTANCE_USERNAME || 'platform_admin', process.env.ADMIN_ACCEPTANCE_PASSWORD || '')
expect(localStorage.getItem('cc-admin-token')).toBeTruthy()
2026-09-29 18:47:41 +08:00
await refreshAuthenticatedSession()
await listVehicles({ page: 1, size: 1 })
2026-09-29 18:47:41 +08:00
expect(traces.some(item => item.region === 'unified' && item.path === '/auth/refresh')).toBe(true)
expect(traces.some(item => item.region === 'unified' && item.authorization)).toBe(true)
})
it('runs marker-owned adapter lifecycles and real page requests', async () => {
await masterDataLifecycle()
await overseasBusinessLifecycle()
await overseasWarehouseCoverage()
await carrierCoverage()
await pageRequestCoverage()
expect(traces.length).toBeGreaterThan(20)
})
afterAll(() => {
wrappers.splice(0).forEach(wrapper => wrapper.unmount())
const reportDir = resolve(process.cwd(), 'reports')
mkdirSync(reportDir, { recursive: true })
const report = { generatedAt: new Date().toISOString(), marker, summary: { requests: traces.length, coverageGaps: coverageGaps.length }, traces, coverageGaps, createdResources }
2026-09-29 18:47:41 +08:00
const file = resolve(reportDir, `phase4-frontend-test-${marker.replace(/[^A-Za-z0-9-]/g, '')}.json`)
writeFileSync(file, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
console.info(`PHASE4_FRONTEND_REPORT=${file}`)
})
})