Files
pda-overseas-uniapp/tests/project-contract.test.mjs
T

192 lines
14 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
test('overseas PDA is an independent Vue 3 app with separate packaging identity', async () => {
const [manifest, pages, pkg, vite, styles] = await Promise.all([
read('../src/manifest.json'), read('../src/pages.json'), read('../package.json'), read('../vite.config.js'), read('../src/styles/pda.scss')
])
assert.match(manifest, /"name"\s*:\s*"柏悦物流\s*PDA\s*海外版"/)
assert.match(manifest, /"packagename"\s*:\s*"com\.baiyuelogistics\.pda\.overseas"/)
assert.match(manifest, /"vueVersion"\s*:\s*"3"/)
assert.doesNotMatch(pages, /pages\/picking|pages\/tasks/)
assert.match(pkg, /"build:app"\s*:\s*"uni build -p app"/)
assert.match(vite, /API_PROXY_TARGET/)
assert.match(vite, /OVERSEAS_API_PROXY_TARGET/)
assert.match(vite, /\/overseas-api/)
assert.match(vite, /replace\(\/\^\\\/overseas-api\//)
assert.match(vite, /removeHeader\('origin'\)/)
assert.match(vite, /removeHeader\('referer'\)/)
assert.match(vite, /__VUE_I18N_FULL_INSTALL__:\s*true/)
assert.match(vite, /__VUE_I18N_LEGACY_API__:\s*false/)
assert.match(vite, /__VUE_I18N_PROD_DEVTOOLS__:\s*false/)
assert.match(vite, /__INTLIFY_PROD_DEVTOOLS__:\s*false/)
assert.match(styles, /home-operation-grid[^}]*grid-template-columns:\s*repeat\(3/)
assert.match(styles, /home-operation[^}]*width:\s*100%/)
assert.doesNotMatch(styles, /home-operation-primary[^}]*grid-column:\s*1\s*\/\s*-1/)
})
test('overseas workbench contains confirmed warehouse and phase-five outbound domains without delivery', async () => {
const [data, session, runtime] = await Promise.all([
read('../src/data/overseas-workbench.js'), read('../src/stores/session.js'), read('../src/config/runtime.js')
])
for (const label of ['入仓作业', '拆柜收货', '退货入库', '退货合箱', '出库履约', '排车拣货', '出库作业', '库内作业', '换标作业', '现场查询']) assert.match(data, new RegExp(label))
for (const excluded of ['集装箱拣货', '装柜取证', '海外派送', '派送单', '客户回访']) assert.doesNotMatch(data, new RegExp(excluded))
assert.match(session, /overseas-pda-auth-session/)
assert.match(runtime, /VITE_OVERSEAS_API_BASE_URL/)
})
test('login has no committed demo credentials and inbound operation implements the locked container flow', async () => {
const [login, auth, runtime, operation, inbound, adapter, overseas] = await Promise.all([
read('../src/pages/login/login.vue'), read('../src/api/auth.js'), read('../src/config/runtime.js'),
read('../src/pages/operation/operation.vue'), read('../src/api/inbound.js'), read('../src/api/receiving-adapter.js'), read('../src/api/overseas.js')
])
assert.match(login, /柏悦物流海外版/)
assert.doesNotMatch(login, /ChangeMe|platform_admin/)
assert.match(login, /DEV_LOGIN\.username|DEV_LOGIN\.password/)
assert.match(auth, /authRequest\('\/auth\/login', \{ loginId, password \}\)/)
assert.doesNotMatch(auth, /authRequest\('\/auth\/login', \{ username, password \}\)/)
assert.match(runtime, /runtimeEnvironment\.DEV|VITE_PDA_DEV_USERNAME|VITE_PDA_DEV_PASSWORD/)
assert.match(runtime, /'\/overseas-api'/)
assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_AUTH_API_BASE_URL/)
assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_OVERSEAS_API_BASE_URL/)
assert.match(operation, /startScanBroadcast/)
for (const action of ['prepareReceivingContainer', 'getCurrentReceiving', 'resolveCurrentCarton', 'resolveCurrentPallet', 'receiveCurrentBulk', 'receiveCurrentCarton', 'receiveCurrentWholePallet', 'completeCurrentReceiving']) assert.match(operation, new RegExp(action))
for (const path of ['/candidates', '/prepare', '/unload-photos/files', '/sign', '/cartons/resolve', '/pallets/resolve', '/receipts/bulk', '/receipts/carton', '/receipts/pallet', '/complete']) assert.match(inbound, new RegExp(path.replaceAll('/', '\\/')))
assert.match(inbound, /current\('\/pallets\/resolve'\)/)
assert.doesNotMatch(inbound, /platform\/v1\/warehouse-admin/, 'PDA只能调用current/pallets/resolve,不能访问Admin仓储资源接口')
assert.doesNotMatch(operation, /resolveCurrentSku|resolvedSku|skuInput|selectedInboundCode|chooseInbound|class="inbound-tabs"/)
assert.doesNotMatch(operation, /InboundList|柜内入仓码/)
assert.doesNotMatch(inbound, /skus\/resolve/)
assert.doesNotMatch(operation, /\.split\([^\n]*['"]#['"]/, '前端必须原样提交完整箱码,禁止按 # 猜测入仓码或箱序号')
assert.match(operation, /const payload = isFba \? \{ inboundCode: this\.activeInbound\.inboundCode, palletCode: this\.resolvedPallet\.palletCode, packageCount: Number\(this\.packageCount\), expectedPalletVersion: this\.resolvedPallet\.version \}/)
assert.doesNotMatch(operation, /skuCode/)
assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = 'carton'/)
assert.match(operation, /name="pallet"[\s\S]*@activate="activeTarget = 'pallet'"/)
assert.match(operation, /receiving\.activeScanTarget = 'pallet'/, '普通FBA或中转解析后直接进入托盘扫码以减少操作')
assert.match(operation, /\[Overseas PDA scan\] dispatch/)
assert.match(operation, /\[Overseas PDA cargo\] failed[\s\S]*requestId[\s\S]*traceId/)
for (const role of ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']) assert.match(adapter, new RegExp(role))
for (const forbidden of ['customerId', 'planNo', 'allocationId', 'warehouseId', 'locationId']) assert.doesNotMatch(inbound, new RegExp(forbidden))
assert.match(operation, /isReceivingConflict|recoverCurrentAfterConflict/)
assert.match(operation, /isNoCurrentReceiving\(recoveryError\)[\s\S]*柜状态已变化,列表已刷新/)
assert.match(operation, /onShow\(\)[\s\S]*this\.phase === 'candidates'[\s\S]*this\.bootstrap\(\)/)
assert.match(operation, /candidateLoading[\s\S]*candidateError/)
assert.match(operation, /candidateError\.requestId[\s\S]*candidateError\.traceId/)
assert.match(operation, /@tap="loadCandidates"[^>]*>重新加载/)
assert.doesNotMatch(operation, /扫描柜号|查询柜号<\/button>|v-model="containerInput"/)
assert.match(operation, /phase === 'candidates'[^\n]*请直接点击待卸货集装箱进入详情/)
assert.match(operation, /async loadCandidates\(feedback = true, refreshWarehouse = true\)[\s\S]*if \(this\.candidateLoading\) return[\s\S]*this\.candidateError = error/)
assert.match(operation, /async prepareContainer\(\)[\s\S]*this\.busy\) return/)
assert.match(operation, /prepareReceivingContainer[\s\S]*releaseIdempotencyKey\(key\.signature\)/)
assert.match(overseas, /replaceTokens|refreshOverseasToken/)
assert.match(overseas, /requestId/)
})
test('warehouse operations use dedicated real pages and contain no prototype task data', async () => {
const [operation, home, module, workbench, pages] = await Promise.all([
read('../src/pages/operation/operation.vue'), read('../src/pages/home/home.vue'),
read('../src/pages/module/module.vue'), read('../src/data/overseas-workbench.js'), read('../src/pages.json')
])
assert.doesNotMatch(operation, /prototypeScan|prototypeInput/)
assert.doesNotMatch(workbench, /PROTOTYPES|getOperationPrototype|模拟成功/)
for (const key of ['stocktake', 'move', 'relabelReceive', 'inventoryQuery', 'locationQuery', 'palletQuery']) {
assert.match(workbench, new RegExp(`key: '${key}'[^\\n]*available: true[^\\n]*route:`))
}
for (const page of ['pages/stocktake/stocktake', 'pages/move/move', 'pages/relabel/relabel', 'pages/query/query']) assert.match(pages, new RegExp(page))
assert.match(home + module, /operation\.route \|\|/)
})
test('receiving phases use a compiled container summary with cabinet and aggregate facts', async () => {
const [operation, summary] = await Promise.all([
read('../src/pages/operation/operation.vue'), read('../src/components/ReceivingSessionSummary.vue')
])
assert.match(operation, /import ReceivingSessionSummary/)
assert.equal((operation.match(/<ReceivingSessionSummary/g) || []).length, 3)
assert.doesNotMatch(operation, /const SessionSummary|template:\s*`<view class="container-card summary-card/)
for (const fact of ['containerNo', 'inboundCodeCount', 'plannedPalletCount', 'destinationValues', 'totalPackages', 'receivedPackages', 'pendingPackages']) assert.match(summary, new RegExp(fact))
assert.match(summary, /receiving-summary-metrics/)
assert.match(summary, /grid-template-columns:repeat\(4/)
assert.match(summary, /padding:10px 12px/)
assert.match(summary, /destination-tags\{[^}]*flex-wrap:wrap/)
assert.doesNotMatch(summary, /destinationSummary|text-overflow:ellipsis/)
assert.match(operation, /v-if="phase !== 'candidates' && phase !== 'receive'" class="receiving-heading"/)
})
test('candidate containers use compact card navigation and a small refresh action', async () => {
const operation = await read('../src/pages/operation/operation.vue')
assert.match(operation, /class="container-card container-card-compact"[\s\S]*container-card-disabled[\s\S]*@tap="openContainer\(item\)"/)
assert.match(operation, /class="container-detail-link"/)
assert.doesNotMatch(operation, /<button class="primary-button"[^>]*openContainer\(item\.containerNo\)/)
assert.match(operation, /class="list-toolbar"><text>共 \{\{ candidates\.length \}\} 柜<\/text>/)
assert.match(operation, /\.list-toolbar button\{[^}]*height:28px[^}]*min-height:28px/)
assert.match(operation, /class="candidate-facts"[\s\S]*总订单[\s\S]*入仓码[\s\S]*已生成计划[\s\S]*未操作/)
assert.match(operation, /class="candidate-destinations"/)
assert.match(operation, /计划到仓:\{\{ formatPlannedArrival\(item\.plannedArrivalAt\) \}\}/)
assert.match(operation, /item\.blockReason \|\| '当前柜入库计划尚未齐全'/)
assert.doesNotMatch(operation, /预计到港/)
assert.doesNotMatch(operation, /item\.receivedPackages[\s\S]*item\.totalPackages[\s\S]*progress-fill/)
})
test('Chinese English and Spanish locale selection is persisted from profile without claiming unfinished copy is translated', async () => {
const [main, i18n, zh, en, es, profile, inbound] = await Promise.all([
read('../src/main.js'), read('../src/i18n/index.js'), read('../src/i18n/locales/zh-CN.js'),
read('../src/i18n/locales/en.js'), read('../src/i18n/locales/es.js'), read('../src/pages/profile/profile.vue'), read('../src/api/inbound.js')
])
assert.match(main, /app\.use\(i18n\)/)
for (const locale of [zh, en, es]) assert.match(locale, /export default \{\}/)
assert.match(i18n, /SUPPORTED_LOCALES = \['zh-CN', 'en', 'es'\]/)
for (const label of ['简体中文', 'English', 'Español']) assert.match(i18n, new RegExp(label))
assert.match(profile, /currentLocale, LOCALE_OPTIONS, setLocale/)
assert.match(profile, /@tap="selectLanguage"[\s\S]*切换语言/)
assert.match(profile, /uni\.showActionSheet[\s\S]*setLocale\(selected\.value\)/)
assert.match(inbound, /warehouse-profile/)
})
test('return receipt is a real PDA flow backed by the dedicated return APIs', async () => {
const [pages, workbench, page, api, warehouseWork] = await Promise.all([
read('../src/pages.json'), read('../src/data/overseas-workbench.js'),
read('../src/pages/return-receipt/return-receipt.vue'), read('../src/api/return-receipt.js'),
read('../src/stores/warehouse-work.js')
])
assert.match(pages, /pages\/return-receipt\/return-receipt/)
assert.match(workbench, /key: 'returnReceipt'[^\n]*available: true[^\n]*route: '\/pages\/return-receipt\/return-receipt'/)
for (const action of ['startReturnReceipt', 'findReturnSkuCandidates', 'confirmReturnReceipt']) assert.match(page, new RegExp(action))
for (const path of ['/start', '/sku-candidates', '/confirm']) assert.match(api, new RegExp(path.replaceAll('/', '\\/')))
assert.match(page, /startScanBroadcast/)
assert.match(page, /startedBy: this\.startedBy/)
assert.match(page, /idempotencyKey: key\.value/)
assert.match(page, /requestId[\s\S]*traceId/)
assert.match(warehouseWork, /returnReceipt:[^\n]*pendingKeys/)
assert.doesNotMatch(page, /customerId/)
})
test('return packing is a separate same-customer inventory operation', async () => {
const [pages, workbench, page, api, warehouseWork] = await Promise.all([
read('../src/pages.json'), read('../src/data/overseas-workbench.js'),
read('../src/pages/return-packing/return-packing.vue'), read('../src/api/return-packing.js'),
read('../src/stores/warehouse-work.js')
])
assert.match(pages, /pages\/return-packing\/return-packing/)
assert.match(workbench, /key: 'returnPacking'[^\n]*available: true[^\n]*route: '\/pages\/return-packing\/return-packing'/)
for (const action of ['findPackingOrders', 'mergeReturnCartons']) assert.match(page, new RegExp(action))
for (const path of ['/packing/orders', '/cartons/merge', '/cartons/']) assert.match(api, new RegExp(path.replaceAll('/', '\\/')))
assert.match(page, /startScanBroadcast/)
assert.match(page, /customerKey[\s\S]*不同客户的退货单不能合箱/)
assert.match(page, /idempotencyKey: key\.value/)
assert.match(page, /requestId[\s\S]*traceId/)
assert.match(warehouseWork, /returnPacking:[^\n]*pendingKeys/)
assert.doesNotMatch(page.match(/<template>([\s\S]*?)<\/template>/)?.[1] || '', /customerId|warehouseId/)
})
test('home reuses the persisted warehouse profile instead of requesting it on every show', async () => {
const [home, inbound, operation] = await Promise.all([read('../src/pages/home/home.vue'), read('../src/api/inbound.js'), read('../src/pages/operation/operation.vue')])
assert.match(home, /if \(!this\.user\?\.warehouseCode \|\| !this\.user\?\.warehouseName\) this\.loadWarehouseProfile\(\)/)
assert.match(inbound, /let warehouseProfileRequest = null/)
assert.match(inbound, /if \(!warehouseProfileRequest\)[\s\S]*\.finally\(\(\) => \{ warehouseProfileRequest = null \}\)/)
assert.match(operation, /await this\.refreshBoundWarehouse\(\)[\s\S]*await this\.loadCandidates\(false, false\)/)
assert.match(operation, /if \(refreshWarehouse\) await this\.refreshBoundWarehouse\(\)/)
})