#!/usr/bin/env node /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import assert from 'node:assert/strict' import { access, readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' const scriptPath = fileURLToPath(import.meta.url) const appRoot = path.resolve(path.dirname(scriptPath), '..') const inventoryPath = path.resolve(appRoot, '../../modern-api/generated/ui-inventory.json') const forbiddenProductionSourceFiles = [ 'src/views/ComponentsView.vue', 'src/views/PatternsView.vue', 'src/views/DeliveryView.vue', 'src/views/ParityView.vue', 'src/views/InventoryView.vue', 'src/views/LegacyRedirectView.vue', 'src/components/erp/CodePreview.vue', 'src/data/elementPlusInventory.js', 'src/data/erpComponentDocs.js', 'src/data/inventorySnapshot.ts', 'src/data/previewVerification.ts', 'src/data/parityVerification.ts', 'src/types/lab-data.d.ts' ] async function source(file) { return readFile(path.join(appRoot, file), 'utf8') } async function fileExists(file) { try { await access(path.join(appRoot, file)) return true } catch { return false } } async function sourceFiles(dir) { const entries = await readdir(dir, { withFileTypes: true }) const files = await Promise.all( entries.map(async (entry) => { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { return sourceFiles(fullPath) } if (/\.(ts|vue)$/.test(entry.name)) { return [fullPath] } return [] }) ) return files.flat() } const appVue = await source('src/App.vue') const routerTs = await source('src/router/index.ts') const businessPageVue = await source('src/views/BusinessPageView.vue') const shellVue = await source('src/components/erp/ErpAppShell.vue') const dashboardVue = await source('src/views/DashboardView.vue') const moduleVue = await source('src/views/ModuleWorkspaceView.vue') const businessCenterVue = await source('src/views/BusinessCenterView.vue') const productAdminVue = await source('src/views/ProductAdminView.vue') const orderAdminVue = await source('src/views/OrderAdminView.vue') const partyAdminVue = await source('src/views/PartyAdminView.vue') const accountingAdminVue = await source('src/views/AccountingAdminView.vue') const inventoryAdminVue = await source('src/views/InventoryAdminView.vue') const manufacturingAdminVue = await source('src/views/ManufacturingAdminView.vue') const humanResAdminVue = await source('src/views/HumanResAdminView.vue') const salesAdminVue = await source('src/views/SalesAdminView.vue') const procurementAdminVue = await source('src/views/ProcurementAdminView.vue') const scrumAdminVue = await source('src/views/ScrumAdminView.vue') const operationsAdminVue = await source('src/views/OperationsAdminView.vue') const contentAdminVue = await source('src/views/ContentAdminView.vue') const marketingAdminVue = await source('src/views/MarketingAdminView.vue') const commerceAdminVue = await source('src/views/CommerceAdminView.vue') const posAdminVue = await source('src/views/PosAdminView.vue') const marketplaceAdminVue = await source('src/views/MarketplaceAdminView.vue') const analyticsAdminVue = await source('src/views/AnalyticsAdminView.vue') const extensionAdminVue = await source('src/views/ExtensionAdminView.vue') const systemVue = await source('src/views/SystemToolsView.vue') const securityAdminVue = await source('src/views/SecurityAdminView.vue') const systemOperationsVue = await source('src/views/SystemOperationsView.vue') const loginVue = await source('src/views/LoginView.vue') const modernCss = await source('src/styles/modern.css') const rendererVue = await source('src/components/erp/ErpPageRenderer.vue') const modernNavigationTs = await source('src/utils/modernNavigation.ts') const displayTs = await source('src/utils/display.ts') const searchFormVue = await source('src/components/erp/ErpSearchForm.vue') const entityFormVue = await source('src/components/erp/ErpEntityForm.vue') const apiTs = await source('src/services/api.ts') const apiTypes = await source('src/types/api.ts') const previewScript = await source('scripts/verify-preview.mjs') const parityScript = await source('scripts/verify-parity.mjs') const captureScreenshotsScript = await source('scripts/capture-admin-screenshots.mjs') const partyWorkspaceVue = await source('src/components/erp/ErpPartyWorkspace.vue') const portalAdminWorkspaceVue = await source('src/components/erp/ErpPortalAdminWorkspace.vue') const gatewayAdminWorkspaceVue = await source('src/components/erp/ErpGatewayAdminWorkspace.vue') const extensionOperationsWorkspaceVue = await source('src/components/erp/ErpExtensionOperationsWorkspace.vue') const ebayOperationsWorkspaceVue = await source('src/components/erp/ErpEbayOperationsWorkspace.vue') const businessIntelligenceWorkspaceVue = await source('src/components/erp/ErpBusinessIntelligenceWorkspace.vue') const birtReportingWorkspaceVue = await source('src/components/erp/ErpBirtReportingWorkspace.vue') const salesAutomationWorkspaceVue = await source('src/components/erp/ErpSalesAutomationWorkspace.vue') const procurementWorkspaceVue = await source('src/components/erp/ErpProcurementWorkspace.vue') const scrumWorkspaceVue = await source('src/components/erp/ErpScrumWorkspace.vue') const humanResWorkspaceVue = await source('src/components/erp/ErpHumanResWorkspace.vue') const workManagementWorkspaceVue = await source('src/components/erp/ErpWorkManagementWorkspace.vue') const manufacturingWorkspaceVue = await source('src/components/erp/ErpManufacturingWorkspace.vue') const marketingWorkspaceVue = await source('src/components/erp/ErpMarketingWorkspace.vue') const reportWorkspaceVue = await source('src/components/erp/ErpReportWorkspace.vue') const systemWorkspaceVue = await source('src/components/erp/ErpSystemWorkspace.vue') const contentWorkspaceVue = await source('src/components/erp/ErpContentWorkspace.vue') const assetMaintenanceWorkspaceVue = await source('src/components/erp/ErpAssetMaintenanceWorkspace.vue') const financeOperationsWorkspaceVue = await source('src/components/erp/ErpFinanceOperationsWorkspace.vue') const financeWorkspaceVue = await source('src/components/erp/ErpFinanceWorkspace.vue') const mediaWorkspaceVue = await source('src/components/erp/ErpMediaWorkspace.vue') const inventoryWorkspaceVue = await source('src/components/erp/ErpInventoryWorkspace.vue') const orderWorkspaceVue = await source('src/components/erp/ErpOrderWorkspace.vue') const fulfillmentWorkspaceVue = await source('src/components/erp/ErpFulfillmentWorkspace.vue') const posWorkspaceVue = await source('src/components/erp/ErpPosWorkspace.vue') const returnWorkspaceVue = await source('src/components/erp/ErpReturnWorkspace.vue') const catalogWorkspaceVue = await source('src/components/erp/ErpCatalogWorkspace.vue') const commerceSurfaceVue = await source('src/components/erp/ErpCommerceSurface.vue') const adapterBlockVue = await source('src/components/erp/ErpAdapterBlock.vue') const domainAdminVue = await source('src/components/erp/ErpDomainAdminView.vue') const dataTableVue = await source('src/components/erp/ErpDataTable.vue') const uploadVue = await source('src/components/erp/ErpUpload.vue') const drawerVue = await source('src/components/erp/ErpDrawer.vue') const moduleCatalog = await source('src/data/moduleCatalog.ts') const packageJson = await source('package.json') const inventory = JSON.parse(await readFile(inventoryPath, 'utf8')) const generatedPageIds = new Set(Object.keys(inventory.pageDefinitions || {})) const lingeringSourceFiles = [] for (const file of forbiddenProductionSourceFiles) { if (await fileExists(file)) lingeringSourceFiles.push(file) } assert.deepEqual( lingeringSourceFiles, [], 'production administrator source must not ship component galleries, delivery/parity workbenches, inventory snapshots, or legacy bridge views' ) assert.doesNotMatch( modernCss, /\.delivery-|\.parity-|\.modern-component-|\.modern-code|\.modern-usage-|\.modern-playbook-|\.modern-wrapper-demo/, 'production administrator stylesheet must not carry component gallery, delivery, parity, or code-preview styles' ) const hardcodedPageIds = new Map() for (const file of await sourceFiles(path.join(appRoot, 'src'))) { const relativeFile = path.relative(appRoot, file) const content = await readFile(file, 'utf8') for (const match of content.matchAll(/(?:#\/pages\/|pageId:\s*['"`])([A-Za-z0-9_-]+__[A-Za-z0-9_.-]+)/g)) { const pageId = match[1] const files = hardcodedPageIds.get(pageId) || new Set() files.add(relativeFile) hardcodedPageIds.set(pageId, files) } } function moduleConfigSource(moduleId, nextModuleId) { const start = moduleCatalog.indexOf(`id: '${moduleId}'`) assert.notEqual(start, -1, `module catalog should include ${moduleId}`) const end = nextModuleId ? moduleCatalog.indexOf(`id: '${nextModuleId}'`, start + 1) : -1 return moduleCatalog.slice(start, end === -1 ? undefined : end) } function escapeRegExp(text) { return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } // Hash deep links keep their leading '#', but the vue-router table stores paths // without it (createWebHashHistory adds the '#'). Normalise a legacy '#/x' anchor // to the router path '/x' so route->view coverage can be asserted against the // central route table in src/router/index.ts. function routerPath(route) { return String(route).replace(/^#/, '') } // Assert the central route table maps a given path to a given view component, // regardless of import style (lazy `() => import('../views/X.vue')` or a direct // reference). Preserves the old `activePath === '#/x' ... return 'view'` intent. function assertRouteMapsToView(route, viewComponent, message) { const path = routerPath(route) const pattern = new RegExp( `path:\\s*'${escapeRegExp(path)}'[\\s\\S]*?${escapeRegExp(viewComponent)}\\.vue` ) assert.match(routerTs, pattern, message) } function assertEntitySource(content, entityName, message) { const directEntityLoad = new RegExp(`getEntityRows\\('${entityName}'`) const wrapperEntitySource = new RegExp(`entityName: '${entityName}'`) assert.ok(directEntityLoad.test(content) || wrapperEntitySource.test(content), message) } const visibleUiLeakageTerms = [ ['component gallery copy', /组件展厅|组件规范|排版规则|全量清单/], ['preview copy', /技术预览|>\s*预览\s*<|title="预览"|预览模式|页面预览/], ['migration copy', /迁移|自动迁移|旧入口|旧 URL|旧页面|历史地址|旧屏幕来源|旧 widget/i], ['parity copy', /业务等价|功能等价|旧新报表比对|逐页比对|真实输出比对/], ['acceptance copy', /验收|待验收|验收清单|业务回归|待业务回归/], ['inventory checklist copy', /页面清单|覆盖率|覆盖台账|待补齐|待输出比对/], ['engineering metadata copy', /页面定义|页面结构|动作契约|控制规则|模板适配|适配器已覆盖|前端已重写/] ] function assertNoVisibleUiLeakage(surfaces) { for (const [fileName, content] of surfaces) { for (const [label, pattern] of visibleUiLeakageTerms) { assert.doesNotMatch( content, pattern, `${fileName} must not expose ${label} in visible administrator product UI` ) } } } const postLoginCoreModuleAnchors = [ { route: '#/orders', activeView: 'order-admin', component: 'OrderAdminView', sourceName: 'OrderAdminView.vue', source: orderAdminVue, labels: ['订单运营台', '订单业务数据'], entities: ['OrderHeader', 'OrderItem', 'Shipment'] }, { route: '#/catalog/products', activeView: 'product-admin', component: 'ProductAdminView', sourceName: 'ProductAdminView.vue', source: productAdminVue, labels: ['商品管理台', '商品业务数据'], entities: ['Product', 'ProductCategory', 'ProductPrice'] }, { route: '#/parties', activeView: 'party-admin', component: 'PartyAdminView', sourceName: 'PartyAdminView.vue', source: partyAdminVue, labels: ['客户与组织管理台', '客户业务数据'], entities: ['Party', 'Person', 'PartyRole'] }, { route: '#/sales', activeView: 'sales-admin', component: 'SalesAdminView', sourceName: 'SalesAdminView.vue', source: salesAdminVue, labels: ['销售管理台', '销售业务数据'], entities: ['SalesOpportunity', 'SalesForecast', 'CommunicationEvent'] }, { route: '#/procurement', activeView: 'procurement-admin', component: 'ProcurementAdminView', sourceName: 'ProcurementAdminView.vue', source: procurementAdminVue, labels: ['采购管理台', '采购业务数据'], entities: ['Requirement', 'SupplierProduct', 'Vendor'] }, { route: '#/accounting', activeView: 'accounting-admin', component: 'AccountingAdminView', sourceName: 'AccountingAdminView.vue', source: accountingAdminVue, labels: ['财务管理台', '财务业务数据'], entities: ['Invoice', 'Payment', 'AcctgTrans'] }, { route: '#/facility', activeView: 'inventory-admin', component: 'InventoryAdminView', sourceName: 'InventoryAdminView.vue', source: inventoryAdminVue, labels: ['库存管理台', '库存业务数据'], entities: ['InventoryItem', 'Facility', 'Shipment'] }, { route: '#/manufacturing', activeView: 'manufacturing-admin', component: 'ManufacturingAdminView', sourceName: 'ManufacturingAdminView.vue', source: manufacturingAdminVue, labels: ['生产管理台', '生产业务数据'], entities: ['WorkEffort', 'Requirement', 'CostComponent'] }, { route: '#/humanres', activeView: 'humanres-admin', component: 'HumanResAdminView', sourceName: 'HumanResAdminView.vue', source: humanResAdminVue, labels: ['人事管理台', '人事业务数据'], entities: ['Person', 'Employment', 'EmplPosition'] } ] const businessPageAnchors = [ 'order__showcart', 'party__NewCustomer', 'accounting__ManualTransaction', 'order__FindRequirements', 'facility__ReceiveInventoryAgainstPurchaseOrder', 'scrum__SprintTask' ] function assertPostLoginProductAnchors() { assert.match(appVue, /` // route (moduleItemIndex(module, `/pages/${page.pageId}`)), and the topbar quick // actions deep-link the same way, so a generated page is reachable when its pageId // is present in the moduleCatalog-driven nav surfaces. for (const pageId of businessPageAnchors) { assert.ok(generatedPageIds.has(pageId), `post-login business page anchor must exist in generated inventory: ${pageId}`) assert.match( shellVue + dashboardVue + businessCenterVue + moduleCatalog, new RegExp(`(?:\\/pages\\/|pageId:\\s*['"\`])${escapeRegExp(pageId)}`), `post-login product UI must link to generated business page anchor: /pages/${pageId}` ) } } const procurementModuleSource = moduleConfigSource('procurement', 'accounting') const missingHardcodedPageIds = [...hardcodedPageIds] .filter(([pageId]) => !generatedPageIds.has(pageId)) .map(([pageId, files]) => ({ pageId, files: [...files].sort() })) .sort((a, b) => a.pageId.localeCompare(b.pageId)) assert.deepEqual(missingHardcodedPageIds, [], 'hardcoded business page IDs must exist in generated OFBiz inventory') assertPostLoginProductAnchors() assertNoVisibleUiLeakage([ ['LoginView.vue', loginVue], ['App.vue', appVue], ['ErpAppShell.vue', shellVue], ['DashboardView.vue', dashboardVue], ['ModuleWorkspaceView.vue', moduleVue], ['BusinessCenterView.vue', businessCenterVue], ['BusinessPageView.vue', businessPageVue], ['SystemToolsView.vue', systemVue], ['SecurityAdminView.vue', securityAdminVue], ['SystemOperationsView.vue', systemOperationsVue], ['ProductAdminView.vue', productAdminVue], ['OrderAdminView.vue', orderAdminVue], ['PartyAdminView.vue', partyAdminVue], ['AccountingAdminView.vue', accountingAdminVue], ['InventoryAdminView.vue', inventoryAdminVue], ['ManufacturingAdminView.vue', manufacturingAdminVue], ['HumanResAdminView.vue', humanResAdminVue], ['SalesAdminView.vue', salesAdminVue], ['ProcurementAdminView.vue', procurementAdminVue], ['ScrumAdminView.vue', scrumAdminVue], ['OperationsAdminView.vue', operationsAdminVue], ['ContentAdminView.vue', contentAdminVue], ['MarketingAdminView.vue', marketingAdminVue], ['CommerceAdminView.vue', commerceAdminVue], ['PosAdminView.vue', posAdminVue], ['MarketplaceAdminView.vue', marketplaceAdminVue], ['AnalyticsAdminView.vue', analyticsAdminVue], ['ExtensionAdminView.vue', extensionAdminVue], ['ErpDomainAdminView.vue', domainAdminVue], ['ErpAdapterBlock.vue', adapterBlockVue], ['ErpDataTable.vue', dataTableVue], ['ErpSearchForm.vue', searchFormVue], ['ErpEntityForm.vue', entityFormVue], ['ErpPageRenderer.vue', rendererVue] ]) for (const text of [ '登录后进入完整 ERP 后台', 'apiLogin', 'targetHash', '订单、商品、客户、财务、库存' ]) { assert.match(loginVue, new RegExp(text), `login door must present a complete OFBiz admin product: ${text}`) } assert.match(apiTs, /\/api\/v1\/login/, 'login service must authenticate inside the modern API webapp session') // The deep link the user requested before authentication is captured from the // active route (returnRoute = `#${route.fullPath}`), surfaced to the login gate as // loginTargetHash, and replayed through the router on successful API session login. assert.match(appVue, /returnRoute\.value\s*=\s*`#\$\{route\.fullPath\}`/, 'app should capture the requested administrator deep link from the active route before login') assert.match(appVue, /loginTargetHash\s*=\s*computed\(\(\)\s*=>\s*returnRoute\.value\)/, 'login gate should resolve the captured deep link as its target hash') assert.match(appVue, /handleLoginSuccess[\s\S]*payload\.targetHash[\s\S]*router\.replace/, 'login should preserve and navigate to the requested administrator deep link after API session login') assert.match(appVue, /:target-hash="loginTargetHash"/, 'login gate should pass the resolved administrator deep link to the login view') assert.match(captureScreenshotsScript, /\/modern\/app\//, 'browser verification should target the deployed modern administrator app') assert.doesNotMatch( loginVue, /inventory\.counts|个业务入口|业务化后台界面/, 'login door must not present the administrator product as a page-count or inventory-backed entry catalog' ) assert.doesNotMatch( appVue, /getSession\(\),\s*\n\s*getNavigation\(\),\s*\n\s*getInventory\(\)/, 'unauthenticated login should not fetch the page inventory before OFBiz session authentication' ) assert.doesNotMatch( loginVue + modernCss, /admin-login__legacy|使用 OFBiz 原登录页|\/webtools\/control\/login/, 'login door must not expose the old login page as product chrome' ) for (const text of [ 'ERP 管理员工作台', '管理员首页', '经营数据', '今日运营', '待办队列', '最近记录', '系统健康与权限', '快速检索', '录入订单', '新建客户', '系统维护', '快捷动作', '当班状态', '可处理范围' ]) { assert.match(dashboardVue, new RegExp(text), `authenticated landing page must be an ERP admin site: ${text}`) } for (const text of [ 'priorityWorkRows', 'workbenchStatusRows', 'continueWorkRows', 'recentDocumentRows', 'businessActionGroups', 'businessSearchRows', 'kpiRows', 'pendingWorkRows', 'erp-kpi', 'erp-row', 'erp-quick' ]) { assert.match(dashboardVue, new RegExp(text), `administrator landing page should include operator work surface: ${text}`) } assert.doesNotMatch( shellVue, /`\$\{module\.navLabel\}入口`|>\s*入口\s*\s*primaryModules\.map/, 'administrator shell should build its core business navigation from the primary module catalog') assert.match( shellVue, /moduleItemIndex\(module,\s*toPath\(moduleLandingPath\(module\)\)\)/, 'administrator shell should route catalog business domains through dedicated modern administrator landing pages' ) assert.doesNotMatch( loginVue + dashboardVue, /Element Plus|个应用|界面方案|原应用清单|ORD-\d+|INV-\d+|PAY-\d+|SHP-\d+|DemoCustomer|value:\s*(?:18|7|12|8|6|5),/, 'administrator workbench must not use fixed demo documents or hardcoded operational counts' ) assert.match( dashboardVue, /recentBusinessEntries|todayOperatingRows|operatingLanes|recentDocumentRows|businessActionGroups|systemHealthRows|businessSearchRows|pendingWorkRows|priorityWorkRows|workbenchStatusRows|businessDataStatusRows|continueWorkRows/, 'administrator workbench should derive daily operations from real OFBiz business data, modules, and navigation' ) assert.doesNotMatch( dashboardVue, /props\.inventory|UiInventory|ParityPage|parityManifest|routeManifest|pageSource|pageList|highRiskPages|businessParityStatus|parityStatus|risk ===|page\.risk/, 'administrator workbench must not derive product UI from migration inventory or parity risk fields' ) assert.doesNotMatch( dashboardVue, /常用操作|数据来源|空数据源|OFBiz Entity|实体可读/, 'administrator workbench should use operator language instead of technical source diagnostics' ) assert.doesNotMatch( dashboardVue, /后端授权|后端连接|待登录授权|已授权业务|条处理路径|OFBiz Security|权限来源|会话与权限|entryCount:\s*module\.quickPages\.length|actions:\s*module\.quickPages\.length/, 'administrator workbench must not expose backend-authorization wording or quick-page counts as business operation language' ) for (const [fileName, content] of [ ['ProductAdminView.vue', productAdminVue], ['OrderAdminView.vue', orderAdminVue], ['PartyAdminView.vue', partyAdminVue], ['AccountingAdminView.vue', accountingAdminVue], ['InventoryAdminView.vue', inventoryAdminVue], ['ManufacturingAdminView.vue', manufacturingAdminVue], ['HumanResAdminView.vue', humanResAdminVue], ['SalesAdminView.vue', salesAdminVue], ['ProcurementAdminView.vue', procurementAdminVue], ['ScrumAdminView.vue', scrumAdminVue], ['OperationsAdminView.vue', operationsAdminVue], ['ContentAdminView.vue', contentAdminVue], ['MarketingAdminView.vue', marketingAdminVue], ['CommerceAdminView.vue', commerceAdminVue], ['PosAdminView.vue', posAdminVue], ['MarketplaceAdminView.vue', marketplaceAdminVue], ['AnalyticsAdminView.vue', analyticsAdminVue], ['ExtensionAdminView.vue', extensionAdminVue], ['SecurityAdminView.vue', securityAdminVue], ['SystemOperationsView.vue', systemOperationsVue] ]) { assert.doesNotMatch( content, /数据源|个来源|不可用来源/, `${fileName} must present business data and recoverable business areas instead of data-source diagnostics` ) } assert.match(apiTs, /export async function getModuleSummary/, 'API client should expose a business data summary loader for module workspaces') assert.match(apiTypes, /export type ModuleSummary/, 'API types should document the module summary contract') assert.match(moduleVue, /getModuleSummary|moduleSummary|moduleEntityMetrics|pendingQueues|processingLanes|业务处理中枢|处理事项|异常交接/, 'module workspaces should use real OFBiz business-domain signals') assert.doesNotMatch( moduleVue, /props\.inventory|parityManifest|ParityPage|modulePages|filteredPages|workflowRows|scenarioColumns|businessParityStatus|parityStatus|frontendRewriteStatus|uiRewriteStatus|scenarioStatus|pending-business-e2e|ready-for-business-e2e/, 'module workspaces must not expose migration or parity status semantics' ) assert.doesNotMatch( moduleVue, /risk ===|page\.risk|highRiskPages|highRisk|高风险|待生成/, 'module workspaces should not derive operator queues from migration risk fields' ) const workspaceLabelExpectations = [ ['ErpOrderWorkspace.vue', orderWorkspaceVue, ['订单录入', '订单运营']], ['ErpFinanceOperationsWorkspace.vue', financeOperationsWorkspaceVue, ['财务运营']], ['ErpFinanceWorkspace.vue', financeWorkspaceVue, ['财务处理']], ['ErpEbayOperationsWorkspace.vue', ebayOperationsWorkspaceVue, ['店铺运营']], ['ErpBusinessIntelligenceWorkspace.vue', businessIntelligenceWorkspaceVue, ['报表分析']], ['ErpBirtReportingWorkspace.vue', birtReportingWorkspaceVue, ['报表发布']], ['ErpSalesAutomationWorkspace.vue', salesAutomationWorkspaceVue, ['销售自动化']], ['ErpInventoryWorkspace.vue', inventoryWorkspaceVue, ['库存管理']], ['ErpFulfillmentWorkspace.vue', fulfillmentWorkspaceVue, ['履约发运']], ['ErpPosWorkspace.vue', posWorkspaceVue, ['收银工作台']], ['ErpReturnWorkspace.vue', returnWorkspaceVue, ['退货处理']], ['ErpCatalogWorkspace.vue', catalogWorkspaceVue, ['商品管理']], ['ErpCommerceSurface.vue', commerceSurfaceVue, ['电商前台']], ['ErpManufacturingWorkspace.vue', manufacturingWorkspaceVue, ['生产管理']], ['ErpWorkManagementWorkspace.vue', workManagementWorkspaceVue, ['项目任务']], ['ErpHumanResWorkspace.vue', humanResWorkspaceVue, ['人事管理']], ['ErpPartyWorkspace.vue', partyWorkspaceVue, ['客户与组织']], ['ErpMarketingWorkspace.vue', marketingWorkspaceVue, ['营销管理']], ['ErpMediaWorkspace.vue', mediaWorkspaceVue, ['媒体上传']], ['ErpReportWorkspace.vue', reportWorkspaceVue, ['财务报表']], ['ErpSystemWorkspace.vue', systemWorkspaceVue, ['系统管理']], ['ErpContentWorkspace.vue', contentWorkspaceVue, ['内容管理']], ['ErpAssetMaintenanceWorkspace.vue', assetMaintenanceWorkspaceVue, ['资产维护']], ['ErpExtensionOperationsWorkspace.vue', extensionOperationsWorkspaceVue, ['扩展应用']], ['ErpPluginOperationsWorkspace.vue', await source('src/components/erp/ErpPluginOperationsWorkspace.vue'), ['插件应用']], ['ErpProcurementWorkspace.vue', procurementWorkspaceVue, ['采购管理']], ['ErpScrumWorkspace.vue', scrumWorkspaceVue, ['敏捷交付']], ['ErpPortalAdminWorkspace.vue', portalAdminWorkspaceVue, ['门户管理']], ['ErpGatewayAdminWorkspace.vue', gatewayAdminWorkspaceVue, ['网关管理']] ] const oldWorkspaceLabelPattern = /ORDER ENTRY|ORDER OPERATIONS|FINANCE OPERATIONS|FINANCE WORKSPACE|PARTY WORKSPACE|PORTAL ADMIN|GATEWAY ADMIN|BIRT REPORTING|REPORT WORKSPACE|EBAY OPERATIONS|BUSINESS INTELLIGENCE|PROCUREMENT WORKSPACE|SCRUM WORKSPACE|SALES AUTOMATION|ASSET MAINTENANCE|HUMANRES WORKSPACE|INVENTORY WORKSPACE|CATALOG WORKSPACE|CONTENT WORKSPACE|SYSTEM ADMIN WORKSPACE|EXTENSION OPERATIONS|PLUGIN OPERATIONS|FULFILLMENT WORKSPACE|RETURN WORKSPACE|UPLOAD WORKSPACE|POS WORKSPACE|MFG WORKSPACE|MARKETING WORKSPACE|WORK MANAGEMENT|COMMERCE SURFACE/ for (const [fileName, content, labels] of workspaceLabelExpectations) { for (const label of labels) { assert.match( content, new RegExp(`erp-workspace-kicker[\\s\\S]{0,180}${escapeRegExp(label)}`), `${fileName} must use Chinese ERP operator-facing workspace label: ${label}` ) } assert.doesNotMatch( content, new RegExp(`erp-workspace-kicker[\\s\\S]{0,180}(?:${oldWorkspaceLabelPattern.source})`), `${fileName} must not expose English engineering workspace labels in the administrator product` ) } for (const text of ['订单', '商品', '客户', '销售', '采购', '财务', '库存', '生产', '人事', '运营']) { assert.match(shellVue + dashboardVue + moduleVue + moduleCatalog, new RegExp(text), `primary shell must expose ERP domains: ${text}`) } assert.match(routerTs, /InventoryAdminView/, 'administrator site should include a dedicated inventory administration view') assertRouteMapsToView('#/facility', 'InventoryAdminView', 'inventory administration should have a dedicated facility route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/facility/, 'inventory administration should be reachable from primary ERP navigation') for (const text of ['库存管理台', '库存执行台', '收货上架', '可用量关注', '调拨跟进', '盘点差异', '库存异常', '库位交接', '库存项', '设施库位', '收货发运', '库存明细', '库存调拨', '盘点调整', '库存风险', '库存业务数据']) { assert.match(inventoryAdminVue, new RegExp(text), `inventory administration page should expose ERP inventory operation: ${text}`) } for (const text of ['inventoryExecutionRows', 'receivingWorkRows', 'availabilityRows', 'transferWorkRows', 'countVarianceRows', 'inventoryExceptionRows', 'inventory-admin-execution-grid', 'inventory-admin-work-row', 'inventory-admin-risk-row']) { assert.match(inventoryAdminVue + modernCss, new RegExp(text), `inventory administration page should expose warehouse operator work surfaces: ${text}`) } for (const entityName of ['InventoryItem', 'Facility', 'FacilityLocation', 'Shipment', 'InventoryItemDetail', 'InventoryTransfer', 'PhysicalInventory']) { assert.match(inventoryAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `inventory administration page should load real ${entityName} rows from the entity API`) } assert.doesNotMatch( inventoryAdminVue, /Demo[A-Za-z0-9_]*|INV-\d+|FAC-\d+|SHP-\d+|LOC-\d+|TRF-\d+|SampleWarehouse|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'inventory administration page should not expose fake inventory rows or engineering language' ) assert.match(routerTs, /ManufacturingAdminView/, 'administrator site should include a dedicated manufacturing administration view') assertRouteMapsToView('#/manufacturing', 'ManufacturingAdminView', 'manufacturing administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/manufacturing/, 'manufacturing administration should be reachable from primary ERP navigation') for (const text of ['生产管理台', '生产调度台', '排产跟进', '物料缺口', '工序准备', '发料领用', '成本关注', '生产异常', '生产运行', '物料需求', 'BOM 工艺', '工序任务', '成本构成', '生产风险', '生产业务数据']) { assert.match(manufacturingAdminVue, new RegExp(text), `manufacturing administration page should expose ERP manufacturing operation: ${text}`) } for (const text of ['manufacturingScheduleRows', 'materialShortageRows', 'routingPreparationRows', 'issuanceWorkRows', 'manufacturingExceptionRows', 'manufacturing-admin-execution-grid', 'manufacturing-admin-work-row', 'manufacturing-admin-risk-row']) { assert.match(manufacturingAdminVue + modernCss, new RegExp(text), `manufacturing administration page should expose production operator work surfaces: ${text}`) } for (const entityName of ['WorkEffort', 'Requirement', 'ProductAssoc', 'WorkEffortGoodStandard', 'CostComponent', 'ItemIssuance']) { assert.match(manufacturingAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `manufacturing administration page should load real ${entityName} rows from the entity API`) } assert.doesNotMatch( manufacturingAdminVue, /Demo[A-Za-z0-9_]*|MFG-\d+|RUN-\d+|BOM-\d+|MAT-\d+|WORK-\d+|SampleFactory|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'manufacturing administration page should not expose fake manufacturing rows or engineering language' ) assert.match(routerTs, /HumanResAdminView/, 'administrator site should include a dedicated human resources administration view') assertRouteMapsToView('#/humanres', 'HumanResAdminView', 'human resources administration should have a dedicated route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/humanres/, 'human resources administration should be reachable from primary ERP navigation') for (const text of ['人事管理台', '人事执行台', '入转离跟进', '岗位空缺', '招聘处理', '绩效复核', '技能资质', '人事异常', '任职交接', '员工档案', '雇佣关系', '岗位编制', '招聘申请', '人事风险', '人事业务数据']) { assert.match(humanResAdminVue, new RegExp(text), `human resources administration page should expose ERP human resources operation: ${text}`) } for (const text of ['humanResExecutionRows', 'employmentLifecycleRows', 'positionVacancyRows', 'recruitingWorkRows', 'performanceReviewRows', 'skillCredentialRows', 'humanResExceptionRows', 'humanres-admin-execution-grid', 'humanres-admin-work-row', 'humanres-admin-risk-row']) { assert.match(humanResAdminVue + modernCss, new RegExp(text), `human resources administration page should expose HR operator work surfaces: ${text}`) } for (const entityName of ['Person', 'Employment', 'EmplPosition', 'EmplPositionFulfillment', 'EmploymentApp', 'JobRequisition', 'PerfReview', 'PartySkill']) { assert.match(humanResAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `human resources administration page should load real ${entityName} rows from the entity API`) } assert.doesNotMatch( humanResAdminVue, /Demo[A-Za-z0-9_]*|EMP-\d+|HR-\d+|POS-\d+|APP-\d+|SampleEmployee|SamplePosition|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'human resources administration page should not expose fake human resources rows or engineering language' ) assert.match(moduleCatalog, /primaryModules\s*=\s*\[[\s\S]*'sales'[\s\S]*\]/, 'sales automation should be a first-class primary ERP module in the administrator shell') assert.match(moduleCatalog, /id: 'sales'[\s\S]*navLabel: '销售'[\s\S]*SalesForceAutomation__FindSalesOpportunity[\s\S]*SalesForceAutomation__ConvertLead/, 'sales module should expose opportunity, lead, forecast, and conversion entry points') assert.doesNotMatch(moduleCatalog, /id: 'marketing'[\s\S]*?prefixes:\s*\[[^\]]*SalesForceAutomation__/, 'marketing module must not own SalesForceAutomation routes') assert.match(apiTs, /sales:\s*\[[\s\S]*SalesOpportunity[\s\S]*SalesForecast[\s\S]*PartyRole[\s\S]*CommunicationEvent/, 'sales module summary must load real sales automation entities') assert.match(moduleCatalog, /primaryModules\s*=\s*\[[\s\S]*'procurement'[\s\S]*\]/, 'procurement should be a first-class primary ERP module in the administrator shell') assert.match(moduleCatalog, /id: 'procurement'[\s\S]*navLabel: '采购'[\s\S]*order__FindRequirements[\s\S]*order__ApproveRequirements[\s\S]*ap__FindVendors[\s\S]*catalog__EditSupplierProduct[\s\S]*facility__ReceiveInventoryAgainstPurchaseOrder/, 'procurement module should expose requirement, approval, vendor, supplier product, and receiving entry points') assert.doesNotMatch(procurementModuleSource, /prefixes:\s*\[[^\]]*(?:order__|ap__|catalog__|facility__)/, 'procurement module must not steal broad order, AP, catalog, or facility page ownership') assert.match(apiTs, /procurement:\s*\[[\s\S]*Requirement[\s\S]*SupplierProduct[\s\S]*Vendor[\s\S]*Quote[\s\S]*CustRequest/, 'procurement module summary must load real procurement and supplier entities') const operationsModuleSource = moduleConfigSource('operations', 'content') const scrumModuleSource = moduleConfigSource('scrum', 'operations') const commerceModuleSource = moduleConfigSource('commerce', 'marketplace') const marketplaceModuleSource = moduleConfigSource('marketplace', 'pos') const analyticsModuleSource = moduleConfigSource('analytics', 'system-admin') const systemAdminModuleSource = moduleConfigSource('system-admin', 'extensions') assert.match(moduleCatalog, /primaryModules\s*=\s*\[[\s\S]*'scrum'[\s\S]*\]/, 'Scrum should be a first-class primary ERP module in the administrator shell') assert.match(scrumModuleSource, /id: 'scrum'[\s\S]*navLabel: '敏捷'[\s\S]*scrum__Sprints[\s\S]*scrum__AddProdBacklog[\s\S]*scrum__SprintTask[\s\S]*scrum__FindResource[\s\S]*scrum__FindTimeSheet[\s\S]*scrum__ProductStatistics/, 'Scrum module should expose sprint, backlog, task board, resource, timesheet, and statistics entry points') assert.doesNotMatch(operationsModuleSource, /scrum__|\/scrum\/control\//, 'generic operations module must not own Scrum route prefixes or controller paths') assert.match(apiTs, /scrum:\s*\[[\s\S]*ProductBacklog[\s\S]*ProjectSprint[\s\S]*ProjectSprintBacklogAndTask[\s\S]*Timesheet[\s\S]*TimeEntry/, 'Scrum module summary must load real agile delivery entities') assert.match(routerTs, /ScrumAdminView/, 'administrator site should include a dedicated Scrum administration view') assertRouteMapsToView('#/scrum', 'ScrumAdminView', 'Scrum administration should have a dedicated top-level route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/scrum/, 'Scrum administration should be reachable from primary ERP navigation') for (const text of ['敏捷交付管理台', '敏捷执行台', 'Backlog 承接', 'Sprint 推进', '任务交接', '工时复核', '资源协调', '交付异常', '产品 Backlog', 'Sprint 排程', '任务板', '团队资源', '工时确认', '交付风险', '敏捷业务数据']) { assert.match(scrumAdminVue, new RegExp(text), `Scrum administration page should expose ERP agile operation: ${text}`) } for (const text of ['scrumExecutionRows', 'backlogHandoffRows', 'sprintDeliveryRows', 'sprintTaskHandoffRows', 'timeReviewRows', 'resourceCoordinationRows', 'scrumExceptionRows', 'scrum-admin-execution', 'scrum-admin-work-row', 'scrum-admin-risk-row']) { assert.match(scrumAdminVue + modernCss, new RegExp(text), `Scrum administration page should expose executable delivery work surfaces: ${text}`) } for (const entityName of ['ProductBacklog', 'ProjectSprint', 'ProjectSprintBacklogAndTask', 'Timesheet', 'TimeEntry', 'WorkEffortPartyAssignment', 'CustRequest']) { assert.match(scrumAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `Scrum administration page should load real ${entityName} rows from the entity API`) } assert.doesNotMatch( scrumAdminVue, /Demo[A-Za-z0-9_]*|SCRUM-\d+|SPRINT-\d+|TASK-\d+|BACKLOG-\d+|SampleSprint|SampleBacklog|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'Scrum administration page should not expose fake agile delivery rows or engineering language' ) assert.match(routerTs, /OperationsAdminView/, 'administrator site should include a dedicated operations administration view') assertRouteMapsToView('#/operations', 'OperationsAdminView', 'operations administration should have a dedicated top-level route') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/operations/, 'operations administration should be reachable from primary ERP navigation') for (const text of ['运营任务管理台', '运营执行台', '任务承接', '排程依赖', '人员分配', '工时复核', '请求跟进', '沟通交接', '运营异常', '任务排程', '工作分配', '工时表', '请求协同', '沟通记录', '运营风险', '运营业务数据']) { assert.match(operationsAdminVue, new RegExp(text), `operations administration page should expose ERP operations work: ${text}`) } for (const text of ['operationsExecutionRows', 'taskHandoffRows', 'dependencyRows', 'assignmentDispatchRows', 'operationsTimeReviewRows', 'requestFollowupRows', 'communicationHandoffRows', 'operationsExceptionRows', 'operations-admin-execution', 'operations-admin-work-row', 'operations-admin-risk-row']) { assert.match(operationsAdminVue + modernCss, new RegExp(text), `operations administration page should expose executable operations work surfaces: ${text}`) } for (const entityName of ['WorkEffort', 'WorkEffortAssoc', 'WorkEffortPartyAssignment', 'Timesheet', 'TimeEntry', 'CustRequest', 'CommunicationEvent']) { assert.match(operationsAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `operations administration page should load real ${entityName} rows from the entity API`) } assert.doesNotMatch( operationsAdminVue, /Demo[A-Za-z0-9_]*|OPS-\d+|WORK-\d+|TASK-\d+|SampleTask|SampleOperation|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'operations administration page should not expose fake operations rows or engineering language' ) const secondaryAdminExpectations = [ { viewName: 'ContentAdminView', content: contentAdminVue, route: '#/content', activeView: 'content-admin', labels: ['内容管理台', '内容资源', '站点管理', 'CMS 树', '论坛消息', '博客文章', '发布风险', '内容业务数据'], entities: ['Content', 'DataResource', 'WebSite', 'WebPage', 'ElectronicText', 'CommunicationEvent'] }, { viewName: 'MarketingAdminView', content: marketingAdminVue, route: '#/marketing', activeView: 'marketing-admin', labels: ['营销管理台', '营销活动', '联系名单', '追踪码', '细分群组', '活动统计', '营销风险', '营销业务数据'], entities: ['MarketingCampaign', 'ContactList', 'TrackingCode', 'SegmentGroup', 'CommunicationEvent', 'PartyRole'] }, { viewName: 'CommerceAdminView', content: commerceAdminVue, route: '#/commerce', activeView: 'commerce-admin', labels: ['电商管理台', '购物车', '会员订单', '商品浏览', '退货请求', '客户资料', '电商风险', '电商业务数据'], entities: ['ShoppingList', 'OrderHeader', 'Product', 'ReturnHeader', 'Party', 'ProductStore'] }, { viewName: 'PosAdminView', content: posAdminVue, route: '#/pos', activeView: 'pos-admin', labels: ['POS 管理台', '门店购物车', '收银订单', '支付记录', '经理授权', '门店库存', '收银风险', 'POS 业务数据'], entities: ['ShoppingList', 'OrderHeader', 'Payment', 'UserLogin', 'Facility', 'InventoryItem'] }, { viewName: 'MarketplaceAdminView', content: marketplaceAdminVue, route: '#/marketplace', activeView: 'marketplace-admin', labels: ['店铺运营管理台', '店铺配置', '物流方式', '库存同步', '活动刊登', '店铺商品', '店铺风险', '店铺业务数据'], entities: ['EbayConfig', 'EbayShippingMethod', 'EbayProductStoreInventory', 'EbayProductListing', 'ProductStore', 'Product'] }, { viewName: 'AnalyticsAdminView', content: analyticsAdminVue, route: '#/analytics', activeView: 'analytics-admin', labels: ['报表分析管理台', '数据维度', '事实数据', '报表资源', '报表发布', '输出队列', '报表风险', '报表业务数据'], entities: ['DateDimension', 'ProductDimension', 'SalesOrderItemFact', 'SalesInvoiceItemFact', 'DataResource', 'Enumeration'] }, { viewName: 'ExtensionAdminView', content: extensionAdminVue, route: '#/extensions', activeView: 'extension-admin', labels: ['扩展应用管理台', '扩展记录', '特征配置', '价目表导入', '报表资源', '支付网关', '短信通道', '扩展业务数据'], entities: ['Example', 'ExampleFeature', 'ExampleFeatureAppl', 'ExcelImportHistory', 'DataResource', 'PaymentGatewayConfig', 'PaymentGatewayFirstData', 'Msg91GatewayConfig'] } ] for (const adminPage of secondaryAdminExpectations) { assert.match(routerTs, new RegExp(`${adminPage.viewName}`), `${adminPage.viewName} should be mounted as a dedicated administrator page via the route table`) assertRouteMapsToView(adminPage.route, adminPage.viewName, `${adminPage.route} should resolve to ${adminPage.activeView}`) assert.match(shellVue + dashboardVue + moduleCatalog, new RegExp(escapeRegExp(adminPage.route)), `${adminPage.viewName} should be reachable from administrator navigation`) assert.match(adminPage.content, /ErpDomainAdminView/, `${adminPage.viewName} should use the shared domain admin renderer that loads entity sources`) for (const text of adminPage.labels) { assert.match(adminPage.content, new RegExp(text), `${adminPage.viewName} should expose ERP operation label: ${text}`) } for (const entityName of adminPage.entities) { assert.match(adminPage.content, new RegExp(`entityName: '${entityName}'`), `${adminPage.viewName} should declare real ${entityName} rows for the entity API renderer`) } assert.doesNotMatch( adminPage.content, /Demo[A-Za-z0-9_]*|Sample[A-Za-z0-9_]*|MOCK-\d+|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, `${adminPage.viewName} should not expose fake rows or engineering language` ) } assert.match(moduleCatalog, /secondaryModules\s*=\s*\[[\s\S]*'marketplace'[\s\S]*\]/, 'marketplace should be a first-class administrator application module') assert.match(marketplaceModuleSource, /id: 'marketplace'[\s\S]*navLabel: '店铺'[\s\S]*ebay__FindEbayConfigurations[\s\S]*ebay__EbayShippingMethods[\s\S]*ebaystore__ebayStoreInventory[\s\S]*ebaystore__ActiveListing/, 'marketplace module should expose eBay configuration, shipping, inventory, and active listing entry points') assert.doesNotMatch(commerceModuleSource, /ebay__|ebaystore__|\/ebay\/control\/|\/ebaystore\/control\//, 'commerce module must not own eBay marketplace route prefixes or controller paths') assert.match(apiTs, /marketplace:\s*\[[\s\S]*EbayConfig[\s\S]*ProductStore[\s\S]*EbayProductListing[\s\S]*EbayProductStoreInventory[\s\S]*EbayShippingMethod/, 'marketplace module summary must load real marketplace entities') assert.match(moduleCatalog, /secondaryModules\s*=\s*\[[\s\S]*'analytics'[\s\S]*\]/, 'analytics should be a first-class administrator module instead of living under system tools') assert.match(analyticsModuleSource, /id: 'analytics'[\s\S]*navLabel: '报表'[\s\S]*bi__main[\s\S]*bi__ReportBuilderSelectStarSchema[\s\S]*bi__ReportBuilderSelectStarSchemaFields[\s\S]*birt__ListFlexibleReport[\s\S]*birt__CreateFlexibleReport[\s\S]*birt__Mail/, 'analytics module should expose BI warehouse and BIRT report publishing entry points') assert.match(analyticsModuleSource, /prefixes:\s*\[[\s\S]*'bi__'[\s\S]*'birt__'[\s\S]*\]/, 'analytics module should own BI and BIRT route prefixes') assert.match(analyticsModuleSource, /legacyIncludes:\s*\[[\s\S]*'\/bi\/control\/'[\s\S]*'\/birt\/control\/'[\s\S]*\]/, 'analytics module should own BI and BIRT controller paths') assert.doesNotMatch(systemAdminModuleSource, /bi__|birt__|\/bi\/control\/|\/birt\/control\/|报表中心|birt__main/, 'system admin module must not own BI/BIRT routes or report center quick links') assert.match(apiTs, /analytics:\s*\[[\s\S]*DateDimension[\s\S]*ProductDimension[\s\S]*SalesOrderItemFact[\s\S]*SalesInvoiceItemFact[\s\S]*DataResource[\s\S]*Enumeration/, 'analytics module summary must load real BI warehouse and BIRT report entities') assert.match(rendererVue, /ErpProcurementWorkspace/, 'procurement pages should use a first-class procurement workspace') assert.match(rendererVue, /isProcurementPage/, 'renderer should detect procurement routes before order, AP, catalog, and inventory fallbacks') assert.match(rendererVue, /procurementWorkspaceBlock/, 'renderer should collapse procurement blocks into one procurement workspace per page') assert.match(rendererVue, /data-modern-procurement-page/, 'renderer should mark page-level procurement workspaces') for (const pageId of [ 'order__FindRequirements', 'order__ApproveRequirements', 'ap__FindVendors', 'catalog__EditSupplierProduct', 'facility__ReceiveInventoryAgainstPurchaseOrder' ]) { assert.match(captureScreenshotsScript, new RegExp(`#\\/pages\\/${pageId}`), `screenshots must cover procurement page ${pageId}`) } for (const text of [ '采购管理', '采购需求', '需求审批', '供应商', '供应商品', '供应商报价', '采购请求', '到货接收', 'Requirement', 'SupplierProduct', 'Vendor', 'Quote', 'CustRequest' ]) { assert.match(procurementWorkspaceVue, new RegExp(text), `procurement workspace must expose real procurement operations: ${text}`) } for (const entityName of ['Requirement', 'SupplierProduct', 'Vendor', 'Quote', 'CustRequest', 'Shipment', 'InventoryItem']) { assert.match(procurementWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `procurement workspace must load real ${entityName} rows from the entity API`) } assert.match(procurementWorkspaceVue, /采购业务数据/, 'procurement workspace must describe entity-backed procurement data') assert.doesNotMatch( procurementWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|REQ-\d+|PO-\d+|VEN-\d+|BigSupplier|Wholesale replenishment/, 'procurement workspace must not synthesize requirements, vendors, supplier products, quotes, requests, receiving, or inventory rows from generated metadata' ) // System governance tools live in their own secondary sidebar group (系统治理) built from systemLinks. assert.match(shellVue, /systemLinks\s*=\s*\[[\s\S]*'\/system'[\s\S]*'\/system\/security'[\s\S]*'\/system\/operations'/, 'shell should keep support/governance tools in a secondary support menu') assert.match(shellVue, /系统治理/, 'shell should group system governance tools under a dedicated secondary sidebar section') assert.match(shellVue, /quickActions[\s\S]*path: '\/pages\/order__showcart'[\s\S]*path: '\/pages\/party__NewCustomer'[\s\S]*path: '\/pages\/catalog__EditProduct'[\s\S]*path: '\/pages\/accounting__ManualTransaction'/, 'topbar quick actions should deep-link to high-frequency administrator work pages') assert.doesNotMatch(shellVue, /order__orderentry/, 'topbar quick actions must not point at missing generated pages') for (const [moduleId, landingPath] of [ ['order', '#/orders'], ['catalog', '#/catalog/products'], ['party', '#/parties'], ['sales', '#/sales'], ['procurement', '#/procurement'], ['accounting', '#/accounting'], ['facility', '#/facility'], ['manufacturing', '#/manufacturing'], ['humanres', '#/humanres'], ['scrum', '#/scrum'], ['operations', '#/operations'], ['content', '#/content'], ['marketing', '#/marketing'], ['commerce', '#/commerce'], ['marketplace', '#/marketplace'], ['pos', '#/pos'], ['analytics', '#/analytics'], ['extensions', '#/extensions'] ]) { assert.match(moduleCatalog, new RegExp(`id: '${moduleId}'[\\s\\S]*landingPath: '${landingPath.replace('/', '\\/')}'`), `${moduleId} module should declare a dedicated administrator landing path`) } for (const [fileName, content, moduleId, landingPath] of [ ['OrderAdminView.vue', orderAdminVue, 'order', '#/orders'], ['ProductAdminView.vue', productAdminVue, 'catalog', '#/catalog/products'], ['PartyAdminView.vue', partyAdminVue, 'party', '#/parties'], ['SalesAdminView.vue', salesAdminVue, 'sales', '#/sales'], ['ProcurementAdminView.vue', procurementAdminVue, 'procurement', '#/procurement'], ['AccountingAdminView.vue', accountingAdminVue, 'accounting', '#/accounting'], ['InventoryAdminView.vue', inventoryAdminVue, 'facility', '#/facility'], ['ManufacturingAdminView.vue', manufacturingAdminVue, 'manufacturing', '#/manufacturing'], ['HumanResAdminView.vue', humanResAdminVue, 'humanres', '#/humanres'], ['ScrumAdminView.vue', scrumAdminVue, 'scrum', '#/scrum'], ['OperationsAdminView.vue', operationsAdminVue, 'operations', '#/operations'], ['ContentAdminView.vue', contentAdminVue, 'content', '#/content'], ['MarketingAdminView.vue', marketingAdminVue, 'marketing', '#/marketing'], ['CommerceAdminView.vue', commerceAdminVue, 'commerce', '#/commerce'], ['MarketplaceAdminView.vue', marketplaceAdminVue, 'marketplace', '#/marketplace'], ['PosAdminView.vue', posAdminVue, 'pos', '#/pos'], ['AnalyticsAdminView.vue', analyticsAdminVue, 'analytics', '#/analytics'], ['ExtensionAdminView.vue', extensionAdminVue, 'extensions', '#/extensions'] ]) { assert.doesNotMatch( content, new RegExp(`#\\/module\\/${moduleId}`), `${fileName} must not send administrator actions back to the generic module shell for ${moduleId}` ) assert.match( content, new RegExp(escapeRegExp(landingPath)), `${fileName} should keep administrator actions on the dedicated ${landingPath} landing page` ) } assert.match(shellVue, /moduleLandingPath/, 'shell should resolve module navigation through dedicated administrator landing paths') assert.match(shellVue, /quickQueues[\s\S]*primaryBusinessModules[\s\S]*path: toPath\(moduleLandingPath\(module\)\)/, 'topbar work entries should route to dedicated ERP module landing pages') assert.doesNotMatch(shellVue, /href="`#\/module\/\$\{item\.id\}`"|href="`#\/module\/\$\{module\.id\}`"/, 'primary navigation must not default every module to the generic module shell') assert.doesNotMatch(shellVue + systemVue + moduleCatalog, /#\/module\/extensions/, 'extension application entry points should use the dedicated extension administrator page instead of the generic module shell') assert.doesNotMatch( shellVue, /href="#\/orders"[\s\S]*v-for="item in primaryBusinessModules"/, 'sidebar core business navigation should be generated from the module catalog, not duplicated by hardcoded links and module links' ) assert.match(dashboardVue, /moduleLandingPath\(module\)/, 'dashboard module shortcuts should open dedicated ERP administrator pages') assert.match(shellVue, /@click="navigate\(item\.path\)"/, 'topbar work alerts should be actionable ERP entries') assert.doesNotMatch(shellVue, /count:\s*(?:18|7|12)|待审核订单|待过账财务|待发运包裹/, 'topbar must not use fixed demo work-alert counts') assert.doesNotMatch( shellVue, /count:\s*module\.quickPages\.length|后端授权|authorizedApplications/, 'administrator shell must not present quick-page counts or backend app lists as business queues' ) for (const text of ['统一运营管理平台', '运营工作台', '核心业务', '渠道与扩展', 'quickQueues', 'navGroups']) { assert.match(shellVue, new RegExp(text), `administrator shell should behave like an ERP operator chrome: ${text}`) } for (const text of ['业务处理中枢', '处理流', '待处理队列', '异常交接', '交接记录', '执行动作', '处理事项']) { assert.match(moduleVue, new RegExp(text), `generic module workspace should present an ERP operations cockpit: ${text}`) } assert.doesNotMatch( moduleVue, /打开常用入口|常用操作|最近记录|真实 OFBiz 模块数据|模块数据源|模块数据|数据源|没有匹配的常用操作/, 'generic module workspace must not read like a route directory or engineering data-source shell' ) assert.doesNotMatch(appVue + shellVue, /adminPreview|preserveAdminPreview|isAdminPreviewMode/, 'product code must not bypass the OFBiz login gate with preview parameters') assert.doesNotMatch( shellVue, /const operationsTools = \[[\s\S]*?交付状态[\s\S]*?\]/, 'delivery and parity tools must not live in the high-frequency operations navigation' ) assert.doesNotMatch( shellVue, /运维入口|旧入口跳转|历史地址|组件展厅/, 'sidebar should not foreground migration or component tooling as a main business entry' ) assert.doesNotMatch( shellVue + dashboardVue + systemVue, /OFBiz 应用|

