This commit is contained in:
2026-08-26 16:50:25 +08:00
parent 5868e774d4
commit 083299602d
31 changed files with 816 additions and 239 deletions
+32
View File
@@ -0,0 +1,32 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
test('App-Plus checks the independent overseas update channel after launch', async () => {
const [app, updater, env] = await Promise.all([
read('../src/App.vue'),
read('../src/utils/app-update.js'),
read('../.env.example')
])
assert.match(app, /#ifdef APP-PLUS[\s\S]*?checkForAppUpdate\(\)/)
assert.match(env, /VITE_PDA_UPDATE_URL=https:\/\//)
assert.match(updater, /plus\.runtime\.getProperty/)
assert.match(updater, /appid:[\s\S]*?platform: 'android'[\s\S]*?appVersion:[\s\S]*?appVersionCode:[\s\S]*?wgtVersion:/)
})
test('overseas online update supports mandatory APK and wgt installation flows', async () => {
const updater = await read('../src/utils/app-update.js')
assert.match(updater, /\['apk', 'wgt'\]\.includes\(packageType\)/)
assert.match(updater, /update\.versionCode > runtime\.appVersionCode/)
assert.match(updater, /compareVersions\(update\.versionName, runtime\.wgtVersion\) > 0/)
assert.match(updater, /更新包必须使用 HTTPS 地址/)
assert.match(updater, /showCancel: !update\.mandatory/)
assert.match(updater, /uni\.downloadFile\([\s\S]*?onProgressUpdate/)
assert.match(updater, /plus\.runtime\.install/)
assert.match(updater, /packageType === 'wgt'[\s\S]*?plus\.runtime\.restart/)
assert.match(updater, /if \(retry\) await runUpdate\(update\)/)
})
+43 -8
View File
@@ -5,8 +5,8 @@ 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] = await Promise.all([
read('../src/manifest.json'), read('../src/pages.json'), read('../package.json'), read('../vite.config.js')
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"/)
@@ -17,6 +17,10 @@ test('overseas PDA is an independent Vue 3 app with separate packaging identity'
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(styles, /home-operation-grid[^}]*grid-template-columns:\s*repeat\(3/)
assert.match(styles, /home-operation[^}]*width:\s*100%/)
})
test('overseas workbench contains only confirmed inbound and in-warehouse domains', async () => {
@@ -34,7 +38,7 @@ test('login has no committed demo credentials and inbound operation implements t
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')
])
assert.match(login, /柏悦物流海外版/)
assert.match(login, /\$t\('app\.title'\)/)
assert.doesNotMatch(login, /ChangeMe|platform_admin/)
assert.match(login, /DEV_LOGIN\.username|DEV_LOGIN\.password/)
assert.match(runtime, /runtimeEnvironment\.DEV|VITE_PDA_DEV_USERNAME|VITE_PDA_DEV_PASSWORD/)
@@ -42,8 +46,14 @@ test('login has no committed demo credentials and inbound operation implements t
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', 'resolveCurrentSku', 'resolveCurrentCarton', 'resolveCurrentPallet', 'receiveCurrentBulk', 'receiveCurrentCarton', 'completeCurrentReceiving']) assert.match(operation, new RegExp(action))
for (const path of ['/candidates', '/prepare', '/unload-photos/files', '/sign', '/skus/resolve', '/cartons/resolve', '/pallets/resolve', '/receipts/bulk', '/receipts/carton', '/complete']) assert.match(inbound, new RegExp(path.replaceAll('/', '\\/')))
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('/', '\\/')))
assert.doesNotMatch(operation, /resolveCurrentSku|resolvedSku|skuInput|selectedInboundCode|chooseInbound|class="inbound-tabs"/)
assert.doesNotMatch(operation, /InboundList|柜内入仓码/)
assert.doesNotMatch(inbound, /skus\/resolve/)
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'/)
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/)
@@ -56,7 +66,32 @@ test('unimplemented warehouse operations cannot submit prototype scans', async (
read('../src/pages/operation/operation.vue'), read('../src/pages/home/home.vue'), read('../src/pages/module/module.vue')
])
assert.doesNotMatch(operation, /prototypeScan|prototypeInput/)
assert.match(operation, /功能暂未开放|扫码不会提交/)
assert.match(home, /暂未开放/)
assert.match(module, /当前仅开放拆柜收货,其他作业暂未开放/)
assert.match(operation, /operation\.unavailableTitle|operation\.unavailableDescription/)
assert.match(home, /common\.unavailable/)
assert.match(module, /module\.onlyReceiving/)
})
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', 'destinationSummary', '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/)
})
test('Chinese English and Spanish locale resources are installed and selectable', 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]) for (const key of ['operationPhase', 'receivingSummary', 'warehouseOperator']) assert.match(locale, new RegExp(key))
assert.match(i18n, /SUPPORTED_LOCALES = \['zh-CN', 'en', 'es'\]/)
assert.match(profile, /setLocale|LOCALE_OPTIONS/)
assert.match(inbound, /warehouse-profile/)
})
+16 -7
View File
@@ -45,18 +45,16 @@ test('three fixed receiving photo roles and string file IDs are frozen', () => {
assert.doesNotMatch(inbound, /customerId|planNo|allocationId|warehouseId|locationId/)
})
test('camera or album replaces only one fixed slot and failed replacement preserves its previous success', () => {
assert.match(page, /choosePhotos\(index, sourceType\)[\s\S]*uni\.chooseImage\(\{ count: 1, sourceType: \[sourceType\]/)
assert.match(page, /choosePhotos\(index, 'camera'\)/)
assert.match(page, /choosePhotos\(index, 'album'\)/)
test('camera replaces only one fixed slot and failed retake preserves its previous success', () => {
assert.match(page, /choosePhoto\(index\)[\s\S]*uni\.chooseImage\(\{ count: 1, sourceType: \['camera'\]/)
assert.doesNotMatch(page, /sourceType:\s*\['album'\]|从相册选择|换照片/)
assert.match(page, /async replacePhoto\(index, path\)[\s\S]*const previous = receiving\.photoSlots\[index\]/)
assert.match(page, /await uploadCurrentUnloadPhoto\(path\)[\s\S]*fileId: String\(file\.fileId\)[\s\S]*localPath: path/)
assert.match(page, /previous\?\.fileId \? \{ \.\.\.previous, status: 'error'/)
assert.match(page, /status: 'uploading'/)
assert.match(page, /status: 'error'/)
assert.match(page, /已保留原图/)
assert.match(page, /photoSlots\[index\]\?\.fileId \? '重拍' : '拍照'/)
assert.match(page, /photoSlots\[index\]\?\.fileId \? '重新选择' : '从相册选择'/)
assert.match(page, /photoStateLabel\(photo\)[\s\S]*operation\.retakeFailedKept[\s\S]*operation\.uploadFailed/)
assert.match(page, /photoSlots\[index\]\?\.fileId \? \$t\('operation\.retake'\) : \$t\('operation\.takePhoto'\)/)
assert.match(page, /previewPhoto\(index\)/)
assert.match(page, /uploadedPhotoCount\(\) \{ return this\.photoSlots\.filter\(\(item\) => item\?\.fileId\)\.length \}/)
assert.match(page, /photosReady\(\) \{ return this\.uploadedPhotoCount === 3 && !this\.photoSlots\.some/)
@@ -64,6 +62,11 @@ test('camera or album replaces only one fixed slot and failed replacement preser
assert.match(page, /async signContainer\(\) \{ if \(!this\.photosReady\) return;/)
assert.match(page, /const photos = this\.photoSlots\.map\(/)
assert.doesNotMatch(page, /count:\s*9|flatMap\(|photoSlots\[index\]\.push/)
assert.match(page, /\.photo-grid\{display:grid;grid-template-columns:1fr/)
assert.match(page, /grid-template-columns:82px minmax\(0,1fr\) 78px/)
assert.match(page, /class="photo-role-info"/)
assert.match(page, /onShow\(\)[\s\S]*this\.inboundMode && !this\.bootstrapped[\s\S]*this\.bootstrap\(\)/)
assert.match(page, /await this\.applyCurrentReceiving\(current\); this\.bootstrapped = true/)
})
test('local thumbnail, preview, and authenticated session restore are wired to the frozen contract', () => {
@@ -71,3 +74,9 @@ test('local thumbnail, preview, and authenticated session restore are wired to t
assert.deepEqual(photoUiBlockers().map((item) => item.code), [])
assert.ok(photoUiBlockers().every((item) => item.deploymentBlocker === true && item.reason))
})
test('fixed receiving action leaves enough scroll-safe space above the PDA bottom bar', () => {
assert.match(page, /receiving-content\{[^}]*padding-bottom:calc\(152px \+ env\(safe-area-inset-bottom,0px\)\)/)
assert.match(page, /primary-button,\.secondary-button,\.ghost-button\{[^}]*align-items:center;justify-content:center[^}]*line-height:normal;text-align:center/)
assert.match(page, /sticky-action button\{[^}]*align-items:center;justify-content:center;line-height:normal;text-align:center/)
})
+9 -5
View File
@@ -2,7 +2,7 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import {
PDA_AUTHORITY, PDA_ROLE, replaceTokens, restoreSession, session,
PDA_AUTHORITY, PDA_ROLE, applyWarehouseProfile, replaceTokens, restoreSession, session,
decodeAccessToken, signInWithTokens, signOut, validatePdaTokenPair
} from '../src/stores/session.js'
import { receiving } from '../src/stores/receiving.js'
@@ -63,6 +63,14 @@ test('JWT decoding preserves unquoted snowflake subjects and warehouse IDs exact
assert.equal(decoded.overseas_warehouse_id, '2091919753155878914')
})
test('warehouse master profile replaces the technical ID only for the token-bound warehouse', () => {
storage(); signOut(); signInWithTokens(tokenPair())
applyWarehouseProfile({ warehouseId: '900', warehouseCode: 'US-LAX-01', warehouseName: '洛杉矶一号仓' })
assert.equal(session.currentUser.warehouse, '洛杉矶一号仓')
assert.equal(session.currentUser.warehouseCode, 'US-LAX-01')
assert.throws(() => applyWarehouseProfile({ warehouseId: '901', warehouseName: '其他仓' }), /不一致/)
})
test('login fails closed for wrong role, authority, warehouse, application, or token subject', () => {
storage(); signOut()
const invalidPairs = [
@@ -123,9 +131,7 @@ test('sign out clears tokens, frozen warehouse identity, and every receiving dra
receiving.current = { containerNo: 'CONT-1', status: 'RECEIVING' }
receiving.selected = receiving.current
receiving.photoSlots = [{ fileId: '1' }, null, null]
receiving.selectedInboundCode = 'IN-1'
receiving.resolvedCarton = { cartonMark: 'BOX-1' }
receiving.resolvedSku = { skuCode: 'SKU-1' }
receiving.resolvedPallet = { palletCode: 'PALLET-1' }
receiving.lastReceipt = { acceptedPackages: 1 }
@@ -139,9 +145,7 @@ test('sign out clears tokens, frozen warehouse identity, and every receiving dra
assert.equal(receiving.current, null)
assert.equal(receiving.selected, null)
assert.deepEqual(receiving.photoSlots, [null, null, null])
assert.equal(receiving.selectedInboundCode, '')
assert.equal(receiving.resolvedCarton, null)
assert.equal(receiving.resolvedSku, null)
assert.equal(receiving.resolvedPallet, null)
assert.equal(receiving.lastReceipt, null)
})