211 lines
13 KiB
JavaScript
211 lines
13 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
import { runInNewContext } from 'node:vm'
|
|
import ts from 'typescript'
|
|
|
|
const api = readFileSync(new URL('./api/businessOrders.ts', import.meta.url), 'utf8')
|
|
const routes = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8')
|
|
const view = readFileSync(new URL('./components/BusinessOrdersView.vue', import.meta.url), 'utf8')
|
|
const demandComponent = readFileSync(new URL('./components/TransportDemandForm.vue', import.meta.url), 'utf8')
|
|
const demandLogic = ts.transpileModule(readFileSync(new URL('./transportDemandForm.ts', import.meta.url), 'utf8'), { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText.replace(/^export /gm, '')
|
|
const demandHelpers = runInNewContext(`${demandLogic}\n({ createTransportDemandForm, transportDemandRules, transportDemandError, buildTransportDemand })`)
|
|
const procurementApi = readFileSync(new URL('./api/procurement.ts', import.meta.url), 'utf8')
|
|
const procurementView = readFileSync(new URL('./views/ProcurementView.vue', import.meta.url), 'utf8')
|
|
|
|
test('renders unknown pallet service and container type as dashes while preserving explicit values', () => {
|
|
const helpers = view.slice(view.indexOf('function read('), view.indexOf('function idempotencyKey('))
|
|
const compiled = ts.transpileModule(helpers, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
|
const { displayCell, yesNo, text } = runInNewContext(`${compiled}\n({ displayCell, yesNo, text })`)
|
|
assert.equal(displayCell({ palletRequired: null }, 'palletRequired'), '—')
|
|
assert.equal(displayCell({ palletRequired: 'NO' }, 'palletRequired'), '否')
|
|
assert.equal(displayCell({ palletRequired: 'YES' }, 'palletRequired'), '是')
|
|
assert.equal(displayCell({ containerType: null }, 'containerType'), '—')
|
|
assert.equal(displayCell({ containerType: '40HQ' }, 'containerType'), '40HQ')
|
|
assert.equal(yesNo(null), '—')
|
|
assert.equal(text(null), '—')
|
|
assert.match(api, /palletRequired\?: string \| null/)
|
|
const create = api.slice(api.indexOf('export interface TransportCreate'), api.indexOf('export interface TransportUpdate'))
|
|
assert.doesNotMatch(create, /palletRequired\?: string \| null/)
|
|
})
|
|
|
|
test('shows merchant-owned arrangement facts without a pool request or editable status control', () => {
|
|
assert.match(api, /fleetArrangement: import\('\.\.\/fleetArrangement'\)\.FleetArrangement \| null/)
|
|
assert.match(view, /aria-label="承运安排"/)
|
|
for (const field of ['signedPackages', 'unsignedPackages', 'returnedPackages']) assert.ok(view.includes(`fleetArrangement.${field}`))
|
|
assert.doesNotMatch(api, /capacity-offers|fleet\/trips/)
|
|
assert.match(view, /if \(query\.page !== 1\) query\.page = 1\s+else void load\(\)/)
|
|
assert.match(view, /if \(reloadDirectly\) void load\(\)/)
|
|
})
|
|
|
|
test('activates all three customer cross-border pages with frozen permissions', () => {
|
|
for (const [path, permission] of [
|
|
['cross-border/procurement', 'merchant:cross-border:procurement:list'],
|
|
['cross-border/domestic-trucking', 'merchant:cross-border:domestic-trucking:list'],
|
|
['cross-border/pallet-shipping', 'merchant:cross-border:pallet-shipping:list'],
|
|
]) {
|
|
assert.match(routes, new RegExp(`path: '${path}'[\\s\\S]*?release: 'active'[\\s\\S]*?permission: '${permission}'`))
|
|
}
|
|
assert.doesNotMatch(view, /PrototypeFeatureView|mock|customerId|tenantId/i)
|
|
assert.match(procurementView, /merchant:cross-border:procurement:create/)
|
|
assert.match(view, /merchant:cross-border:domestic-trucking:create/)
|
|
assert.match(view, /merchant:cross-border:pallet-shipping:create/)
|
|
assert.match(view, /v-if="canCreate"[^>]*type="primary"/)
|
|
})
|
|
|
|
test('uses only merchant overseas endpoints and keeps retry idempotency stable', () => {
|
|
assert.match(procurementApi, /overseas-procurements/)
|
|
assert.doesNotMatch(api, /overseas-procurements|ProcurementSummary|ProcurementDetail/)
|
|
assert.match(api, /us-truck-orders/)
|
|
assert.match(api, /local-looseboard-orders/)
|
|
assert.doesNotMatch(api, /\/platform\/|\/internal\/|customerId|tenantId/)
|
|
assert.match(view, /const stableSubmitKey = submitKey\.value \|\| idempotencyKey\(\)/)
|
|
assert.match(view, /submitKey\.value = stableSubmitKey/)
|
|
assert.match(view, /idempotencyKey: stableSubmitKey/g)
|
|
})
|
|
|
|
test('covers list detail create loading error empty pagination and actions-first contracts', () => {
|
|
assert.match(view, /mode\.value = 'detail'/)
|
|
assert.match(view, /mode\.value = 'list'/)
|
|
assert.match(view, /mode = 'create'/)
|
|
assert.match(view, /v-loading="loading"/)
|
|
assert.match(view, /class="business-load-error"/)
|
|
assert.match(view, /empty-text="暂无订单"/)
|
|
assert.match(view, /<QueryToolbar[^>]*layout="grid"/)
|
|
assert.match(view, /<DataTable[^>]*:row-actions="getRowActions"/)
|
|
assert.match(view, /key: 'actions', label: '操作', width: 90, fixed: 'left'/)
|
|
assert.match(view, /class="business-table-card"/)
|
|
assert.doesNotMatch(procurementView, /inventoryQuantity|outboundQuantity|reviewRemark/)
|
|
assert.match(procurementView, /procurementStatuses/)
|
|
assert.match(procurementView, /ProcurementDetailPanel/)
|
|
})
|
|
|
|
test('opens every dashboard cross-border action on a dedicated protected create page', () => {
|
|
for (const [path, permission] of [
|
|
['cross-border/procurement/create', 'merchant:cross-border:procurement:create'],
|
|
['cross-border/domestic-trucking/create', 'merchant:cross-border:domestic-trucking:create'],
|
|
['cross-border/pallet-shipping/create', 'merchant:cross-border:pallet-shipping:create'],
|
|
]) {
|
|
assert.match(routes, new RegExp(`path: '${path}'[\\s\\S]*?props: \\{ createPage: true \\}[\\s\\S]*?permission: '${permission}'`))
|
|
}
|
|
assert.match(view, /props\.createPage \? 'create' : 'list'/)
|
|
assert.match(view, /if \(props\.createPage\) await router\.push/)
|
|
})
|
|
|
|
test('opens all cross-border details on dedicated protected routes', () => {
|
|
for (const [path, routeName] of [
|
|
['cross-border/procurement/:orderNo/detail', 'cross-border-procurement-detail'],
|
|
['cross-border/domestic-trucking/:orderNo/detail', 'cross-border-domestic-trucking-detail'],
|
|
['cross-border/pallet-shipping/:orderNo/detail', 'cross-border-pallet-shipping-detail'],
|
|
]) {
|
|
assert.match(routes, new RegExp(`path: '${path.replaceAll('/', '\\/')}'[\\s\\S]*?name: '${routeName}'`))
|
|
}
|
|
assert.match(view, /name: detailRouteName\.value, params: \{ orderNo: row\.orderNo \}/)
|
|
assert.match(view, /class="business-detail-error"/)
|
|
})
|
|
|
|
test('preserves the existing transport responsive rules without adding a narrow-screen procurement acceptance gate', () => {
|
|
assert.match(view, /@media \(max-width: 767px\)/)
|
|
assert.match(view, /queryFields/)
|
|
assert.match(view, /\.detail-grid \{ grid-template-columns: 1fr; \}/)
|
|
assert.match(view, /\.form-card :deep\(\.el-form-item\) \{ display: block; \}/)
|
|
assert.match(demandComponent, /\.transport-demand-budget \{ display: grid; width: 100%/)
|
|
})
|
|
|
|
test('uses the shared dense order-list and detail visual language', () => {
|
|
assert.match(view, /class="business-orders client-ui"/)
|
|
assert.match(view, /\.business-detail-page \{[^}]*border: 1px solid var\(--client-border\);[^}]*border-radius: var\(--client-radius\);/)
|
|
assert.match(view, /\.detail-grid \{[^}]*grid-template-columns: repeat\(4, minmax\(0, 1fr\)\)/)
|
|
assert.match(view, /\.detail-grid div \{[^}]*min-height: 34px;[^}]*grid-template-columns: 76px minmax\(0, 1fr\)/)
|
|
})
|
|
|
|
function transportHarness(kind = 'trucking', failFirst = false) {
|
|
const calls = []
|
|
const script = view.match(/<script setup lang="ts">([\s\S]*?)<\/script>/)[1]
|
|
const compiled = ts.transpileModule(script, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText
|
|
.replace(/^import .*?;$/gm, '').replace(/^export \{\};$/gm, '')
|
|
const harness = runInNewContext(`${compiled}\n({ form, formRef, submit, detail, detailFields })`, {
|
|
defineProps: () => ({ kind, createPage: true, detailPage: false }),
|
|
withDefaults: props => props,
|
|
computed: getter => ({ get value() { return typeof getter === 'function' ? getter() : getter.get() } }),
|
|
reactive: value => value, ref: value => ({ value }), onMounted: () => {}, watch: () => {},
|
|
useAuthStore: () => ({ hasPermission: () => true }), useRouter: () => ({ push: async () => {} }), useRoute: () => ({ params: {} }),
|
|
...demandHelpers, poolOrderStatusLabels: {},
|
|
ElMessage: { success: () => {}, error: () => {} },
|
|
createTransport: async (business, payload) => { calls.push({ business, payload: { ...payload } }); if (failFirst && calls.length === 1) throw new Error('retry'); },
|
|
})
|
|
harness.formRef.value = { validate: async () => true, clearValidate: () => {} }
|
|
return { ...harness, calls, rules: { get value() { return demandHelpers.transportDemandRules(kind === 'trucking' ? 'truck' : 'looseboard', harness.form) } } }
|
|
}
|
|
|
|
test('truck requires both contacts vehicle and minute times without inheriting looseboard fields', () => {
|
|
const truck = transportHarness()
|
|
for (const key of ['vehicleType', 'originContactName', 'originContactPhone', 'destinationContactName', 'destinationContactPhone', 'pickupAt', 'deliveryAt']) assert.equal(truck.rules.value[key][0].required, true)
|
|
for (const key of ['palletCount', 'contactName', 'contactInfo', 'requestedDate']) assert.equal(truck.rules.value[key], undefined)
|
|
const loose = transportHarness('looseboard')
|
|
assert.equal(loose.rules.value.contactName, undefined)
|
|
assert.equal(loose.rules.value.contactInfo, undefined)
|
|
assert.equal(loose.rules.value.transportMode, undefined)
|
|
for (const key of ['originContactName', 'originContactPhone', 'destinationContactName', 'destinationContactPhone', 'pickupAt', 'deliveryAt']) assert.equal(loose.rules.value[key][0].required, true)
|
|
truck.form.pickupAt = '2026-10-20T09:30:00'
|
|
let failure
|
|
truck.rules.value.deliveryAt[1].validator({}, '2026-10-20T09:29:00', error => { failure = error })
|
|
assert.equal(failure.message, 'C点送达时间不能早于B点取货时间')
|
|
})
|
|
|
|
test('truck retry submits the complete new contract with identical local times and idempotency key', async () => {
|
|
const current = transportHarness('trucking', true)
|
|
Object.assign(current.form, { vehicleType: 'PALLET_30', origin: 'Los Angeles', destination: 'Ontario', originContactName: 'A', originContactPhone: '+1-555-0100', destinationContactName: 'B', destinationContactPhone: '+1-555-0101', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00' })
|
|
await current.submit()
|
|
assert.equal(current.form.destinationContactName, 'B')
|
|
await current.submit()
|
|
assert.equal(current.calls.length, 2)
|
|
const first = current.calls[0].payload
|
|
const second = current.calls[1].payload
|
|
assert.equal(first.idempotencyKey, second.idempotencyKey)
|
|
assert.equal(second.vehicleType, 'PALLET_30')
|
|
assert.equal(second.originContactPhone, '+1-555-0100')
|
|
assert.equal(second.destinationContactName, 'B')
|
|
assert.equal(second.pickupAt, '2026-10-20T09:30:00')
|
|
assert.equal(second.deliveryAt, '2026-10-20T15:45:00')
|
|
for (const field of ['transportType', 'containerType', 'palletCount', 'contactName', 'contactInfo', 'requestedDate']) assert.equal(Object.hasOwn(second, field), false)
|
|
})
|
|
|
|
test('truck details render saved minute times and unknown pool demand without timezone conversion', () => {
|
|
const current = transportHarness()
|
|
current.detail.value = { summary: { vehicleType: null, pickupAt: '2026-10-20T09:30:00', deliveryAt: null }, originAddress: null, destinationAddress: null, remark: null }
|
|
const fields = new Map(current.detailFields.value)
|
|
assert.equal(fields.get('车辆类型'), '—')
|
|
assert.equal(fields.get('B点取货时间'), '2026-10-20 09:30')
|
|
assert.equal(fields.get('C点送达时间'), '—')
|
|
assert.equal(fields.has('柜型'), false)
|
|
assert.equal(fields.has('托盘数'), false)
|
|
})
|
|
|
|
|
|
test('looseboard saves split contacts and both local minute times without obsolete fields', async () => {
|
|
const current = transportHarness('looseboard', true)
|
|
Object.assign(current.form, { originContactName: 'A', originContactPhone: '1', destinationContactName: 'B', destinationContactPhone: '2', productName: '家居用品', estimatedPalletCount: 3, packageCount: 8, origin: 'LA', destination: 'ONT', pickupAt: '2026-10-20T09:00:00', deliveryAt: '2026-10-20T15:30:00' })
|
|
await current.submit(); await current.submit()
|
|
const payload = current.calls[1].payload
|
|
assert.equal(payload.estimatedPalletCount, 3)
|
|
assert.equal(payload.packageCount, 8)
|
|
assert.equal(payload.productName, '家居用品')
|
|
assert.equal(payload.pickupAt, '2026-10-20T09:00:00')
|
|
assert.equal(payload.originContactName, 'A')
|
|
assert.equal(payload.destinationContactPhone, '2')
|
|
assert.equal(payload.deliveryAt, '2026-10-20T15:30:00')
|
|
assert.equal(payload.idempotencyKey, current.calls[0].payload.idempotencyKey)
|
|
for (const field of ['palletRequired', 'transportType', 'transportMode', 'containerType', 'contactName', 'contactInfo', 'requestedDate']) assert.equal(Object.hasOwn(payload, field), false)
|
|
})
|
|
|
|
test('looseboard requires both times and rejects backwards schedule', () => {
|
|
const current = transportHarness('looseboard')
|
|
assert.equal(current.rules.value.pickupAt[0].required, true)
|
|
assert.equal(current.rules.value.deliveryAt[0].required, true)
|
|
let failure
|
|
current.form.pickupAt = '2026-10-20T10:30:00'
|
|
current.rules.value.deliveryAt[1].validator({}, '2026-10-20T10:29:00', error => { failure = error })
|
|
assert.ok(failure)
|
|
})
|