业务应用<\/h2>|businessApps|navigationRows|admin-app-list|admin-app-row|modern-aside-section--apps|应用权限|system-app-list|navigation\.slice/, 'administrator shell and landing page must present ERP business domains instead of an OFBiz application catalog' ) assert.doesNotMatch(shellVue, /\/webtools\/control\/main/, 'modern administrator chrome must not expose a primary button back to the old WebTools main page') // The business-domain module directory is now the grouped sidebar navigation tree // (each module is an el-sub-menu listing its quick pages), not a duplicate grid on // the dashboard. Assert it on the shell where it actually lives. assert.match( shellVue, /el-sub-menu[\s\S]*module\.navLabel[\s\S]*module\.quickPages/, 'administrator chrome should expose a business-domain module directory (grouped sidebar tree)' ) assert.match( dashboardVue, /quickAdminActionRows[\s\S]*快捷动作[\s\S]*erp-quick/, 'administrator landing page should expose business-domain operation rows' ) assert.doesNotMatch( dashboardVue, /组件展厅|旧入口名称|按组件或清单组织|迁移/, 'authenticated landing page must not read like a preview, component gallery, or migration dashboard' ) assert.match(systemVue, /系统维护|安全与会话|运行任务|运行日志|缓存维护|导入导出|用户与权限|权限与业务域/, 'system page should own administrator operations') assert.match(businessCenterVue, /处理动作|当前班次|业务受理台|统一业务中心/, 'business center should use business-facing product language') assert.match(businessCenterVue, /currentHashQuery|syncQueryFromRoute|当前办理口径/, 'business center should consume global business search query') assert.match(businessCenterVue, /getGlobalBusinessSearch|businessSearch|businessRecords|当前班次/, 'business center should use real OFBiz entity search results') assert.match(apiTs, /export async function getGlobalBusinessSearch/, 'API client should expose global business search') assert.match(apiTypes, /export type GlobalBusinessSearch/, 'API types should document global business search') assert.match(shellVue, /\/business\?query=\$\{encodeURIComponent/, 'global command search should deep-link into the business center') assert.doesNotMatch(businessCenterVue, />预览<\/el-button>|title="页面路由"|eyebrow="页面清单"/, 'business center must not read as a preview catalog') assert.doesNotMatch( businessCenterVue, /业务入口|交接入口|高频入口|常用处理入口/, 'business entry center should present processing paths, not an entry directory' ) for (const text of ['统一业务中心', '业务受理台', '待办事项', '当前班次', '当前办理口径', '今日承接', '优先处理', '异常队列', '续办记录', '待办队列', '快捷处理', '处理建议', '进入处理']) { assert.match(businessCenterVue, new RegExp(text), `business entry center should expose ERP-facing field: ${text}`) } assert.doesNotMatch( businessCenterVue + moduleVue, /待登录授权|待授权|需授权|实体可读|可读实体|数据连接|数据状态|业务深链|处理路径/, 'business center and module workspaces must not use catalog, authorization-diagnostic, or route-path wording as primary product language' ) for (const text of ['businessWorkLanes', 'businessShiftRows', 'priorityDispatchRows', 'exceptionQueueRows', 'continueRecordRows', 'queueCards', 'handoffRows', 'business-command-grid', 'business-shift-board', 'business-priority-board', 'business-exception-board', 'business-continue-board', '业务受理台', '办理口径', '待办总量']) { assert.match(businessCenterVue, new RegExp(text), `business center should behave like an ERP operations console instead of a page directory: ${text}`) } assert.doesNotMatch( businessCenterVue, /props\.inventory\.routeManifest|routeManifest|label="页面标识"|label="适配"|label="验收清单"|label="业务等价"|label="控制规则"|row\.adapterCovered|row\.businessParityStatus|row\.controller|缺失入口|已接入工作区|需专属处理台|历史地址|待补齐事项|missingRoutes|missingActions|generated-renderable|generated-adapter-renderable|generated-needs-custom-vue|risk ===|row\.risk/, 'business entry center must not use route inventory or migration columns in the primary operator table' ) assert.doesNotMatch( appVue + businessCenterVue + systemVue, /LegacyRedirectView|#\/legacy|\/legacy\?path=|goSourcePath|sourcePath|来源入口解析|历史地址映射/, 'administrator product must not expose a visible legacy/source-address bridge; old URLs should be redirected by the OFBiz filter into business pages' ) assert.doesNotMatch(rendererVue, /不是页面预览/, 'business pages must not describe themselves as previews') assert.doesNotMatch( businessPageVue, /page\.acceptance|checklist|pending-business-e2e|rewritten-preview-passed|frontend-passed|ready-for-business-e2e|验收|等价/, 'business page chrome must not expose migration acceptance state to ERP operators' ) for (const text of ['处理台', '资料明细', '处理动作', '操作记录', '当前单据', '处理进度']) { assert.match( businessPageVue, new RegExp(text), `generated business pages should read as an ERP operator workspace: ${text}` ) } assert.doesNotMatch( businessPageVue, /eyebrow="业务页面"|label="业务数据"|label="流转记录"|>\s*业务能力\s*<|>\s*业务区块\s*'\)/, 'parity smoke must not treat the SPA index shell as proof that a business page rendered' ) assert.doesNotMatch( previewScript + parityScript, /src\/data\/(?:previewVerification|parityVerification)\.ts|outTs|writeFile\(outTs/, 'verification reports must stay under plugins/modern-ui/verification and must not be written into production src/data' ) assert.doesNotMatch( previewScript, /#\/legacy\?path=|legacy-redirect|来源入口解析|旧入口桥接/, 'preview verification must exercise the administrator product and server-side URL cutover, not a visible legacy bridge page' ) assert.match(rendererVue, /ErpPartyWorkspace/, 'party pages should use a first-class customer and organization workspace') assert.match(rendererVue, /isPartyOperationsBlock/, 'renderer should route party adapters into the Party workspace') assert.match(rendererVue, /isPartyPage/, 'renderer should detect Party routes as page-level customer and organization workspaces') assert.match(rendererVue, /partyWorkspaceBlock/, 'renderer should collapse Party blocks into one customer and organization workspace per page') assert.match(rendererVue, /data-modern-party-page/, 'renderer should mark page-level Party administrator workspaces') for (const text of ['客户沟通', '关系/联系人', '请求转化', '发送前检查']) { assert.match(partyWorkspaceVue, new RegExp(text), `party workspace must expose real customer operations: ${text}`) } assert.match(rendererVue, /ErpPortalAdminWorkspace/, 'portal pages should use a first-class portal administrator workspace') assert.match(rendererVue, /isPortalAdminPage/, 'renderer should detect myportal and PortalPage routes before generic fallbacks') assert.match(rendererVue, /portalAdminWorkspaceBlock/, 'renderer should collapse portal blocks into one administrator workspace per page') assert.match(rendererVue, /data-modern-portal-admin-page/, 'renderer should mark page-level portal administrator workspaces') for (const text of ['门户管理', '门户页面', '栏目布局', 'Portlet 编排', '权限分配', 'PortalPage', 'PortalPageColumn', 'PortalPagePortlet', 'PortalPortlet', 'SecurityGroup', 'createPortalPage', 'updatePortalPage']) { assert.match(portalAdminWorkspaceVue, new RegExp(text), `portal admin workspace must expose real portal operations: ${text}`) } for (const entityName of ['PortalPage', 'PortalPageColumn', 'PortalPagePortlet', 'PortalPortlet', 'SecurityGroup']) { assert.match(portalAdminWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `portal admin workspace must load real ${entityName} rows from the entity API`) } assert.match(rendererVue, /ErpGatewayAdminWorkspace/, 'gateway pages should use a first-class payment and message gateway administrator workspace') assert.match(rendererVue, /isGatewayAdminPage/, 'renderer should detect FirstData and message gateway routes before generic fallbacks') assert.match(rendererVue, /gatewayAdminWorkspaceBlock/, 'renderer should collapse gateway blocks into one administrator workspace per page') assert.match(rendererVue, /data-modern-gateway-admin-page/, 'renderer should mark page-level gateway administrator workspaces') for (const text of ['网关管理', '支付网关', '短信通道', '连接检查', 'PaymentGatewayConfig', 'PaymentGatewayFirstData', 'TelecomGatewayConfig', 'Msg91GatewayConfig', 'updatePaymentGatewayFirstData']) { assert.match(gatewayAdminWorkspaceVue, new RegExp(text), `gateway admin workspace must expose real gateway operations: ${text}`) } for (const entityName of ['PaymentGatewayConfig', 'PaymentGatewayFirstData', 'TelecomGatewayConfig', 'Msg91GatewayConfig']) { assert.match(gatewayAdminWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `gateway admin workspace must load real ${entityName} rows from the entity API`) } assert.match(rendererVue, /ErpExtensionOperationsWorkspace/, 'extension and sample plugin pages should use a first-class extension operations workspace') assert.match(rendererVue, /isExtensionOperationsPage/, 'renderer should detect example, exampleext, pricat, and pricatdemo routes') assert.match(rendererVue, /extensionOperationsWorkspaceBlock/, 'renderer should collapse extension blocks into one operator workspace per page') assert.match(rendererVue, /data-modern-extension-operations-page/, 'renderer should mark page-level extension operations workspaces') for (const text of ['扩展应用', '扩展记录', '表单动作', '报表输出', '价目表导入', '导入日志', 'Example', 'ExampleFeature', 'ExcelImportHistory', 'DataResource']) { assert.match(extensionOperationsWorkspaceVue, new RegExp(text), `extension operations workspace must expose real extension operations: ${text}`) } for (const entityName of ['Example', 'ExampleFeature', 'ExampleFeatureAppl', 'ExcelImportHistory', 'DataResource']) { assert.match(extensionOperationsWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `extension operations workspace must load real ${entityName} rows from the entity API`) } assert.match(rendererVue, /ErpEbayOperationsWorkspace/, 'eBay pages should use a first-class marketplace operations workspace') assert.match(rendererVue, /isEbayOperationsPage/, 'renderer should detect ebay and ebaystore routes') assert.match(rendererVue, /ebayOperationsWorkspaceBlock/, 'renderer should collapse eBay blocks into one marketplace workspace per page') assert.match(rendererVue, /data-modern-ebay-operations-page/, 'renderer should mark page-level eBay operations workspaces') for (const text of ['店铺运营', '店铺配置', '刊登管理', '库存同步', '订单导入', '反馈/售后', '自动规则', 'EbayConfig', 'EbayProductStoreInventory', 'EbayProductListing', 'EbayProductStorePref', 'EbayShippingMethod', 'ProductStore']) { assert.match(ebayOperationsWorkspaceVue, new RegExp(text), `eBay operations workspace must expose real marketplace operations: ${text}`) } assert.match(ebayOperationsWorkspaceVue, /function ebayRowsRequest/, 'eBay operations workspace must keep marketplace row loading behind a dedicated request helper') assert.match(ebayOperationsWorkspaceVue, /getEntityRows\(entityName,\s*params\)/, 'eBay operations workspace request helper must still call the real entity row API') assert.match(ebayOperationsWorkspaceVue, /normalizeFallbackEbayEntityRows\(result,\s*entityName,\s*params\)/, 'eBay operations workspace must normalize optional eBay empty data without hiding real requests') for (const entityName of ['EbayConfig', 'EbayProductStoreInventory', 'EbayProductListing', 'EbayProductStorePref', 'EbayShippingMethod']) { assert.match(ebayOperationsWorkspaceVue, new RegExp(`ebayRowsRequest\\('${entityName}'`), `eBay operations workspace must load real ${entityName} rows through the marketplace request helper`) } for (const entityName of ['ProductStore', 'OrderHeader']) { assert.match(ebayOperationsWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `eBay operations workspace must load shared ${entityName} rows from the entity API`) } assert.match(rendererVue, /ErpBusinessIntelligenceWorkspace/, 'BI pages should use a first-class analytics and report builder workspace') assert.match(rendererVue, /isBusinessIntelligencePage/, 'renderer should detect Business Intelligence routes before catalog/report fallbacks') assert.match(rendererVue, /businessIntelligenceWorkspaceBlock/, 'renderer should collapse BI blocks into one analytics workspace per page') assert.match(rendererVue, /data-modern-business-intelligence-page/, 'renderer should mark page-level BI operations workspaces') for (const text of ['报表分析', '数据仓库', '星型模型', '维度管理', '事实表', '报表构建', '字段选择', '结果输出', 'DateDimension', 'ProductDimension', 'SalesOrderItemFact', 'SalesInvoiceItemFact', 'CurrencyDimension']) { assert.match(businessIntelligenceWorkspaceVue, new RegExp(text), `BI workspace must expose real analytics operations: ${text}`) } for (const entityName of ['DateDimension', 'CurrencyDimension', 'ProductDimension', 'SalesOrderItemFact', 'SalesInvoiceItemFact']) { assert.match(businessIntelligenceWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `BI workspace must load real ${entityName} rows from the entity API`) } assert.match(rendererVue, /ErpBirtReportingWorkspace/, 'BIRT pages should use a first-class report publishing workspace') assert.match(rendererVue, /isBirtReportingPage/, 'renderer should detect BIRT routes before plugin/report fallbacks') assert.match(rendererVue, /birtReportingWorkspaceBlock/, 'renderer should collapse BIRT blocks into one report publishing workspace per page') assert.match(rendererVue, /data-modern-birt-reporting-page/, 'renderer should mark page-level BIRT reporting workspaces') for (const text of ['报表发布', '报表库', '主模板', '设计资源', '参数表单', '输出队列', '邮件发送', 'FLEXIBLE_REPORT', 'REPORT_MASTER', 'rptDesign', 'sendBirtMail', 'DataResource', 'Content']) { assert.match(birtReportingWorkspaceVue, new RegExp(text), `BIRT workspace must expose real reporting operations: ${text}`) } for (const entityName of ['Content', 'DataResource', 'ContentAssoc', 'ContentAttribute', 'CustomMethod', 'Enumeration']) { assert.match(birtReportingWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `BIRT workspace must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( birtReportingWorkspaceVue, /便携文档输出|页面输出|电子表格输出|application\/pdf|application\/vnd\.ms-excel/, 'BIRT workspace must not invent report output rows when OFBiz Enumeration data is empty' ) assert.doesNotMatch( birtReportingWorkspaceVue, /预览参数/, 'BIRT reporting workspace should use production report operation language, not preview wording' ) for (const [componentName, workspaceBlock, marker] of [ ['ErpCatalogWorkspace', 'catalogWorkspaceBlock', 'data-modern-catalog-page'], ['ErpCommerceSurface', 'commerceWorkspaceBlock', 'data-modern-commerce-page'], ['ErpPosWorkspace', 'posWorkspaceBlock', 'data-modern-pos-page'], ['ErpManufacturingWorkspace', 'manufacturingWorkspaceBlock', 'data-modern-manufacturing-page'], ['ErpMarketingWorkspace', 'marketingWorkspaceBlock', 'data-modern-marketing-page'], ['ErpReportWorkspace', 'reportWorkspaceBlock', 'data-modern-report-page'], ['ErpPartyWorkspace', 'partyWorkspaceBlock', 'data-modern-party-page'], ['ErpSystemWorkspace', 'systemWorkspaceBlock', 'data-modern-system-admin-page'], ['ErpContentWorkspace', 'contentWorkspaceBlock', 'data-modern-content-page'], ['ErpAssetMaintenanceWorkspace', 'assetMaintenanceWorkspaceBlock', 'data-modern-asset-maintenance-page'], ['ErpFinanceWorkspace', 'financeWorkspaceBlock', 'data-modern-finance-page'], ['ErpFinanceOperationsWorkspace', 'financeOperationsWorkspaceBlock', 'data-modern-finance-operations-page'], ['ErpPortalAdminWorkspace', 'portalAdminWorkspaceBlock', 'data-modern-portal-admin-page'], ['ErpGatewayAdminWorkspace', 'gatewayAdminWorkspaceBlock', 'data-modern-gateway-admin-page'], ['ErpExtensionOperationsWorkspace', 'extensionOperationsWorkspaceBlock', 'data-modern-extension-operations-page'], ['ErpEbayOperationsWorkspace', 'ebayOperationsWorkspaceBlock', 'data-modern-ebay-operations-page'], ['ErpBusinessIntelligenceWorkspace', 'businessIntelligenceWorkspaceBlock', 'data-modern-business-intelligence-page'], ['ErpBirtReportingWorkspace', 'birtReportingWorkspaceBlock', 'data-modern-birt-reporting-page'], ['ErpSalesAutomationWorkspace', 'salesAutomationWorkspaceBlock', 'data-modern-sales-automation-page'], ['ErpProcurementWorkspace', 'procurementWorkspaceBlock', 'data-modern-procurement-page'], ['ErpScrumWorkspace', 'scrumWorkspaceBlock', 'data-modern-scrum-page'] ]) { assert.match(rendererVue, new RegExp(componentName), `renderer should import and mount ${componentName}`) assert.match(rendererVue, new RegExp(workspaceBlock), `renderer should compute a page-level ${workspaceBlock}`) assert.match(rendererVue, new RegExp(marker), `renderer should expose ${marker} for browser verification`) } for (const detectorName of [ 'isCatalogPage', 'isCommercePage', 'isPosPage', 'isManufacturingPage', 'isMarketingPage', 'isReportPage', 'isSystemAdminPage', 'isContentPage', 'isAssetMaintenancePage', 'isSalesAutomationPage', 'isProcurementPage', 'isPortalAdminPage', 'isGatewayAdminPage', 'isExtensionOperationsPage', 'isEbayOperationsPage', 'isBusinessIntelligencePage', 'isPayableReceivablePage', 'isFinancePage', 'isScrumPage' ]) { assert.match(rendererVue, new RegExp(detectorName), `renderer should detect ${detectorName} routes and generated adapters`) } assert.match(rendererVue, /pageId\.startsWith\('ap__'\)/, 'accounts payable pages must be detected as finance operations pages') assert.match(rendererVue, /pageId\.startsWith\('ar__'\)/, 'accounts receivable pages must be detected as finance operations pages') assert.match(rendererVue, /haystack\.includes\('\/ap\/control\/'\)/, 'AP controller routes must route to the finance operations workspace') assert.match(rendererVue, /haystack\.includes\('\/ar\/control\/'\)/, 'AR controller routes must route to the finance operations workspace') assert.match(rendererVue, /isPayableReceivablePage\(\)[\s\S]*block\.type === 'finance-workspace'/, 'AP/AR generated finance blocks must open the finance operations workspace') for (const pageId of [ 'ap__FindApInvoices', 'ap__FindApPayments', 'ar__FindArInvoices', 'ar__FindPayments' ]) { assert.match(captureScreenshotsScript, new RegExp(`#\\/pages\\/${pageId}`), `screenshots must cover ${pageId}`) } assert.match( rendererVue, /function firstWorkspaceBlock\(types: string\[\], preferredNeedles: string\[\] = \[\]\)/, 'renderer should select the page-specific workspace block instead of blindly using the first generated adapter' ) assert.match( rendererVue, /showcart[\s\S]*show cart[\s\S]*\/cart\//, 'commerce cart pages should select the cart surface even when layout sidebars generate catalog surfaces first' ) assert.match( rendererVue, /isInternalLegacyControlTarget\(value\)[\s\S]*#\/pages\/\$\{pageIdFromTarget\(value\)\}/, 'generated page links must convert internal OFBiz /control/ targets back into modern #/pages routes before allowing absolute paths' ) for (const [legacyContext, pagePrefix] of [ ['ordermgr', 'order'], ['partymgr', 'party'], ['sfa', 'SalesForceAutomation'] ]) { assert.match( modernNavigationTs, new RegExp(`${legacyContext}: '${pagePrefix}'`), `generated page links must map /${legacyContext}/control routes to the ${pagePrefix} PageDefinition prefix` ) } assert.match( rendererVue, /keyword[\s\S]*search[\s\S]*category[\s\S]*categories[\s\S]*layered/, 'commerce pages should select search and category surfaces by page semantics' ) for (const [fileName, content] of [ ['ErpPartyWorkspace.vue', partyWorkspaceVue], ['ErpManufacturingWorkspace.vue', manufacturingWorkspaceVue], ['ErpCatalogWorkspace.vue', catalogWorkspaceVue], ['ErpCommerceSurface.vue', commerceSurfaceVue], ['ErpReturnWorkspace.vue', returnWorkspaceVue], ['ErpAdapterBlock.vue', adapterBlockVue], ['ErpDataTable.vue', dataTableVue], ['ErpMarketingWorkspace.vue', marketingWorkspaceVue], ['ErpReportWorkspace.vue', reportWorkspaceVue], ['ErpSystemWorkspace.vue', systemWorkspaceVue], ['ErpContentWorkspace.vue', contentWorkspaceVue], ['ErpAssetMaintenanceWorkspace.vue', assetMaintenanceWorkspaceVue], ['ErpFinanceOperationsWorkspace.vue', financeOperationsWorkspaceVue], ['ErpFinanceWorkspace.vue', financeWorkspaceVue], ['ErpExtensionOperationsWorkspace.vue', extensionOperationsWorkspaceVue], ['ErpEbayOperationsWorkspace.vue', ebayOperationsWorkspaceVue], ['ErpBusinessIntelligenceWorkspace.vue', businessIntelligenceWorkspaceVue], ['ErpBirtReportingWorkspace.vue', birtReportingWorkspaceVue], ['ErpSalesAutomationWorkspace.vue', salesAutomationWorkspaceVue], ['ErpProcurementWorkspace.vue', procurementWorkspaceVue], ['ErpScrumWorkspace.vue', scrumWorkspaceVue], ['ErpMediaWorkspace.vue', mediaWorkspaceVue] ]) { assert.doesNotMatch( content, /旧 widget|PageDefinition|业务 E2E|逐页比对|旧新报表比对|真实输出比对|仍需业务 E2E|按旧 widget|动作契约|不是组件预览|旧 OFBiz/, `${fileName} must read as an ERP operator workspace, not migration or component-preview material` ) } for (const [fileName, content] of [ ['ErpOrderWorkspace.vue', orderWorkspaceVue], ['ErpFinanceOperationsWorkspace.vue', financeOperationsWorkspaceVue], ['ErpFinanceWorkspace.vue', financeWorkspaceVue], ['ErpEbayOperationsWorkspace.vue', ebayOperationsWorkspaceVue], ['ErpBusinessIntelligenceWorkspace.vue', businessIntelligenceWorkspaceVue], ['ErpBirtReportingWorkspace.vue', birtReportingWorkspaceVue], ['ErpSalesAutomationWorkspace.vue', salesAutomationWorkspaceVue], ['ErpInventoryWorkspace.vue', inventoryWorkspaceVue], ['ErpFulfillmentWorkspace.vue', fulfillmentWorkspaceVue], ['ErpPosWorkspace.vue', posWorkspaceVue], ['ErpReturnWorkspace.vue', returnWorkspaceVue], ['ErpCatalogWorkspace.vue', catalogWorkspaceVue], ['ErpCommerceSurface.vue', commerceSurfaceVue], ['ErpManufacturingWorkspace.vue', manufacturingWorkspaceVue], ['ErpWorkManagementWorkspace.vue', workManagementWorkspaceVue], ['ErpHumanResWorkspace.vue', humanResWorkspaceVue], ['ErpPartyWorkspace.vue', partyWorkspaceVue], ['ErpMarketingWorkspace.vue', marketingWorkspaceVue], ['ErpMediaWorkspace.vue', mediaWorkspaceVue], ['ErpReportWorkspace.vue', reportWorkspaceVue], ['ErpSystemWorkspace.vue', systemWorkspaceVue], ['ErpContentWorkspace.vue', contentWorkspaceVue], ['ErpAssetMaintenanceWorkspace.vue', assetMaintenanceWorkspaceVue], ['ErpExtensionOperationsWorkspace.vue', extensionOperationsWorkspaceVue], ['ErpEbayOperationsWorkspace.vue', ebayOperationsWorkspaceVue], ['ErpBusinessIntelligenceWorkspace.vue', businessIntelligenceWorkspaceVue], ['ErpBirtReportingWorkspace.vue', birtReportingWorkspaceVue], ['ErpSalesAutomationWorkspace.vue', salesAutomationWorkspaceVue], ['ErpProcurementWorkspace.vue', procurementWorkspaceVue], ['ErpScrumWorkspace.vue', scrumWorkspaceVue], ['OrderAdminView.vue', orderAdminVue], ['PartyAdminView.vue', partyAdminVue], ['ProductAdminView.vue', productAdminVue], ['AccountingAdminView.vue', accountingAdminVue], ['InventoryAdminView.vue', inventoryAdminVue], ['ManufacturingAdminView.vue', manufacturingAdminVue], ['ErpUpload.vue', uploadVue], ['ErpAdapterBlock.vue', adapterBlockVue], ['ErpDataTable.vue', dataTableVue], ['ErpDrawer.vue', drawerVue] ]) { assert.doesNotMatch( content, /Demo[A-Za-z0-9_]*|\/demo\b|ORD-\d+|INV-\d+|PAY-\d+|SHP-\d+|SO-\d+|TX-\d+|FAC-\d+|GZ-\d+|WG-\d+|KIT-\d+|SRV-\d+|DISC-\d+|SUB-\d+|MAT-\d+|BOM-\d+|PRUN-\d+|PROMO-|EMP-\d+|EMPL-\d+|POS-\d+|COM-\d+|PARTY-\d+|REQ-\d+|CMP-|TRK-|LIST-|CNT-(?:IMG|DOC)-\d+|RetailStore|BigSupplier|Wholesale replenishment|Return authorization|Retail replacement|SalesLead|OpsLead|FinanceLead|example\.com|product-main\.jpg|agreement\.pdf|应收账款|主营收入|应交税费|8850|8230|620/, `${fileName} must not render fixed demo business records in the administrator product` ) } for (const [fileName, content] of [ ['DashboardView.vue', dashboardVue], ['OrderAdminView.vue', orderAdminVue], ['PartyAdminView.vue', partyAdminVue], ['ProductAdminView.vue', productAdminVue], ['AccountingAdminView.vue', accountingAdminVue], ['InventoryAdminView.vue', inventoryAdminVue], ['ManufacturingAdminView.vue', manufacturingAdminVue], ['SystemToolsView.vue', systemVue], ['SystemOperationsView.vue', systemOperationsVue], ['ErpFulfillmentWorkspace.vue', fulfillmentWorkspaceVue], ['ErpWorkManagementWorkspace.vue', workManagementWorkspaceVue], ['ErpAdapterBlock.vue', adapterBlockVue] ]) { assert.doesNotMatch( content, /count:\s*(?:18|7|12|8|6|5)|(?:18|7|12|8|6|5|38|16|186|158\.00|8,850|17\.5)<\/strong>|value:\s*(?:18|7|12|8|6|5),/, `${fileName} must derive operator counts from inventory, navigation, page fields, or actions` ) } for (const [fileName, content] of [ ['ErpReturnWorkspace.vue', returnWorkspaceVue], ['ErpCatalogWorkspace.vue', catalogWorkspaceVue], ['ErpCommerceSurface.vue', commerceSurfaceVue], ['ErpManufacturingWorkspace.vue', manufacturingWorkspaceVue], ['ErpAdapterBlock.vue', adapterBlockVue], ['ErpDataTable.vue', dataTableVue], ['ErpWorkManagementWorkspace.vue', workManagementWorkspaceVue], ['ErpHumanResWorkspace.vue', humanResWorkspaceVue], ['ErpPartyWorkspace.vue', partyWorkspaceVue], ['ErpMarketingWorkspace.vue', marketingWorkspaceVue], ['ErpMediaWorkspace.vue', mediaWorkspaceVue], ['ErpReportWorkspace.vue', reportWorkspaceVue], ['ErpSystemWorkspace.vue', systemWorkspaceVue], ['ErpContentWorkspace.vue', contentWorkspaceVue], ['ErpAssetMaintenanceWorkspace.vue', assetMaintenanceWorkspaceVue], ['ErpExtensionOperationsWorkspace.vue', extensionOperationsWorkspaceVue], ['ErpEbayOperationsWorkspace.vue', ebayOperationsWorkspaceVue], ['ErpBusinessIntelligenceWorkspace.vue', businessIntelligenceWorkspaceVue], ['ErpBirtReportingWorkspace.vue', birtReportingWorkspaceVue], ['ErpSalesAutomationWorkspace.vue', salesAutomationWorkspaceVue], ['ErpProcurementWorkspace.vue', procurementWorkspaceVue], ['ErpScrumWorkspace.vue', scrumWorkspaceVue] ]) { assert.match( content, /fieldRowsFor|actionRowsFor|capabilityRowsFor|pageContextRows|getEntityRows/, `${fileName} should derive rendered rows from OFBiz metadata or real entity APIs` ) } assert.doesNotMatch( businessPageVue, /旧 URL 参数|旧 GET|旧入口兼容|业务回归|功能等价|页面规则|控制规则|待业务回归/, 'generated business pages must keep compatibility and parity wording out of the operator-facing chrome' ) assert.doesNotMatch( displayTs, /旧页面入口|旧屏幕来源|模板适配|模板规则|报表输出'[\s\S]{0,40}'report-preview'|路由工作台|领域工作台|页面定义|屏幕定义|表单定义|菜单定义|控制器 XML|页面规则文件/, 'shared display labels must translate generated OFBiz metadata into administrator-facing ERP language' ) for (const text of [ '应付发票查询', '应付付款查询', '应收发票查询', '应收付款查询', '客户查询', '购物车', '门户页面查询', '报表查询', '销售机会查询', '销售线索查询', '销售预测查询', '采购需求查询', '供应商查询', 'Sprint 排程', '任务板' ]) { assert.match(displayTs, new RegExp(text), `generated OFBiz page titles should be localized for ERP operators: ${text}`) } assert.doesNotMatch( displayTs, /return labels\[lower\] \|\| normalized \|\| raw/, 'page titles should pass through business-language fallback before exposing raw OFBiz names' ) assert.doesNotMatch( rendererVue + adapterBlockVue, /Element Plus rendering|legacy source traceability|media review|approval workflow|规则推断|现代控件渲染|来源可追踪/, 'business page renderer must not expose implementation, migration, or rendering-source labels in operator chrome' ) assert.match(rendererVue, /ErpHumanResWorkspace/, 'human resource pages should use a first-class HR operations workspace') assert.match(rendererVue, /humanResWorkspaceBlock/, 'renderer should collapse HumanRes blocks into one HR workspace per page') assert.match(rendererVue, /renderedBlocks/, 'renderer should avoid duplicate HumanRes workspace rendering through the normal block loop') for (const text of ['人事管理', '员工查询', '员工档案', '雇佣关系', '岗位编制', '人事队列']) { assert.match(humanResWorkspaceVue, new RegExp(text), `HumanRes workspace must expose HR operations: ${text}`) } assert.match(rendererVue, /ErpWorkManagementWorkspace/, 'work effort, project, and scrum pages should use a first-class work management workspace') assert.match(rendererVue, /isWorkManagementPage/, 'renderer should detect WorkEffort and ProjectMgr routes') assert.match(rendererVue, /workManagementWorkspaceBlock/, 'renderer should collapse WorkEffort and ProjectMgr blocks into one work management workspace per page') for (const text of ['项目任务', '任务队列', '项目', '工时', 'Backlog', '排程流转', '执行链路']) { assert.match(workManagementWorkspaceVue, new RegExp(text), `Work management workspace must expose project/task operations: ${text}`) } assert.match(rendererVue, /ErpScrumWorkspace/, 'Scrum pages should use a first-class scrum operations workspace') assert.match(rendererVue, /isScrumPage/, 'renderer should detect Scrum routes before generic work management fallbacks') assert.match(rendererVue, /scrumWorkspaceBlock/, 'renderer should collapse Scrum blocks into one scrum workspace per page') assert.match(rendererVue, /data-modern-scrum-page/, 'renderer should mark page-level Scrum workspaces') assert.match(rendererVue, /if \(isScrumPage\(\)\) return false/, 'generic work management routing must not absorb Scrum pages') for (const pageId of [ 'scrum__Sprints', 'scrum__AddProdBacklog', 'scrum__SprintTask', 'scrum__FindResource', 'scrum__FindTimeSheet', 'scrum__ProductStatistics' ]) { assert.match(captureScreenshotsScript, new RegExp(`#\\/pages\\/${pageId}`), `screenshots must cover ${pageId}`) } for (const text of [ '敏捷交付', '产品 Backlog', 'Sprint', '任务板', '团队资源', '工时/计费', '会议与内容', 'ProductBacklog', 'ProjectSprint', 'ProjectSprintBacklogAndTask', 'ProjectSprintBacklogTaskAndTimeEntryTimeSheet' ]) { assert.match(scrumWorkspaceVue, new RegExp(text), `Scrum workspace must expose real agile delivery operations: ${text}`) } for (const entityName of [ 'Product', 'CustRequest', 'CustRequestItem', 'CustRequestWorkEffort', 'WorkEffort', 'WorkEffortAssoc', 'WorkEffortPartyAssignment', 'Timesheet', 'TimeEntry', 'Party', 'PartyRole', 'CommunicationEvent', 'Content', 'ProductBacklog', 'ProjectSprint', 'ProjectSprintBacklogAndTask', 'ProjectSprintBacklogTaskAndTimeEntryTimeSheet' ]) { assert.match(scrumWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `Scrum workspace must load real ${entityName} rows from the entity API`) } assert.match(scrumWorkspaceVue, /Scrum 业务数据/, 'Scrum workspace must describe entity-backed Scrum data') assert.doesNotMatch( scrumWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|SPRINT-\d+|TASK-\d+|BACKLOG-\d+|USER-\d+|example\.com/, 'Scrum workspace must not synthesize backlog, sprint, task, resource, timesheet, meeting, or billing rows from generated metadata' ) assert.match(rendererVue, /ErpInventoryWorkspace/, 'facility inventory pages should use a first-class inventory operations workspace') assert.match(rendererVue, /isInventoryOperationsPage/, 'renderer should distinguish inventory facility routes from fulfillment routes') assert.match(rendererVue, /inventoryWorkspaceBlock/, 'renderer should collapse inventory facility blocks into one inventory workspace per page') assert.match(rendererVue, /if \(pageId\.startsWith\('facility__'\)\) return false/, 'facility pages must not be routed through the return workspace') assert.match(rendererVue, /packorder[\s\S]*shipment[\s\S]*ship/, 'only pack, shipment, and ship facility routes should use the fulfillment workspace') for (const text of ['库存管理', '库存项', '设施库位', '收货上架', '盘点调整', '库存流转', '库存控制', '履约联动']) { assert.match(inventoryWorkspaceVue, new RegExp(text), `Inventory workspace must expose inventory operations: ${text}`) } assert.match(inventoryWorkspaceVue, /getEntityRows\('InventoryItem'/, 'inventory workspace must load real InventoryItem rows from the entity API') assert.match(inventoryWorkspaceVue, /getEntityRows\('Facility'/, 'inventory workspace must load real Facility rows from the entity API') assert.match(inventoryWorkspaceVue, /getEntityRows\('Shipment'/, 'inventory workspace must load real Shipment rows from the entity API') assert.match(inventoryWorkspaceVue, /库存业务数据/, 'inventory workspace must describe entity-backed inventory data') assert.doesNotMatch( inventoryWorkspaceVue, /42<\/strong>|fieldRows\.value\.map|fieldRows\.value\.slice|pageContextRows\(props\.page, props\.block\)|capabilityRowsFor\(props\.block|页面设施|业务设施|index \+ 1/, 'inventory workspace must not synthesize inventory, facility, receiving, or adjustment rows from generated metadata' ) assert.match(catalogWorkspaceVue, /getEntityRows\('Product'/, 'catalog workspace must load real Product rows from the entity API') assert.match(catalogWorkspaceVue, /getEntityRows\('ProductPrice'/, 'catalog workspace must load real ProductPrice rows from the entity API') assert.match(catalogWorkspaceVue, /getEntityRows\('ProductPromo'/, 'catalog workspace must load real ProductPromo rows from the entity API') assert.match(catalogWorkspaceVue, /getEntityRows\('ProductCategory'/, 'catalog workspace must load real ProductCategory rows from the entity API') assert.match(catalogWorkspaceVue, /商品业务数据/, 'catalog workspace must describe entity-backed product data') assert.doesNotMatch( catalogWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value\.map|字段分组|按业务生效日期|\(index \+ 1\)/, 'catalog workspace must not synthesize products, prices, promos, or category trees from generated metadata' ) assert.match(partyWorkspaceVue, /getEntityRows\('Party'/, 'party workspace must load real Party rows from the entity API') assert.match(partyWorkspaceVue, /getEntityRows\('CommunicationEvent'/, 'party workspace must load real CommunicationEvent rows from the entity API') assert.match(partyWorkspaceVue, /getEntityRows\('CustRequest'/, 'party workspace must load real CustRequest rows from the entity API') assert.match(partyWorkspaceVue, /客户业务数据/, 'party workspace must describe entity-backed party data') assert.doesNotMatch( partyWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|High|Medium|Low/, 'party workspace must not synthesize communications, relationships, or requests from generated metadata' ) assert.match(fulfillmentWorkspaceVue, /getEntityRows\('Shipment'/, 'fulfillment workspace must load real Shipment rows from the entity API') assert.match(fulfillmentWorkspaceVue, /getEntityRows\('InventoryItem'/, 'fulfillment workspace must load real InventoryItem rows from the entity API') assert.match(fulfillmentWorkspaceVue, /履约业务数据/, 'fulfillment workspace must describe entity-backed fulfillment data') assert.doesNotMatch( fulfillmentWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|包裹字段|\bindex \+ 1\b/, 'fulfillment workspace must not synthesize shipments or inventory movements from generated metadata' ) assert.match(returnWorkspaceVue, /getEntityRows\('ReturnHeader'/, 'return workspace must load real ReturnHeader rows from the entity API') assert.match(returnWorkspaceVue, /getEntityRows\('ReturnItem'/, 'return workspace must load real ReturnItem rows from the entity API') assert.match(returnWorkspaceVue, /退货业务数据/, 'return workspace must describe entity-backed return data') assert.doesNotMatch( returnWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|processRowsFor\(|\bindex \+ 1\b/, 'return workspace must not synthesize returns, refunds, or quality checks from generated metadata' ) assert.match(posWorkspaceVue, /getEntityRows\('Product'/, 'POS workspace must load real Product rows from the entity API') assert.match(posWorkspaceVue, /getEntityRows\('Payment'/, 'POS workspace must load real Payment rows from the entity API') assert.match(posWorkspaceVue, /收银业务数据/, 'POS workspace must describe entity-backed POS data') assert.doesNotMatch( posWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|\bindex \+ 1\b|\* 10|tenderedAmount = ref\(200\)/, 'POS workspace must not synthesize cart lines, payments, or tender totals from generated metadata' ) assert.match(manufacturingWorkspaceVue, /getEntityRows\('Requirement'/, 'manufacturing workspace must load real Requirement rows from the entity API') assert.match(manufacturingWorkspaceVue, /getEntityRows\('ProductAssoc'/, 'manufacturing workspace must load real ProductAssoc rows from the entity API') assert.match(manufacturingWorkspaceVue, /getEntityRows\('WorkEffort'/, 'manufacturing workspace must load real WorkEffort rows from the entity API') assert.match(manufacturingWorkspaceVue, /生产业务数据/, 'manufacturing workspace must describe entity-backed production data') assert.doesNotMatch( manufacturingWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|processRowsFor\(|\bindex \+ 1\b/, 'manufacturing workspace must not synthesize MRP, BOM, or production run rows from generated metadata' ) assert.match(marketingWorkspaceVue, /getEntityRows\('MarketingCampaign'/, 'marketing workspace must load real MarketingCampaign rows from the entity API') assert.match(marketingWorkspaceVue, /getEntityRows\('TrackingCode'/, 'marketing workspace must load real TrackingCode rows from the entity API') assert.match(marketingWorkspaceVue, /getEntityRows\('ContactList'/, 'marketing workspace must load real ContactList rows from the entity API') assert.match(marketingWorkspaceVue, /营销业务数据/, 'marketing workspace must describe entity-backed marketing data') assert.doesNotMatch( marketingWorkspaceVue, /发送预览|fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b/, 'marketing workspace must not synthesize campaigns, tracking, or contact lists from generated metadata' ) assert.match(rendererVue, /ErpSalesAutomationWorkspace/, 'SFA pages should use a first-class sales automation workspace') assert.match(rendererVue, /isSalesAutomationPage/, 'renderer should detect SalesForceAutomation and SFA routes before marketing fallbacks') assert.match(rendererVue, /salesAutomationWorkspaceBlock/, 'renderer should collapse SFA blocks into one sales automation workspace per page') assert.match(rendererVue, /data-modern-sales-automation-page/, 'renderer should mark page-level sales automation workspaces') for (const pageId of [ 'SalesForceAutomation__FindSalesOpportunity', 'SalesForceAutomation__FindLeads', 'SalesForceAutomation__FindSalesForecast', 'SalesForceAutomation__ConvertLead' ]) { assert.match(captureScreenshotsScript, new RegExp(`#\\/pages\\/${pageId}`), `screenshots must cover ${pageId}`) } for (const text of [ '销售自动化', '销售线索', '机会管道', '账户/联系人', '销售预测', '跟进事件', '线索转化', 'SalesOpportunity', 'SalesOpportunityStage', 'SalesOpportunityRole', 'SalesForecast', 'SalesForecastDetail', 'PartyRole' ]) { assert.match(salesAutomationWorkspaceVue, new RegExp(text), `sales automation workspace must expose real SFA operations: ${text}`) } for (const entityName of [ 'SalesOpportunity', 'SalesOpportunityStage', 'SalesOpportunityRole', 'SalesForecast', 'SalesForecastDetail', 'Party', 'PartyRole', 'Person', 'PartyGroup', 'CommunicationEvent' ]) { assert.match(salesAutomationWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `sales automation workspace must load real ${entityName} rows from the entity API`) } assert.match(salesAutomationWorkspaceVue, /销售业务数据/, 'sales automation workspace must describe entity-backed sales data') assert.doesNotMatch( salesAutomationWorkspaceVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|LEAD-\d+|OPP-\d+|FORECAST-\d+|SalesLead|OpsLead|FinanceLead|example\.com/, 'sales automation workspace must not synthesize leads, opportunities, accounts, contacts, forecasts, or events from generated metadata' ) assert.match(mediaWorkspaceVue, /getEntityRows\('Content'/, 'media workspace must load real Content rows from the entity API') assert.match(mediaWorkspaceVue, /getEntityRows\('DataResource'/, 'media workspace must load real DataResource rows from the entity API') assert.match(mediaWorkspaceVue, /媒体业务数据/, 'media workspace must describe entity-backed media data') assert.doesNotMatch( mediaWorkspaceVue + rendererVue, /上传、预览|资产预览|asset preview|>\s*预览\s*\s*!booting\.value\s*&&\s*!isAuthenticated\.value\)/, 'administrator site should only open after a real OFBiz session') assert.match(routerTs, /ProductAdminView/, 'administrator site should mount a first-class product administration page') assertRouteMapsToView('#/catalog/products', 'ProductAdminView', 'product administration should be a dedicated administrator route') assert.match(routerTs, /OrderAdminView/, 'administrator site should mount a first-class order operations page') assertRouteMapsToView('#/orders', 'OrderAdminView', 'order operations should be a dedicated administrator route') assert.match(routerTs, /PartyAdminView/, 'administrator site should mount a first-class party administration page') assertRouteMapsToView('#/parties', 'PartyAdminView', 'party administration should be a dedicated administrator route') assert.match(routerTs, /AccountingAdminView/, 'administrator site should mount a first-class accounting administration page') assertRouteMapsToView('#/accounting', 'AccountingAdminView', 'accounting administration should be a dedicated administrator route') assert.match(routerTs, /SalesAdminView/, 'administrator site should mount a first-class sales administration page') assertRouteMapsToView('#/sales', 'SalesAdminView', 'sales administration should be a dedicated administrator route') assert.match(routerTs, /ProcurementAdminView/, 'administrator site should mount a first-class procurement administration page') assertRouteMapsToView('#/procurement', 'ProcurementAdminView', 'procurement administration should be a dedicated administrator route') assert.match(routerTs, /SecurityAdminView/, 'administrator site should mount a first-class security administration page') assertRouteMapsToView('#/system/security', 'SecurityAdminView', 'security administration should be a dedicated administrator route') assert.match(routerTs, /SystemOperationsView/, 'administrator site should mount a first-class system operations page') assertRouteMapsToView('#/system/operations', 'SystemOperationsView', 'system operations should be a dedicated administrator route') assert.match(appVue, /:target-hash="loginTargetHash"/, 'login should preserve the requested administrator deep link after API session login') // DashboardView now injects the session (provide(SessionKey, session) in App.vue) instead of receiving it as a prop. assert.match(appVue, /provide\(SessionKey, session\)/, 'administrator dashboard should receive the current OFBiz session context through provided app state') // The root route is the ERP workbench; unknown authenticated routes fall through the catch-all to NotFoundView. assertRouteMapsToView('/', 'DashboardView', 'the ERP administrator workbench should be the default authenticated landing route') assert.match(routerTs, /path:\s*'\/:pathMatch\(\.\*\)\*'[\s\S]*NotFoundView/, 'unknown authenticated routes should fall through the catch-all instead of a dead view') assert.match(routerTs, /BusinessPageView/, 'generated OFBiz routes should render as business pages, not previews') assert.doesNotMatch(appVue + routerTs, /PagePreviewView|page-preview|previewPageId/, 'administrator product should not keep preview-page route naming') assert.doesNotMatch(routerTs, /'\/components'|'\/patterns'|PatternsView|ComponentsView/, 'component and pattern pages must not be top-level authenticated routes') assert.doesNotMatch( appVue + routerTs, /LegacyRedirectView|DeliveryView|ParityView|ComponentsView|PatternsView|InventoryView/, 'production administrator site must not mount legacy bridge, engineering delivery, parity, component, pattern, or inventory workbenches' ) assert.doesNotMatch(routerTs, /'\/legacy'|'\/delivery'|'\/parity'|'\/components'|'\/patterns'|'\/inventory'/, 'support tools and legacy bridge pages must not be top-level administrator routes') assert.match(shellVue + dashboardVue + moduleCatalog, /#\/catalog\/products/, 'product administration should be reachable from the shell, dashboard, or catalog module') for (const text of ['商品管理台', '商品资料', '目录分类', '价格规则', '促销活动', '库存状态', '商品业务数据']) { assert.match(productAdminVue, new RegExp(text), `product administration page must expose real administrator operation: ${text}`) } for (const entityName of ['Product', 'ProductCategory', 'ProductPrice', 'ProductPromo', 'InventoryItem']) { assertEntitySource(productAdminVue, entityName, `product administration page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( productAdminVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|SKU-\d+|PROD-\d+|RetailStore|BigSupplier|Wholesale replenishment|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'product administration page must not synthesize product rows or expose engineering preview language' ) assert.match(shellVue + dashboardVue + moduleCatalog, /#\/orders/, 'order operations should be reachable from the shell, dashboard, or order module') for (const text of ['订单运营台', '订单执行台', '待审核订单', '待履约订单', '付款关注', '退货风险', '今日处理流', '交接记录', '订单队列', '订单明细', '履约与发运', '退货授权', '订单治理', '订单业务数据']) { assert.match(orderAdminVue, new RegExp(text), `order operations page must expose real administrator operation: ${text}`) } for (const text of ['orderExecutionRows', 'orderFlowRows', 'orderHandoffRows', 'orderRiskRows', 'order-admin-execution', 'order-admin-flow-row', 'order-admin-handoff-row', 'order-admin-risk-row']) { assert.match(orderAdminVue + modernCss, new RegExp(text), `order operations page must behave like an executable order console: ${text}`) } for (const entityName of ['OrderHeader', 'OrderItem', 'OrderStatus', 'Shipment', 'ReturnHeader']) { assert.match(orderAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `order operations page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( orderAdminVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|ORD-\d+|SHP-\d+|RET-\d+|SampleCustomer|Wholesale replenishment|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'order operations page must not synthesize order rows or expose engineering preview language' ) assert.match(shellVue + dashboardVue + moduleCatalog, /#\/sales/, 'sales administration should be reachable from the shell, dashboard, or sales module') for (const text of ['销售管理台', '销售执行台', '线索承接', '机会推进', '预测复核', '跟进交接', '客户角色', '销售异常', '销售线索', '机会管道', '销售预测', '跟进事件', '销售治理', '销售风险', '销售业务数据']) { assert.match(salesAdminVue, new RegExp(text), `sales administration page must expose real administrator operation: ${text}`) } for (const text of ['salesExecutionRows', 'leadHandoffRows', 'opportunityPipelineRows', 'forecastReviewRows', 'communicationFollowupRows', 'salesExceptionRows', 'sales-admin-execution-grid', 'sales-admin-work-row', 'sales-admin-risk-row']) { assert.match(salesAdminVue + modernCss, new RegExp(text), `sales administration page must expose sales operator work surfaces: ${text}`) } for (const entityName of ['SalesOpportunity', 'SalesOpportunityStage', 'SalesOpportunityRole', 'SalesForecast', 'SalesForecastDetail', 'PartyRole', 'CommunicationEvent']) { assert.match(salesAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `sales administration page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( salesAdminVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|LEAD-\d+|SFA-\d+|OPP-\d+|FORECAST-\d+|SampleLead|SampleOpportunity|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'sales administration page must not synthesize sales rows or expose engineering preview language' ) assert.match(shellVue + dashboardVue + moduleCatalog, /#\/procurement/, 'procurement administration should be reachable from the shell, dashboard, or procurement module') for (const text of ['采购管理台', '采购需求', '需求审批', '供应商', '供应商品', '供应商报价', '到货接收', '采购治理', '采购风险', '采购业务数据']) { assert.match(procurementAdminVue, new RegExp(text), `procurement administration page must expose real administrator operation: ${text}`) } for (const entityName of ['Requirement', 'SupplierProduct', 'Vendor', 'Quote', 'CustRequest', 'Shipment', 'InventoryItem']) { assert.match(procurementAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `procurement administration page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( procurementAdminVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|REQ-\d+|PO-\d+|SUP-\d+|VEN-\d+|SampleVendor|SampleSupplier|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'procurement administration page must not synthesize procurement rows or expose engineering preview language' ) assert.match(shellVue + dashboardVue + moduleCatalog, /#\/parties/, 'party administration should be reachable from the shell, dashboard, or party module') for (const text of ['客户与组织管理台', '客户主体', '个人档案', '组织档案', '角色关系', '联系方式', '沟通记录', '客户治理', '客户业务数据']) { assert.match(partyAdminVue, new RegExp(text), `party administration page must expose real administrator operation: ${text}`) } for (const entityName of ['Party', 'Person', 'PartyGroup', 'PartyRole', 'ContactMech', 'CommunicationEvent']) { assertEntitySource(partyAdminVue, entityName, `party administration page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( partyAdminVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|PARTY-\d+|CUST-\d+|ORG-\d+|Person-\d+|SampleCustomer|example\.com|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'party administration page must not synthesize party rows or expose engineering preview language' ) assert.match(shellVue + dashboardVue + moduleCatalog, /#\/accounting/, 'accounting administration should be reachable from the shell, dashboard, or accounting module') for (const text of ['财务管理台', '财务结账台', '应收跟进', '应付安排', '收付款匹配', '核销队列', '现金流关注', '财务异常', '本期结账', '发票队列', '付款收款', '会计凭证', '凭证明细', '总账科目', '核销处理', '账龄风险', '财务业务数据']) { assert.match(accountingAdminVue, new RegExp(text), `accounting administration page must expose real administrator operation: ${text}`) } for (const text of ['financeCloseRows', 'receivableRows', 'payableRows', 'reconciliationRows', 'cashAttentionRows', 'financeExceptionRows', 'accounting-admin-close-grid', 'accounting-admin-work-row', 'accounting-admin-exception-row']) { assert.match(accountingAdminVue + modernCss, new RegExp(text), `accounting administration page must expose finance operator work surfaces: ${text}`) } for (const entityName of ['Invoice', 'Payment', 'AcctgTrans', 'AcctgTransEntry', 'GlAccount']) { assert.match(accountingAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `accounting administration page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( accountingAdminVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|INV-\d+|PAY-\d+|GL-\d+|ACCT-\d+|SampleCustomer|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'accounting administration page must not synthesize finance rows or expose engineering preview language' ) assert.match(shellVue + dashboardVue + moduleCatalog, /#\/procurement/, 'procurement administration should be reachable from the shell, dashboard, or procurement module') for (const text of ['采购管理台', '采购执行台', '需求审批', '询价比价', '供应商风险', '到货接收', '补货交接', '采购异常', '供应商协同', '采购需求', '供应商', '供应商品', '供应商报价', '采购风险', '采购业务数据']) { assert.match(procurementAdminVue, new RegExp(text), `procurement administration page must expose real administrator operation: ${text}`) } for (const text of ['procurementExecutionRows', 'approvalWorkRows', 'quoteComparisonRows', 'supplierRiskRows', 'receivingWorkRows', 'procurementHandoffRows', 'procurement-admin-execution-grid', 'procurement-admin-work-row', 'procurement-admin-risk-row']) { assert.match(procurementAdminVue + modernCss, new RegExp(text), `procurement administration page must expose procurement operator work surfaces: ${text}`) } for (const entityName of ['Requirement', 'SupplierProduct', 'Vendor', 'Quote', 'CustRequest', 'Shipment', 'InventoryItem']) { assert.match(procurementAdminVue, new RegExp(`getEntityRows\\('${entityName}'`), `procurement administration page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( procurementAdminVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|REQ-\d+|PO-\d+|SUP-\d+|VEN-\d+|SampleVendor|SampleSupplier|组件展厅|页面清单|业务等价|待验收|迁移|技术预览/, 'procurement administration page must not synthesize procurement rows or expose engineering preview language' ) assert.doesNotMatch(systemVue, /props\.inventory|UiInventory|routeManifest|counts\?\.routes|counts\?\.actions|counts\?\.services/, 'system maintenance must derive operator signals from navigation/session tools, not UI migration inventory counts') assert.doesNotMatch(systemVue, /业务回归记录|界面组件规范|排版规则|覆盖台账|现代页面|动作契约|#\/system\/delivery|#\/system\/parity|#\/system\/components|#\/system\/patterns|#\/system\/inventory|待补齐事项|pendingE2ePages|customParityPages|missingRoutes|missingActions|历史地址/, 'system maintenance page must not foreground validation, component documentation, inventory counts, compatibility tools, or engineering workbenches') assert.match(systemVue, /#\/system\/security/, 'system maintenance should link to the dedicated security administration page') assert.match(systemVue + shellVue, /#\/system\/operations/, 'system maintenance and shell should link to the dedicated system operations page') assert.doesNotMatch(systemVue, /用户与权限[\s\S]*path:\s*'#\/module\/party'/, 'user and permission administration must not be routed through the party module') for (const text of ['运行日志', '缓存维护', '定时任务', '导入导出', '用户与权限']) { assert.match(systemVue, new RegExp(text), `system maintenance should expose real administrator operation: ${text}`) } assert.doesNotMatch( systemVue + moduleCatalog, /快速进入安全、服务|管理 WebTools|OFBiz 扩展|OFBiz 管理|接口|后端|后台|实体对象|业务对象|业务实体|待处理对象|可处理对象/, 'system maintenance and module directory labels must avoid technical wording in visible product copy' ) for (const text of ['系统运行管理', '计划任务', '任务队列', '缓存维护', '导入导出', '运行资源', '运行审计', '运行业务数据']) { assert.match(systemOperationsVue, new RegExp(text), `system operations page must expose real administrator operation: ${text}`) } for (const entityName of ['JobSandbox', 'SystemProperty', 'DataResource', 'ExcelImportHistory']) { assert.match(systemOperationsVue, new RegExp(`getEntityRows\\('${entityName}'`), `system operations page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( systemOperationsVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|JOB-\d+|CACHE-\d+|IMPORT-\d+|待验收|业务等价|组件展厅|页面清单|迁移|技术预览/, 'system operations page must not synthesize operations or expose engineering preview language' ) for (const text of ['用户与权限管理', '账号与登录', '安全组', '授权关系', '权限审计', '权限业务数据']) { assert.match(securityAdminVue, new RegExp(text), `security administration page must expose real administrator operation: ${text}`) } for (const entityName of ['UserLogin', 'SecurityGroup', 'UserLoginSecurityGroup']) { assertEntitySource(securityAdminVue, entityName, `security administration page must load real ${entityName} rows from the entity API`) } assert.doesNotMatch( securityAdminVue, /fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|USER-\d+|GROUP-\d+|SEC-\d+/, 'security administration page must not synthesize users, groups, or permission links from generated metadata' ) for (const [fileName, content] of [ ['ErpAppShell.vue', shellVue], ['DashboardView.vue', dashboardVue], ['OrderAdminView.vue', orderAdminVue], ['PartyAdminView.vue', partyAdminVue], ['ProductAdminView.vue', productAdminVue], ['AccountingAdminView.vue', accountingAdminVue], ['BusinessCenterView.vue', businessCenterVue], ['SystemToolsView.vue', systemVue], ['SystemOperationsView.vue', systemOperationsVue], ['SecurityAdminView.vue', securityAdminVue], ['ExtensionAdminView.vue', extensionAdminVue], ['ModuleWorkspaceView.vue', moduleVue] ]) { assert.doesNotMatch( content, /组件展厅|交付|验收|迁移|业务等价|覆盖率|覆盖台账|待补齐|旧入口|旧 URL|旧页面|技术预览|页面清单|自动迁移|历史地址/, `${fileName} must not expose preview, migration, parity, compatibility, or component-gallery language in the administrator product` ) } assert.match(packageJson, /verify:admin-product/, 'package scripts should expose product-level admin verification') assert.doesNotMatch( businessPageVue, /前端已重写|适配器已覆盖|页面结构">已生成|界面规则|待输出比对|待验收/, 'generated business pages should not present migration or acceptance wording as product chrome' ) console.log(JSON.stringify({ status: 'passed', checked: [ 'LoginView.vue', 'DashboardView.vue', 'ErpAppShell.vue', 'ModuleWorkspaceView.vue', 'BusinessCenterView.vue', 'SystemToolsView.vue', 'ErpPageRenderer.vue', 'BusinessPageView.vue', 'ErpPartyWorkspace.vue', 'ErpPortalAdminWorkspace.vue', 'ErpGatewayAdminWorkspace.vue', 'ErpHumanResWorkspace.vue', 'ErpWorkManagementWorkspace.vue', 'ErpInventoryWorkspace.vue', 'ErpFulfillmentWorkspace.vue', 'ErpReturnWorkspace.vue', 'ErpCatalogWorkspace.vue', 'ErpCommerceSurface.vue', 'ErpManufacturingWorkspace.vue', 'ErpSystemWorkspace.vue', 'ErpContentWorkspace.vue', 'ErpAdapterBlock.vue', 'ErpDataTable.vue', 'ErpUpload.vue', 'ExtensionAdminView.vue', 'App.vue', 'package.json', 'generated page IDs' ], hardcodedPageIds: hardcodedPageIds.size }, null, 2))