2026-08-20 20:10:20 +08:00
|
|
|
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 () => {
|
2026-08-26 16:50:25 +08:00
|
|
|
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')
|
2026-08-20 20:10:20 +08:00
|
|
|
])
|
2026-08-25 10:39:50 +08:00
|
|
|
assert.match(manifest, /"name"\s*:\s*"柏悦物流\s*PDA\s*海外版"/)
|
2026-08-20 20:10:20 +08:00
|
|
|
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"/)
|
2026-08-26 15:24:51 +08:00
|
|
|
assert.match(vite, /API_PROXY_TARGET/)
|
|
|
|
|
assert.match(vite, /OVERSEAS_API_PROXY_TARGET/)
|
|
|
|
|
assert.match(vite, /\/overseas-api/)
|
|
|
|
|
assert.match(vite, /replace\(\/\^\\\/overseas-api\//)
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.match(vite, /removeHeader\('origin'\)/)
|
|
|
|
|
assert.match(vite, /removeHeader\('referer'\)/)
|
|
|
|
|
assert.match(styles, /home-operation-grid[^}]*grid-template-columns:\s*repeat\(3/)
|
|
|
|
|
assert.match(styles, /home-operation[^}]*width:\s*100%/)
|
2026-08-20 20:10:20 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('overseas workbench contains only confirmed inbound and in-warehouse domains', 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/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-24 19:29:41 +08:00
|
|
|
test('login has no committed demo credentials and inbound operation implements the locked container flow', async () => {
|
2026-08-25 19:56:47 +08:00
|
|
|
const [login, runtime, operation, inbound, adapter, overseas] = await Promise.all([
|
|
|
|
|
read('../src/pages/login/login.vue'), 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')
|
2026-08-20 20:10:20 +08:00
|
|
|
])
|
2026-08-26 17:09:07 +08:00
|
|
|
assert.match(login, /柏悦物流海外版/)
|
2026-08-20 20:10:20 +08:00
|
|
|
assert.doesNotMatch(login, /ChangeMe|platform_admin/)
|
2026-08-25 19:56:47 +08:00
|
|
|
assert.match(login, /DEV_LOGIN\.username|DEV_LOGIN\.password/)
|
|
|
|
|
assert.match(runtime, /runtimeEnvironment\.DEV|VITE_PDA_DEV_USERNAME|VITE_PDA_DEV_PASSWORD/)
|
2026-08-26 15:24:51 +08:00
|
|
|
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/)
|
2026-08-20 20:10:20 +08:00
|
|
|
assert.match(operation, /startScanBroadcast/)
|
2026-08-26 16:50:25 +08:00
|
|
|
for (const action of ['prepareReceivingContainer', 'getCurrentReceiving', 'resolveCurrentCarton', 'resolveCurrentPallet', 'receiveCurrentBulk', 'receiveCurrentCarton', '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', '/complete']) assert.match(inbound, new RegExp(path.replaceAll('/', '\\/')))
|
2026-08-26 20:40:00 +08:00
|
|
|
assert.match(inbound, /current\('\/pallets\/resolve'\)/)
|
|
|
|
|
assert.doesNotMatch(inbound, /platform\/v1\/warehouse-admin/, 'PDA只能调用current/pallets/resolve,不能访问Admin仓储资源接口')
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.doesNotMatch(operation, /resolveCurrentSku|resolvedSku|skuInput|selectedInboundCode|chooseInbound|class="inbound-tabs"/)
|
|
|
|
|
assert.doesNotMatch(operation, /InboundList|柜内入仓码/)
|
|
|
|
|
assert.doesNotMatch(inbound, /skus\/resolve/)
|
2026-08-26 17:51:02 +08:00
|
|
|
assert.doesNotMatch(operation, /\.split\([^\n]*['"]#['"]/, '前端必须原样提交完整箱码,禁止按 # 猜测入仓码或箱序号')
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.match(operation, /const payload = isFba \? \{ inboundCode: this\.activeInbound\.inboundCode, palletCode: this\.resolvedPallet\.palletCode, packageCount: Number\(this\.packageCount\) \}/)
|
|
|
|
|
assert.doesNotMatch(operation, /skuCode/)
|
|
|
|
|
assert.match(operation, /clearReceiptContext\(\)[\s\S]*activeScanTarget = 'carton'/)
|
2026-08-26 20:40:00 +08:00
|
|
|
assert.match(operation, /name="pallet"[\s\S]*@activate="activeTarget = 'pallet'"/)
|
|
|
|
|
assert.doesNotMatch(operation, /receiving\.activeScanTarget = 'pallet'/, '箱码解析成功后必须由作业员手动选择托盘扫码,不得自动跳转')
|
2026-08-26 17:23:42 +08:00
|
|
|
assert.match(operation, /\[Overseas PDA scan\] dispatch/)
|
|
|
|
|
assert.match(operation, /\[Overseas PDA carton\] resolved/)
|
|
|
|
|
assert.match(operation, /\[Overseas PDA carton\] failed[\s\S]*requestId[\s\S]*traceId/)
|
2026-08-24 19:29:41 +08:00
|
|
|
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/)
|
2026-08-27 15:56:13 +08:00
|
|
|
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.match(operation, /async loadCandidates\(feedback = 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\)/)
|
2026-08-24 15:25:36 +08:00
|
|
|
assert.match(overseas, /replaceTokens|refreshOverseasToken/)
|
|
|
|
|
assert.match(overseas, /requestId/)
|
2026-08-20 20:10:20 +08:00
|
|
|
})
|
2026-08-25 10:39:50 +08:00
|
|
|
|
|
|
|
|
test('unimplemented warehouse operations cannot submit prototype scans', async () => {
|
|
|
|
|
const [operation, home, module] = await Promise.all([
|
|
|
|
|
read('../src/pages/operation/operation.vue'), read('../src/pages/home/home.vue'), read('../src/pages/module/module.vue')
|
|
|
|
|
])
|
|
|
|
|
assert.doesNotMatch(operation, /prototypeScan|prototypeInput/)
|
2026-08-26 17:09:07 +08:00
|
|
|
assert.match(operation, /功能暂未开放|扫码不会提交/)
|
|
|
|
|
assert.match(home, /暂未开放/)
|
|
|
|
|
assert.match(module, /当前仅开放拆柜收货,其他作业暂未开放/)
|
2026-08-26 16:50:25 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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/)
|
2026-08-26 17:09:07 +08:00
|
|
|
for (const fact of ['containerNo', 'inboundCodeCount', 'plannedPalletCount', 'destinationValues', 'totalPackages', 'receivedPackages', 'pendingPackages']) assert.match(summary, new RegExp(fact))
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.match(summary, /receiving-summary-metrics/)
|
|
|
|
|
assert.match(summary, /grid-template-columns:repeat\(4/)
|
|
|
|
|
assert.match(summary, /padding:10px 12px/)
|
2026-08-26 17:09:07 +08:00
|
|
|
assert.match(summary, /destination-tags\{[^}]*flex-wrap:wrap/)
|
|
|
|
|
assert.doesNotMatch(summary, /destinationSummary|text-overflow:ellipsis/)
|
2026-08-26 17:23:42 +08:00
|
|
|
assert.match(operation, /v-if="phase !== 'receive'" class="receiving-heading"/)
|
2026-08-26 16:50:25 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-26 17:09:07 +08:00
|
|
|
test('Chinese English and Spanish locale scaffolding is installed without exposing unfinished translations', async () => {
|
2026-08-26 16:50:25 +08:00
|
|
|
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\)/)
|
2026-08-26 17:09:07 +08:00
|
|
|
for (const locale of [zh, en, es]) assert.match(locale, /export default \{\}/)
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.match(i18n, /SUPPORTED_LOCALES = \['zh-CN', 'en', 'es'\]/)
|
2026-08-26 17:09:07 +08:00
|
|
|
assert.doesNotMatch(profile, /setLocale|LOCALE_OPTIONS|profile\.language/)
|
2026-08-26 16:50:25 +08:00
|
|
|
assert.match(inbound, /warehouse-profile/)
|
2026-08-25 10:39:50 +08:00
|
|
|
})
|