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('page configuration starts at login and registers independent PDA routes', async () => { const pages = JSON.parse(await read('../pages.json')) assert.equal(pages.pages[0].path, 'pages/login/login') assert.equal(pages.pages[0].style.navigationStyle, 'custom') assert.deepEqual(pages.pages.map((page) => page.path), [ 'pages/login/login', 'pages/home/home', 'pages/module/module', 'pages/operation/operation', 'pages/tasks/tasks', 'pages/dropship-tasks/dropship-tasks', 'pages/dropship-palletization/dropship-palletization', 'pages/history/history', 'pages/profile/profile', 'pages/scanner-test/scanner-test', 'pages/picking/picking' ]) }) test('application uses the Vue 3 uni-app entry and no unused Android permissions', async () => { const [manifest, main] = await Promise.all([read('../manifest.json'), read('../main.js')]) assert.match(manifest, /"vueVersion"\s*:\s*"3"/) assert.match(manifest, /"uniStatistics"\s*:\s*\{\s*"enable"\s*:\s*false\s*\}/) assert.match(main, /createSSRApp/) assert.doesNotMatch(main, /new\s+Vue/) assert.doesNotMatch(manifest, /android\.permission\.(VIBRATE|ACCESS_NETWORK_STATE)/) }) test('login uses the real auth endpoint and relaunches to the modular workbench', async () => { const [login, auth, session, runtime, vite, manifest] = await Promise.all([ read('../pages/login/login.vue'), read('../api/auth.js'), read('../stores/session.js'), read('../config/runtime.js'), read('../vite.config.js'), read('../manifest.json') ]) for (const text of ['柏悦物流 · PDA作业端', '工号 / 用户名', '登录中']) assert.match(login, new RegExp(text)) assert.doesNotMatch(login, /DEMO_ACCOUNTS|快捷登录|作业仓库/) assert.match(auth, /\/auth\/login/) assert.match(auth, /authRequest\('\/auth\/login', \{ loginId, password \}\)/) assert.doesNotMatch(auth, /authRequest\('\/auth\/login', \{ username, password \}\)/) assert.match(login, /loginRequest\(userId, this\.form\.password\)/) assert.match(login, /signInWithTokens\(tokens\)/) assert.match(login, /form: \{ userId: '', password: '' \}/) assert.doesNotMatch(login, /platform_admin|ChangeMe123!/) assert.match(login, /记住账号/) assert.match(login, /loadLoginPreference\(\)/) assert.match(login, /saveLoginPreference\(this\.rememberAccount, userId\)/) assert.match(session, /pda-auth-session/) assert.match(session, /pda-login-account-preference/) assert.match(session, /claims\.app_code !== 'PLATFORM'/) assert.match(runtime, /import\.meta\.env\.VITE_API_BASE_URL/) assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_API_BASE_URL \|\| '\/gateway-api'/) assert.doesNotMatch(runtime, /typeof\s+import\.meta/) assert.match(vite, /API_PROXY_TARGET/) assert.match(vite, /process\.env\.UNI_INPUT_DIR \|\| process\.cwd\(\)/) assert.match(vite, /'\/gateway-api'[\s\S]*?target: proxyTarget[\s\S]*?changeOrigin: true[\s\S]*?replace\(\/\^\\\/gateway-api\/[\s\S]*?'\/api'/) assert.match(vite, /removeHeader\('origin'\)/) assert.match(vite, /removeHeader\('referer'\)/) assert.match(vite, /proxy\.on\('error'/) assert.doesNotMatch(manifest, /"proxy"\s*:|pathRewrite|disableHostCheck/) assert.match(login, /uni\.reLaunch\(\{ url: '\/pages\/home\/home' \}\)/) }) test('picking task list uses remote loading orders and never falls back to demo tasks', async () => { const [tasks, home, modulePage] = await Promise.all([read('../pages/tasks/tasks.vue'), read('../pages/home/home.vue'), read('../pages/module/module.vue')]) assert.match(tasks, /listLoadingOrders/) assert.match(tasks, /当前网络不可用,请联网后刷新/) assert.match(tasks, /进入并开始拣货/) assert.match(tasks, /待平台补录集装箱号/) assert.match(tasks, /请先在平台预装详情补录真实集装箱号/) assert.match(tasks, /AWAITING_REVIEW[\s\S]*?待上传证据\/确认装柜/) assert.match(tasks, /task\.workflowStatus === 'AWAITING_REVIEW' \? '继续装柜取证'/) assert.match(tasks, /\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'COMPLETION_FAILED'\]\.includes\(task\.workflowStatus\)/) assert.match(tasks, /\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'PROCESSING', 'COMPLETION_FAILED'\]\.includes\(task\.workflowStatus\)/) assert.match(tasks, /\['AWAITING_REVIEW', 'COMPLETION_FAILED'\]\.includes\(task\.workflowStatus\) \|\| \(task\.assignmentVersion !== null/) assert.match(tasks, /暂无待处理的拣货任务/) assert.match(tasks, /class="task-code">\{\{ containerReference\(task\) \}\}/) assert.doesNotMatch(tasks, /task-identifiers|businessReference\(task\)/) assert.match(tasks, /button v-for="task in tasks"[\s\S]*?class="task-card"[\s\S]*?@tap="enterTask\(task\)"/) assert.match(tasks, /records\.filter\(\(task\) => this\.isTaskVisible\(task\)\)/) assert.match(tasks, /isTaskReady\(task\)[\s\S]*?task\.recordStatus === 'ACTIVE'[\s\S]*?!task\.mergedIntoContainerBookingId/) assert.match(tasks, /records\.filter\(\(task\) => this\.isMergedTask\(task\)\)\.forEach\(\(task\) => this\.clearMergedTaskDraft\(task\)\)/) assert.match(tasks, /clearMergedTaskDraft\(task\)[\s\S]*?clearPickingDraft\(\{ operatorId, taskId: task\.taskId, loadingPlanId: task\.loadingPlanId, containerBookingId: task\.containerBookingId/) assert.match(tasks, /task\.taskId[\s\S]*?task\.loadingPlanId[\s\S]*?task\.containerBookingId[\s\S]*?task\.currentContainerNo[\s\S]*?task\.version[\s\S]*?task\.assignmentVersion/) assert.doesNotMatch(tasks, /task\.taskStatus === 'LOADING'/) assert.doesNotMatch(tasks, /demo-data|DEMO_/) assert.match(home, /routes\[operation.name\]/) assert.match(modulePage, /routes\[operation.name\]/) }) test('workbench, module, history and profile are independent sources', async () => { const [home, module, history, profile] = await Promise.all([ read('../pages/home/home.vue'), read('../pages/module/module.vue'), read('../pages/history/history.vue'), read('../pages/profile/profile.vue') ]) for (const text of ['首页', '库内作业', 'home-operation-grid', 'home-operation-primary', '开始作业', 'uni-icons']) assert.match(home, new RegExp(text)) assert.doesNotMatch(home, /getPickingOverview|pendingBoxCount|箱待拣/) assert.doesNotMatch(home, /BottomNav|status-strip|task-card/) for (const text of ['业务模块', '业务功能', '一件代发', '出库拣货']) assert.match(module, new RegExp(text)) assert.match(home, /:disabled="!operation\.available"/) assert.match(home, /home-operation-disabled/) assert.match(home, /home-operation-meta home-operation-pending/) assert.match(home, /home-operation-meta home-operation-unavailable/) assert.match(home, /openOperation\(operation\)[\s\S]*?if \(!operation\.available\) return[\s\S]*?'一件代发': '\/pages\/dropship-tasks\/dropship-tasks'[\s\S]*?'出库拣货': '\/pages\/tasks\/tasks'[\s\S]*?'库位查询'/) assert.match(home, /pages\/operation\/operation\?module=query&name=/) assert.match(module, /:disabled="!operation\.available"/) assert.match(module, /operation-item-disabled/) assert.match(module, /openOperation\(operation\)[\s\S]*?if \(!operation\.available\) return[\s\S]*?'一件代发': '\/pages\/dropship-tasks\/dropship-tasks'[\s\S]*?'出库拣货': '\/pages\/tasks\/tasks'/) assert.match(module, /pages\/operation\/operation\?module=\$\{encodeURIComponent\(this\.moduleKey\)\}&name=/) assert.match(history, /作业记录/) assert.match(profile, /个人中心/) assert.match(profile, /signOut\(\)/) assert.match(profile, /class="logout-dialog-card"/) assert.doesNotMatch([home, module, history, profile].join('\n'), /BottomNav/) }) test('home exposes only the two confirmed operations and keeps hidden definitions', async () => { const data = await read('../data/demo-data.js') const { MODULES } = await import('../data/demo-data.js') for (const text of ['入库', '出库', '库存', '查询', '出库拣货']) assert.match(data, new RegExp(text)) assert.match(data, /key: 'inbound'[\s\S]*?name: '一件代发'[\s\S]*?available: true, visible: true/) assert.match(data, /key: 'outbound'[\s\S]*?name: '出库拣货'[\s\S]*?available: true, visible: true/) assert.match(await read('../pages/home/home.vue'), /operations: module\.operations\.filter\(\(operation\) => operation\.visible\)/) assert.deepEqual(MODULES.flatMap((module) => module.operations .filter((operation) => operation.visible) .map((operation) => `${module.key}:${operation.name}`)), [ 'inbound:一件代发', 'outbound:出库拣货' ]) const locationQuery = MODULES.find((module) => module.key === 'query')?.operations .find((operation) => operation.name === '库位查询') assert.ok(locationQuery) assert.ok(!locationQuery.visible) assert.ok(!locationQuery.available) assert.ok(MODULES.flatMap((module) => module.operations).some((operation) => operation.name === '到货收货' && !operation.visible)) for (const boxNo of ['CBX-20260709-001', 'CBX-20260709-002', 'CBX-20260709-003']) assert.match(data, new RegExp(boxNo)) }) test('first picking box provides the M001 through M010 scanner demo', async () => { const { DEMO_PICKING_ITEMS, createBoxes } = await import('../data/demo-data.js') assert.equal(DEMO_PICKING_ITEMS.length, 10) assert.deepEqual(DEMO_PICKING_ITEMS.map((item) => item.sku), Array.from({ length: 10 }, (_, index) => `M${String(index + 1).padStart(3, '0')}`)) assert.equal(new Set(DEMO_PICKING_ITEMS.map((item) => item.id)).size, 10) assert.ok(DEMO_PICKING_ITEMS.every((item) => item.qty === 1 && item.unit === '件')) assert.deepEqual(createBoxes()[0].items.map((item) => item.sku), DEMO_PICKING_ITEMS.map((item) => item.sku)) }) test('non-picking operations remain complete static task pages', async () => { const [data, page] = await Promise.all([read('../data/demo-data.js'), read('../pages/operation/operation.vue')]) for (const text of ['到货收货', '商品上架', '入库质检', '出库复核', '装箱交接', '库存盘点', '商品移库', '库存调整', '商品查询', '库位查询', '任务查询']) assert.match(data, new RegExp(text)) for (const text of ['静态原型', '支持硬件扫描头连续扫描', '任务列表', '待处理', '进行中', '已完成']) assert.match(page, new RegExp(text)) }) test('picking page keeps scan, draft and risky transition contracts', async () => { const [page, scanner] = await Promise.all([read('../pages/picking/picking.vue'), read('../utils/scan-broadcast.js')]) for (const text of ['确认拣出', '部分拣货', '跳拣', '提交拣货完成', '去拍照', '最近扫码']) assert.match(page, new RegExp(text)) assert.match(page, /saveCurrentBoxDraft\(\)[\s\S]*?this\.boxDrafts =/) assert.match(page, /loadBoxState\(index\)[\s\S]*?const draft = this\.boxDrafts\[index\]/) assert.match(page, /requestBoxSwitch\(index\)[\s\S]*?this\.openOverlay\('switchBoxConfirm'\)/) assert.match(page, /submitPicking\(\)[\s\S]*?this\.openOverlay\('submitConfirm'\)/) assert.match(page, /submitStatusHint\(\)[\s\S]*?提交时需选择原因/) assert.match(page, /onBackPress\(\)/) assert.doesNotMatch(page, /scanFocused|restoreScanFocus|hideKeyboard|@confirm="handleScan"/) assert.match(page, /= 0 && processScanEnd > processScanStart) assert.doesNotMatch(page.slice(processScanStart, processScanEnd), /this\.scanValue = ''/) assert.match(scanner, /android\.intent\.action\.SCANRESULT/) assert.match(scanner, /SCAN_BROADCAST_EXTRA = 'value'/) assert.match(scanner, /registerReceiver\(receiver, filter\)/) assert.match(scanner, /Context\.RECEIVER_EXPORTED/) assert.match(scanner, /unregisterReceiver\(receiver\)/) assert.match(page, //) assert.match(page, /leavePicking\(\)[\s\S]*?navigateBackOr\('\/pages\/home\/home'\)/) }) test('picking cabinet selector synchronizes all ready server tasks by stable task id', async () => { const page = await read('../pages/picking/picking.vue') assert.match(page, /listLoadingOrders/) assert.match(page, /async onShow\(\)[\s\S]*?await this\.refreshAvailableBoxes\(\)[\s\S]*?await this\.syncRemoteTask\(\)/) assert.match(page, /async refreshAvailableBoxes\(\)[\s\S]*?listLoadingOrders\(\{ page: 1, size: 50, warehouseId: session\.currentUser\?\.warehouseId \}\)/) assert.match(page, /isSelectableTask\(task\)[\s\S]*?recordStatus === 'ACTIVE'[\s\S]*?\['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW'\]/) assert.match(page, /existingByTaskId[\s\S]*?recordByTaskId[\s\S]*?orderedTaskIds[\s\S]*?this\.boxes = nextBoxes/) assert.match(page, /selectedTaskId[\s\S]*?findIndex\(\(box\) => String\(box\.taskId \|\| ''\) === selectedTaskId\)/) assert.match(page, /selectBox\(index\)[\s\S]*?this\.remoteTaskId = this\.currentBox\.taskId \|\| null[\s\S]*?this\.resetEvidenceState\(\)[\s\S]*?this\.syncRemoteTask\(\)/) }) test('navigation returns to the real source and prevents duplicate utility pages', async () => { const [navigation, header, modulePage, operation, picking] = await Promise.all([ read('../utils/navigation.js'), read('../components/PortalHeader.vue'), read('../pages/module/module.vue'), read('../pages/operation/operation.vue'), read('../pages/picking/picking.vue') ]) assert.match(navigation, /pages\.length > 1[\s\S]*?uni\.navigateBack/) assert.match(navigation, /uni\.reLaunch\(\{ url: fallbackUrl \}\)/) assert.match(navigation, /getCurrentRoute\(\) === route/) assert.match(header, /aria-label="返回上一页"/) assert.match(header, /navigateBackOr\(this\.fallbackUrl\)/) assert.match(header, /navigateToUnique\('\/pages\/profile\/profile'\)/) assert.match(header, /@change="handleMenuChange"/) assert.match(header, /this\.\$emit\('menu-change', Boolean\(event\.show\)\)/) assert.doesNotMatch(header, /goHome\(\)/) assert.doesNotMatch([modulePage, operation].join('\n'), /class="inline-back"/) assert.match(operation, /:fallback-url="`\/pages\/module\/module\?key=\$\{moduleKey\}`"/) assert.match(picking, /navigateBackOr\('\/pages\/home\/home'\)/) }) test('home consumes Android Back while its utility menu is open', async () => { const [home, profile, header] = await Promise.all([ read('../pages/home/home.vue'), read('../pages/profile/profile.vue'), read('../components/PortalHeader.vue') ]) assert.match(home, /@menu-change="menuOpen = \$event"/) assert.doesNotMatch(home, /:show-profile="false"/) assert.match(home, /onBackPress\(\)[\s\S]*?if \(!this\.menuOpen\) return false[\s\S]*?this\.\$refs\.portalHeader\.closeMenu\(\)[\s\S]*?return true/) assert.match(profile, /:show-profile="false"/) assert.match(profile, /openLogout\(\) \{ this\.logoutOpen = true; this\.\$refs\.logoutPopup\.open\(\) \}/) assert.match(profile, /closeLogout\(\) \{ this\.logoutOpen = false; this\.\$refs\.logoutPopup\.close\(\) \}/) assert.match(header, /v-if="showProfile" class="avatar-button"/) assert.match(header, /class="portal-header-spacer"/) }) test('business dialogs use uni-ui popup components consistently', async () => { const [popupHeader, header, profile, picking] = await Promise.all([ read('../components/PopupHeader.vue'), read('../components/PortalHeader.vue'), read('../pages/profile/profile.vue'), read('../pages/picking/picking.vue') ]) assert.match(popupHeader, /type="closeempty"/) assert.match(popupHeader, /aria-label="`关闭\$\{title\}`"/) assert.match(popupHeader, /@tap="\$emit\('close'\)"/) assert.match(header, /作业记录<|>个人中心]*:is-mask-click="true"[^>]*@change="handleOverlayChange\\('${name}', \\$event\\)"`)) } assert.equal((picking.match(/ { const [header, page] = await Promise.all([read('../components/PortalHeader.vue'), read('../pages/scanner-test/scanner-test.vue')]) assert.doesNotMatch(header, /广播扫码测试|goScannerTest/) for (const text of ['接收器已启动', '最近收到', '等待扫描', 'Action', 'Extra 标签', '接收记录']) assert.match(page, new RegExp(text)) assert.match(page, /startScanBroadcast/) assert.match(page, /onHide\(\) \{ this\.stopScanner\(\) \}/) assert.match(page, /onUnload\(\) \{ this\.stopScanner\(\) \}/) assert.match(page, /this\.history = \[\{ id:[\s\S]*?\.slice\(0, 20\)/) }) test('picking scan results use the shared success and error audio feedback', async () => { const [audio, picking] = await Promise.all([ read('../utils/audio-feedback.js'), read('../pages/picking/picking.vue') ]) assert.match(audio, /success: '\/static\/mp3\/success\.mp3'/) assert.match(audio, /error: '\/static\/mp3\/fail\.mp3'/) assert.match(audio, /uni\.createInnerAudioContext\(\)/) assert.match(audio, /export function playSuccessSound\(\)/) assert.match(audio, /export function playErrorSound\(\)/) assert.match(picking, /import \{ playErrorSound, playSuccessSound \} from '@\/utils\/audio-feedback\.js'/) assert.match(picking, /result\.type === 'unknown'[\s\S]*?playErrorSound\(\)/) assert.match(picking, /await this\.pickScannedBox\(result\.item, value\)[\s\S]*?if \(submitted\) playSuccessSound\(\)/) }) test('reusable scan target input separates field selection from page-level broadcast handling', async () => { const [component, picking] = await Promise.all([read('../components/ScanTargetInput.vue'), read('../pages/picking/picking.vue')]) assert.match(component, /emits: \['activate', 'update:modelValue', 'submit'\]/) assert.match(component, /\$emit\('activate', name\)/) assert.match(component, /scan-target-input-active/) assert.match(component, / { const [store, session, page] = await Promise.all([read('../stores/picking.js'), read('../stores/session.js'), read('../pages/picking/picking.vue')]) assert.match(store, /getPickingSession/) assert.match(store, /savePickingSession/) assert.match(store, /resetPickingSession/) assert.match(session, /signOut\(\)[\s\S]*?resetPickingSession\(\)/) assert.match(page, /onUnload\(\) \{ this\.stopHardwareScanner\(\); this\.persistSession\(\)/) }) test('shared visual contracts preserve accessible PDA targets', async () => { const [app, styles, picking, tasks] = await Promise.all([read('../App.vue'), read('../styles/pda.scss'), read('../pages/picking/picking.vue'), read('../pages/tasks/tasks.vue')]) assert.match(app, /@import '@\/styles\/pda\.scss'/) assert.doesNotMatch(styles, /<\/?style\b/) for (const className of ['pick-btn', 'partial-quick-btn']) assert.match(styles, new RegExp(`\\.${className}\\s*\\{[\\s\\S]*?height:\\s*48px;`)) assert.match(styles, /\.pick-item-actions\s*\{[^}]*grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\);[^}]*width:\s*100%;/) assert.match(styles, /\.pick-btn\s*\{[^}]*width:\s*100%;[^}]*margin:\s*0;[^}]*align-items:\s*center;[^}]*justify-content:\s*center;/) assert.match(styles, /\.pick-btn::after\s*\{[^}]*border:\s*0;/) assert.match(styles, /\.processed-actions\s*\{[^}]*grid-template-columns:\s*1fr;/) assert.match(styles, /\.pick-btn-undo\s*\{[^}]*border-color:\s*#efb7b7;[^}]*color:\s*#b42323;[^}]*background:\s*#fff5f5;/) assert.doesNotMatch(styles, /\.scan-btn\s*\{/) assert.match(styles, /\.modal-cancel-btn,[\s\S]*?height:\s*48px;/) assert.match(styles, /\.modal-sheet\s*\{[\s\S]*?padding:\s*20px 18px 18px;/) assert.doesNotMatch(styles, /\.modal-sheet\s*\{[\s\S]*?padding:[^;]*env\(/) assert.match(styles, /\.submit-decision-modal\s*\{[^}]*padding:\s*18px 16px 16px;/) assert.match(styles, /\.evidence-card\s*\{[^}]*margin-bottom:\s*8px;/) assert.match(styles, /\.evidence-card:last-child\s*\{[^}]*margin-bottom:\s*0;/) assert.match(tasks, /\.task-card\s*\{[^}]*margin:\s*0 0 12px;/) assert.match(tasks, /\.task-card:last-child\s*\{[^}]*margin-bottom:\s*0;/) assert.match(styles, /\.work-content\s*\{[^}]*padding:\s*1px 0 104px;/) assert.match(picking, /class="box-select-summary"/) assert.match(styles, /\.box-select-summary\s*\{[^}]*min-width:\s*0;[^}]*overflow-wrap:\s*anywhere;[^}]*white-space:\s*normal;/) assert.doesNotMatch(styles, /\.box-select-summary\s*\{[^}]*(text-overflow:\s*ellipsis|white-space:\s*nowrap)/) assert.match(styles, /\.box-dropdown\s*\{[^}]*top:\s*calc\(100% \+ 5px\);/) assert.match(styles, /\.submit-section\s*\{[\s\S]*?bottom:\s*8px;[\s\S]*?width:\s*calc\(100% - 20px\);/) assert.match(styles, /\.submit-btn\s*\{[\s\S]*?flex:\s*1;[\s\S]*?height:\s*56px;/) assert.match(styles, /\.home-group-mark\s*\{[^}]*margin-right:\s*7px;/) assert.match(styles, /\.home-operation-grid\s*\{[^}]*grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\);/) assert.match(styles, /\.home-operation\s*\{[^}]*width:\s*100%;[^}]*min-height:\s*78px;[^}]*align-self:\s*stretch;/) assert.match(styles, /\.home-operation-name\s*\{[^}]*min-height:\s*30px;[^}]*white-space:\s*normal;/) assert.doesNotMatch(styles, /\.home-operation-name\s*\{[^}]*(text-overflow:\s*ellipsis|-webkit-line-clamp|white-space:\s*nowrap)/) assert.match(styles, /\.home-operation-meta\s*\{[^}]*border-radius:\s*7px;[^}]*white-space:\s*nowrap;/) assert.match(styles, /@media \(max-width:\s*340px\)\s*\{[\s\S]*?\.home-operation-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\);/) assert.match(styles, /\.header-menu-item\s*\{[^}]*width:\s*calc\(100% - 32px\);[^}]*margin:\s*0 16px;/) assert.match(styles, /\.header-menu-close\s*\{[^}]*width:\s*calc\(100% - 32px\);/) assert.match(picking, /aria-modal="true"/) assert.match(picking, /role="status"/) assert.match(picking, /transform:\s*'scaleX\('/) })