import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "node:test"; const appRoot = fileURLToPath(new URL("..", import.meta.url)); test("PDA 的 H5、App 和小程序统一使用 Sylvan 品牌标识", async () => { const [config, loginPage, registerPage, aboutPage, indexHtml, fullLogo, markPng] = await Promise.all([ readFile(path.join(appRoot, "src/config.js"), "utf8"), readFile(path.join(appRoot, "src/pages/login.vue"), "utf8"), readFile(path.join(appRoot, "src/pages/register.vue"), "utf8"), readFile(path.join(appRoot, "src/pages/mine/about/index.vue"), "utf8"), readFile(path.join(appRoot, "index.html"), "utf8"), readFile(path.join(appRoot, "src/static/sylvan-logo.svg"), "utf8"), readFile(path.join(appRoot, "src/static/sylvan-mark.png")), ]); assert.match(config, /logo:\s*["']\/static\/sylvan-logo\.png["']/); assert.match(loginPage, /globalConfig\.appInfo\.logo/); assert.match(registerPage, /globalConfig\.appInfo\.logo/); assert.match(aboutPage, /src=["']\/static\/sylvan-logo\.png["']/); assert.match(indexHtml, /href=["']\/static\/favicon\.ico["']/); assert.match(fullLogo, /viewBox="0 0 516\.1 194\.52"/); assert.equal(markPng.subarray(1, 4).toString("ascii"), "PNG"); assert.doesNotMatch(`${config}\n${aboutPage}`, /\/static\/logo(?:200)?\.png/); assert.doesNotMatch(fullLogo, / { for (const [relativePath, contracts] of Object.entries(apiContracts)) { await t.test(relativePath, async () => { const source = await readFile(path.join(appRoot, relativePath), "utf8"); for (const [functionName, url, method] of contracts) { const functionSource = sourceForFunction(source, functionName); const normalizedFunctionSource = functionSource.replaceAll("'", '"'); const normalizedUrl = url.replaceAll("'", '"'); assert.match( normalizedFunctionSource, new RegExp(`url:\\s*${escapeRegExp(normalizedUrl)}`), ); assert.match(functionSource, new RegExp(`method:\\s*["']${method}["']`)); } }); } }); test("扫码值只去除首尾空白并拒绝空值", async () => { const { normalizeScanValue } = await importSource("src/utils/palletScan.js"); assert.equal(normalizeScanValue(" QR-001\n"), "QR-001"); assert.throws(() => normalizeScanValue(" \n\t "), /二维码不能为空/); }); test("同一扫码值只在短时窗口内被识别为重复", async () => { const { isDuplicateScan } = await importSource("src/utils/palletScan.js"); assert.equal(isDuplicateScan("QR-001", "QR-001", 1200, 1000, 500), true); assert.equal(isDuplicateScan("QR-001", "QR-002", 1200, 1000, 500), false); assert.equal(isDuplicateScan("QR-001", "QR-001", 1801, 1000, 500), false); }); test("幂等键优先使用 randomUUID,回退时使用 UUID v4 安全随机字节", async () => { const { createIdempotencyKey } = await importSource("src/utils/palletScan.js"); const expected = "12345678-1234-4123-8123-123456789abc"; assert.equal(createIdempotencyKey({ randomUUID: () => expected }), expected); assert.equal( createIdempotencyKey({ getRandomValues(bytes) { bytes.fill(0); return bytes; }, }), "00000000-0000-4000-8000-000000000000", ); }); test("PalletScanInput 使用 HID 回车提交并由 loading 锁定输入", async () => { const source = await readFile( path.join(appRoot, "src/components/business/PalletScanInput.vue"), "utf8", ); assert.match(source, /@confirm=["']handleSubmit["']/); assert.match(source, /normalizeScanValue/); assert.match(source, /isDuplicateScan/); assert.match(source, /emit\(["']scan["']/); assert.match(source, /emit\(["']invalid["']/); assert.match(source, /emit\(["']duplicate["']/); assert.match(source, /loading/); assert.doesNotMatch(source, /uni\.scanCode|camera|摄像头/); }); test("PDA 路由和工作台入口覆盖五类现场业务", async () => { const pages = JSON.parse( await readFile(path.join(appRoot, "src/pages.json"), "utf8"), ); const pagePaths = new Set(pages.pages.map((page) => page.path)); for (const pagePath of [ "pages/business/inbound/index", "pages/business/filling/index", "pages/business/shipment/index", "pages/business/usage/index", "pages/business/return/index", ]) { assert.equal(pagePaths.has(pagePath), true, `${pagePath} 路由缺失`); } const [workSource, menuSource] = await Promise.all([ readFile(path.join(appRoot, "src/pages/work/index.vue"), "utf8"), readFile(path.join(appRoot, "src/config/businessMenu.js"), "utf8"), ]); assert.match(workSource, /hasPermi\(entry\.permission\)/); assert.match(menuSource, /business:inbound:list/); assert.match(menuSource, /business:filling:list/); assert.match(menuSource, /business:shipment:list/); assert.match(menuSource, /business:usage:list/); assert.match(menuSource, /business:return:list/); assert.match(menuSource, /pages\/business\/inbound\/index/); assert.match(menuSource, /pages\/business\/filling\/index/); assert.match(menuSource, /pages\/business\/shipment\/index/); assert.match(menuSource, /pages\/business\/usage\/index/); assert.match(menuSource, /pages\/business\/return\/index/); }); test("首页展示真实运营驾驶舱,不保留模板入口和虚假统计", async () => { const source = await readFile(path.join(appRoot, "src/pages/index.vue"), "utf8"); assert.match(source, /listScanExceptions/); assert.match(source, /最近扫码异常/); assert.match(source, /TestBatchBanner/); assert.doesNotMatch( source, /快捷入口|新闻|计划|请假申请|周报|工作效率|新通知|待办任务|85%/, ); }); test("工作台只展示真实业务入口并使用后端列表权限", async () => { const [workSource, menuModule] = await Promise.all([ readFile(path.join(appRoot, "src/pages/work/index.vue"), "utf8"), importSource("src/config/businessMenu.js"), ]); assert.deepEqual( menuModule.businessMenuEntries.map(({ title, permission, path: entryPath }) => ({ title, permission, path: entryPath, })), [ { title: "仓库入库", permission: "business:inbound:list", path: "/pages/business/inbound/index", }, { title: "工厂装填", permission: "business:filling:list", path: "/pages/business/filling/index", }, { title: "发货出库", permission: "business:shipment:list", path: "/pages/business/shipment/index", }, { title: "客户现场使用", permission: "business:usage:list", path: "/pages/business/usage/index", }, { title: "退回仓库", permission: "business:return:list", path: "/pages/business/return/index", }, ], ); assert.match(workSource, /businessMenuEntries/); assert.match(workSource, /hasPermi\(entry\.permission\)/); assert.match(workSource, /navigateTo\(entry\.path\)/); assert.doesNotMatch(workSource, /AI 生产力|智能对话|图像生成|系统管理|handleBuilding|模块建设中/); }); test("运营驾驶舱 API 对接现有扫码异常接口", async () => { const source = await readFile(path.join(appRoot, "src/api/business/dashboard.js"), "utf8"); assert.match(source, /export function listScanExceptions\(/); assert.match(source, /url:\s*["']\/business\/dashboard\/scan-exceptions["']/); assert.match(source, /method:\s*["']get["']/); }); test("APP 页面标题与后端业务菜单名称一致", async () => { const [pages, fillingSource] = await Promise.all([ JSON.parse(await readFile(path.join(appRoot, "src/pages.json"), "utf8")), readFile(path.join(appRoot, "src/pages/business/filling/index.vue"), "utf8"), ]); const fillingPage = pages.pages.find((page) => page.path === "pages/business/filling/index"); assert.equal(fillingPage?.style?.navigationBarTitleText, "工厂装填"); assert.match(fillingSource, />工厂装填 { const requestSource = await readFile( path.join(appRoot, "src/utils/request.js"), "utf8", ); assert.match(requestSource, /X-Test-Batch-Id/); assert.match(requestSource, /TEST_BATCH_UNAVAILABLE/); assert.match(requestSource, /uni\.\$emit\(["']test-batch-unavailable["']/); assert.match(requestSource, /reLaunch\(\{\s*url:\s*["']\/pages\/work\/index["']/); for (const page of ["inbound", "filling", "shipment", "usage", "return"]) { const source = await readFile( path.join(appRoot, `src/pages/business/${page}/index.vue`), "utf8", ); assert.match(source, /TestBatchBanner/); } }); test("PDA 不再暴露入库和发货草稿物理删除", async () => { for (const file of ["src/api/business/inbound.js", "src/api/business/shipment.js"]) { const source = await readFile(path.join(appRoot, file), "utf8"); assert.doesNotMatch(source, /delInbound|delShipment|method:\s*["']delete["']/); } for (const page of ["inbound", "shipment"]) { const source = await readFile( path.join(appRoot, `src/pages/business/${page}/index.vue`), "utf8", ); assert.doesNotMatch(source, /deleteDraft|deleteLoading|business:(?:inbound|shipment):remove/); } }); test("登录页只预填账号,不把管理员密码写入源码", async () => { const source = await readFile(path.join(appRoot, "src/pages/login.vue"), "utf8"); assert.match(source, /username:\s*["']admin["']/); assert.match(source, /password:\s*["']["']/); assert.doesNotMatch(source, /password:\s*["'][^"']+["']/); }); test("PDA 后端地址支持 VITE_APP_BASE_API 并保留本地默认值", async () => { const source = await readFile(path.join(appRoot, "src/config.js"), "utf8"); assert.match(source, /import\.meta\.env\.VITE_APP_BASE_API/); assert.match(source, /VITE_APP_BASE_API\s*\|\|\s*["']http:\/\/localhost:9099["']/); }); test("入库页面遵循 UNSTOCKED 扫码、草稿和幂等确认契约", async () => { const source = await readFile( path.join(appRoot, "src/pages/business/inbound/index.vue"), "utf8", ); assert.match(source, /PalletScanInput/); assert.match(source, /scanInboundPallet/); assert.match(source, /UNSTOCKED/); assert.match(source, /warehouseId/); assert.match(source, /palletIds/); assert.match(source, /getInbound\(/); assert.match(source, /updateInbound/); assert.match(source, /confirmInbound/); assert.match(source, /idempotencyKey/); assert.match(source, /business:inbound:(?:query|add|edit|confirm)/); }); test("发货页面遵循来源仓库、目标现场和整单确认契约", async () => { const source = await readFile( path.join(appRoot, "src/pages/business/shipment/index.vue"), "utf8", ); assert.match(source, /PalletScanInput/); assert.match(source, /scanShipmentPallet/); assert.match(source, /FILLED_IN_WAREHOUSE/); assert.match(source, /remainingPackages/); assert.match(source, /sourceWarehouseId/); assert.match(source, /targetSiteId/); assert.match(source, /palletIds/); assert.match(source, /getShipment\(/); assert.match(source, /updateShipment/); assert.match(source, /confirmShipment/); assert.match(source, /idempotencyKey/); assert.match(source, /business:shipment:(?:query|add|edit|confirm)/); }); test("装填页面遵循空托盘状态和 FillingCreateModel 字段契约", async () => { const source = await readFile( path.join(appRoot, "src/pages/business/filling/index.vue"), "utf8", ); assert.match(source, /PalletScanInput/); assert.match(source, /scanFillingPallet/); assert.match(source, /EMPTY_IN_WAREHOUSE/); for (const field of [ "qrToken", "warehouseId", "strainId", "batchNo", "packageSpec", "initialPackages", "idempotencyKey", ]) { assert.match(source, new RegExp(field)); } assert.match(source, /addFilling/); assert.match(source, /business:filling:(?:query|add)/); }); test("现场使用页面遵循产线绑定、余量和使用结果契约", async () => { const source = await readFile( path.join(appRoot, "src/pages/business/usage/index.vue"), "utf8", ); assert.match(source, /PalletScanInput/); assert.match(source, /scanUsagePallet/); assert.match(source, /AT_SITE_UNUSED/); assert.match(source, /AT_SITE_PARTIAL/); assert.match(source, /currentLineId/); assert.match(source, /getUsageLineOptions/); assert.match(source, /usedPackages/); assert.match(source, /remainingPackages/); assert.match(source, /flowState/); assert.match(source, /addUsage/); assert.match(source, /idempotencyKey/); assert.match(source, /business:usage:(?:query|add)/); }); test("退回页面实现申请与确认两阶段的集合和幂等契约", async () => { const source = await readFile( path.join(appRoot, "src/pages/business/return/index.vue"), "utf8", ); assert.match(source, /PalletScanInput/); assert.match(source, /scanReturnPallet/); assert.match(source, /mode:\s*["']request["']/); assert.match(source, /mode:\s*["']confirm["']/); assert.match(source, /sourceSiteId/); assert.match(source, /targetWarehouseId/); assert.match(source, /palletIds/); assert.match(source, /scannedPalletIds/); assert.match(source, /requestReturn/); assert.match(source, /getReturn\(/); assert.match(source, /confirmReturn/); assert.match(source, /sameIdSet/); assert.match(source, /REQUESTED/); assert.match(source, /requestIdempotencyKey/); assert.match(source, /confirmIdempotencyKey/); assert.match(source, /business:return:(?:query|request|confirm)/); }); function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } async function importSource(relativePath) { const source = await readFile(path.join(appRoot, relativePath), "utf8"); return import(`data:text/javascript;charset=utf-8,${encodeURIComponent(source)}`); }