恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2047 lines
129 KiB
JavaScript
2047 lines
129 KiB
JavaScript
#!/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, /<ErpAppShell[\s\S]*v-else/, 'authenticated product UI must render inside the ERP application shell after the login gate')
|
|
assert.match(appVue, /if \(sessionData\.authenticated\) \{\s*await loadAuthenticatedData\(\)/, 'authenticated session refresh must load administrator navigation and inventory only after login')
|
|
assert.match(appVue, /loadAuthenticatedData[\s\S]*getNavigation\(\)[\s\S]*getInventory\(\)/, 'post-login product load must hydrate backend navigation and generated business page definitions')
|
|
assertRouteMapsToView('#/business', 'BusinessCenterView', 'post-login product must include a business center route')
|
|
assertRouteMapsToView('#/pages/:pageId(.*)', 'BusinessPageView', 'post-login product must render generated OFBiz business pages through BusinessPageView')
|
|
// BusinessPageView now reads pageId/query from the route and inventory from inject() instead of :page-id/:inventory props.
|
|
assert.match(businessPageVue, /route\.params\.pageId/, 'business pages must derive their page id from the route')
|
|
assert.match(businessPageVue, /inject\(InventoryKey/, 'business pages must read inventory from injected app state instead of a :inventory prop')
|
|
assert.match(shellVue + dashboardVue + businessCenterVue, /\/business/, 'post-login product chrome must expose the global business center anchor')
|
|
|
|
for (const anchor of postLoginCoreModuleAnchors) {
|
|
assert.match(routerTs, new RegExp(`${anchor.component}`), `${anchor.sourceName} must be mounted in the authenticated administrator app via the route table`)
|
|
assertRouteMapsToView(
|
|
anchor.route,
|
|
anchor.component,
|
|
`${anchor.route} must resolve to ${anchor.activeView} after login`
|
|
)
|
|
assert.match(
|
|
shellVue + dashboardVue + moduleCatalog,
|
|
new RegExp(escapeRegExp(routerPath(anchor.route))),
|
|
`${anchor.route} must be reachable from post-login shell, dashboard, or module catalog anchors`
|
|
)
|
|
for (const label of anchor.labels) {
|
|
assert.match(anchor.source, new RegExp(escapeRegExp(label)), `${anchor.sourceName} must expose product UI anchor: ${label}`)
|
|
}
|
|
for (const entityName of anchor.entities) {
|
|
assertEntitySource(anchor.source, entityName, `${anchor.sourceName} must load real ${entityName} rows for the product anchor`)
|
|
}
|
|
}
|
|
|
|
// The sidebar el-menu renders every module landing + quickPage as a `/pages/<pageId>`
|
|
// 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*</,
|
|
'administrator shell should expose work queues instead of generic entry/catalog language'
|
|
)
|
|
assert.doesNotMatch(
|
|
shellVue,
|
|
/authorizedApplications\s*=\s*computed|后端授权/,
|
|
'administrator shell must not render raw OFBiz application lists as the administrator navigation surface'
|
|
)
|
|
// The professional sidebar derives its business-domain tree from the module catalog
|
|
// (primary/secondary modules grouped into 核心业务 / 渠道与扩展) rather than from a
|
|
// raw backend-authorization list, and routes each module through its dedicated
|
|
// modern administrator landing page via moduleLandingPath().
|
|
assert.match(shellVue, /navGroups\s*=\s*computed\([\s\S]*primaryBusinessModules[\s\S]*secondaryBusinessModules/, 'administrator shell should derive authorized business domains from the module catalog')
|
|
assert.match(shellVue, /primaryBusinessModules\s*=\s*computed\(\(\)\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>业务应用<\/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*</,
|
|
'generated business pages must use operator-facing workbench language instead of metadata, capability, or flow-ledger chrome'
|
|
)
|
|
assert.doesNotMatch(
|
|
businessPageVue,
|
|
/权限映射|NONE|可用工具|查询 \/ 列表 \/ 查找 \/ 上传/,
|
|
'generated business pages must not expose permission-map fallbacks or tool-catalog wording in the operator chrome'
|
|
)
|
|
assert.doesNotMatch(
|
|
rendererVue,
|
|
/page\.acceptance|业务状态[\s\S]*待处理配置|需专属业务流程|需复核/,
|
|
'business page renderer must derive visible status from business structure, not migration acceptance state'
|
|
)
|
|
assert.doesNotMatch(
|
|
rendererVue,
|
|
/ElMessage|条件已准备/,
|
|
'page-level business action feedback must be a persistent operation receipt instead of transient toast feedback'
|
|
)
|
|
for (const text of ['处理回执', '处理编号', '提交内容', '下一步处理', 'recentActionReceipts']) {
|
|
assert.match(rendererVue, new RegExp(text), `page-level action feedback must read as an ERP operation receipt: ${text}`)
|
|
}
|
|
assert.doesNotMatch(
|
|
searchFormVue,
|
|
/ElMessage|条件已就绪/,
|
|
'generated ERP forms must let the page-level Action API result own submit feedback'
|
|
)
|
|
assert.doesNotMatch(
|
|
entityFormVue,
|
|
/ElMessage|表单数据已收集/,
|
|
'generated ERP entity forms must let the page-level Action API result own submit feedback'
|
|
)
|
|
assert.match(
|
|
rendererVue,
|
|
/const actionPayload = actionPayloadFor\(payload\)[\s\S]*runAction\(actionId, actionPayload\)/,
|
|
'generated ERP forms with service or event targets must submit through the Action API bridge'
|
|
)
|
|
assert.doesNotMatch(dataTableVue, /previewRows|sampleValue|等待后端数据|待加载/, 'ERP data tables must not synthesize placeholder business rows')
|
|
assert.match(dataTableVue, /emptyDescription[\s\S]*业务数据/, 'ERP data tables must render unavailable or empty states instead of fake rows')
|
|
assert.match(dataTableVue, /visibleColumns\.value\.slice\(0,\s*12\)/, 'ERP data tables must cap visible columns at 12 before rendering')
|
|
assert.match(dataTableVue, /isUnavailableData[\s\S]*remoteState\.value === 'unavailable'[\s\S]*remoteState\.value === 'error'/, 'ERP data tables must only show unavailable alerts for unavailable or error states')
|
|
assert.match(adapterBlockVue, /dataSourceForBlock|业务数据源/, 'generic adapter blocks should bridge to real data sources or honest unavailable states')
|
|
assert.doesNotMatch(
|
|
adapterBlockVue,
|
|
/页面条件|可执行入口|业务数据源需要页面定义|待配置|流程入口/,
|
|
'generic adapter blocks must translate generated metadata into ERP workbench language before rendering it'
|
|
)
|
|
assert.doesNotMatch(
|
|
domainAdminVue,
|
|
/个来源|不可用来源/,
|
|
'domain administrator pages must present loaded and unavailable business areas instead of source-count diagnostics'
|
|
)
|
|
for (const text of ['domainWorkQueueRows', 'recentBusinessRows', 'handoffRows', 'businessActionRows', '业务队列', '业务记录', '快捷处理', '异常交接', '处理流', '进入处理']) {
|
|
assert.match(domainAdminVue, new RegExp(text), `shared domain administrator renderer must behave like an ERP operation console: ${text}`)
|
|
}
|
|
assert.doesNotMatch(
|
|
domainAdminVue,
|
|
/组件展厅|页面清单|业务等价|待验收|迁移|技术预览|预览/,
|
|
'shared domain administrator renderer must not expose component, preview, migration, or parity language'
|
|
)
|
|
assert.doesNotMatch(
|
|
domainAdminVue,
|
|
/接口|后端|对象|实体对象|业务对象|业务实体|待处理对象|可处理对象/,
|
|
'shared domain administrator renderer must describe visible records and tables instead of technical object/entity wording'
|
|
)
|
|
assert.doesNotMatch(
|
|
adapterBlockVue,
|
|
/fieldRows\.value\.map|fieldRows\.value\.slice|actionRows\.value\.map|capabilityRows\.value|capabilityRowsFor\(|processRowsFor\(|pageContextRows\(|\bindex \+ 1\b|阶段 \$\{/,
|
|
'generic adapter blocks must not synthesize business rows from generated metadata'
|
|
)
|
|
assert.doesNotMatch(apiTs, /API_TIMEOUT_MS\s*=\s*900/, 'production ERP API client must not use preview-grade 900ms timeouts')
|
|
assert.match(apiTs, /READ_API_TIMEOUT_MS\s*=\s*(?:[89]\d{3}|[1-9]\d{4,})/, 'read API calls must allow enough time for real OFBiz entity, page, and lookup responses')
|
|
assert.match(apiTs, /ACTION_API_TIMEOUT_MS\s*=\s*(?:3\d{4}|[4-9]\d{4,})/, 'Action API calls must allow enough time for real OFBiz service and event execution')
|
|
assert.match(apiTs, /LOOKUP_API_TIMEOUT_MS\s*=\s*(?:1[2-9]\d{3}|[2-9]\d{4,})/, 'lookup and dynamic option calls must allow enough time for real OFBiz searches')
|
|
assert.match(apiTs, /runAction[\s\S]*ACTION_API_TIMEOUT_MS/, 'service and event actions must use the action timeout instead of read timeout behavior')
|
|
assert.match(parityScript, /openModernUiRuntime\(chrome\)/, 'parity smoke must open one reusable browser render runtime')
|
|
assert.match(parityScript, /verifyModernUiRuntime\(page, modernRuntime\)/, 'parity smoke must verify modern business pages through a browser render pass')
|
|
assert.match(parityScript, /renderMode: 'chrome-cdp'/, 'parity smoke must record Chrome/CDP rendering evidence for modern UI pages')
|
|
assert.doesNotMatch(
|
|
parityScript,
|
|
/response\.ok && text\.includes\('<div id="app">'\)/,
|
|
'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)|<strong>(?: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,
|
|
/<strong>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*</,
|
|
'media upload workspaces must use production media operation language, not preview wording'
|
|
)
|
|
assert.doesNotMatch(
|
|
mediaWorkspaceVue,
|
|
/fieldRows\.value\.map|字段 \$\{index \+ 1\}|\bindex \+ 1\b/,
|
|
'media workspace must not synthesize assets from generated metadata'
|
|
)
|
|
|
|
assert.match(reportWorkspaceVue, /getEntityRows\('AcctgTransEntry'/, 'report workspace must load real AcctgTransEntry rows from the entity API')
|
|
assert.match(reportWorkspaceVue, /getEntityRows\('Invoice'/, 'report workspace must load real Invoice rows from the entity API')
|
|
assert.match(reportWorkspaceVue, /getEntityRows\('Payment'/, 'report workspace must load real Payment rows from the entity API')
|
|
assert.match(reportWorkspaceVue, /报表业务数据/, 'report workspace must describe entity-backed report data')
|
|
assert.doesNotMatch(
|
|
reportWorkspaceVue,
|
|
/fieldRows\.value\.map|字段 \$\{index \+ 1\}|\bindex \+ 1\b|index % 2/,
|
|
'report workspace must not synthesize accounting lines from generated form fields'
|
|
)
|
|
|
|
for (const text of ['系统管理', '资料维护', '运行任务', '安全与权限', '运行日志', '标签资源', '导入导出', '资源与数据文件']) {
|
|
assert.match(systemWorkspaceVue, new RegExp(text), `system workspace must expose administrator operations: ${text}`)
|
|
}
|
|
for (const entityName of [
|
|
'UserLogin',
|
|
'SecurityGroup',
|
|
'UserLoginSecurityGroup',
|
|
'EntityAuditLog',
|
|
'SystemProperty',
|
|
'VisualTheme',
|
|
'JobSandbox',
|
|
'DataResource',
|
|
'FileExtension',
|
|
'ExcelImportHistory'
|
|
]) {
|
|
assert.match(systemWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `system workspace must load real ${entityName} rows from the entity API`)
|
|
}
|
|
assert.match(
|
|
systemWorkspaceVue,
|
|
/实时日志需授权查看|受权限保护的日志读取服务/,
|
|
'system workspace must show an honest unavailable state for runtime logs instead of fake log rows'
|
|
)
|
|
assert.match(
|
|
systemWorkspaceVue,
|
|
/实时运行信息需授权查看|受权限保护的运行服务/,
|
|
'system workspace must show an honest unavailable state for service runtime details'
|
|
)
|
|
assert.doesNotMatch(
|
|
systemWorkspaceVue,
|
|
/Backend|接口尚未开放|后端运行接口|暂未接入|标签 API|实体 API|Action API/,
|
|
'system workspace must not expose backend/API implementation wording in the administrator product'
|
|
)
|
|
assert.doesNotMatch(
|
|
systemVue + moduleCatalog,
|
|
/快速进入安全、服务|管理 WebTools|OFBiz 扩展|接口|后端|后台|实体对象|业务对象|业务实体|待处理对象|可处理对象/,
|
|
'system tools and module catalog copy must use administrator-facing task, record, and connection language'
|
|
)
|
|
assert.doesNotMatch(
|
|
systemWorkspaceVue,
|
|
/fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|SRV-\d+|LOG-\d+/,
|
|
'system workspace must not synthesize system records from generated metadata'
|
|
)
|
|
|
|
for (const text of ['内容管理', '内容库', '站点/CMS', '博客/论坛', '媒体资源', '发布队列', 'SEO/路径']) {
|
|
assert.match(contentWorkspaceVue, new RegExp(text), `content workspace must expose content operations: ${text}`)
|
|
}
|
|
for (const entityName of [
|
|
'Content',
|
|
'DataResource',
|
|
'ContentAssoc',
|
|
'ContentRole',
|
|
'ElectronicText',
|
|
'CommunicationEvent',
|
|
'WebSite',
|
|
'WebSiteContent',
|
|
'WebSitePathAlias',
|
|
'ContentKeyword'
|
|
]) {
|
|
assert.match(contentWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `content workspace must load real ${entityName} rows from the entity API`)
|
|
}
|
|
assert.match(contentWorkspaceVue, /内容业务数据/, 'content workspace must describe entity-backed content data')
|
|
assert.match(contentWorkspaceVue, /发布任务和静态页生成需要发布权限/, 'content workspace must show an honest unavailable state for publish runtime details')
|
|
assert.doesNotMatch(
|
|
contentWorkspaceVue,
|
|
/fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|CNT-\d+|BLOG-\d+|CMS-\d+/,
|
|
'content workspace must not synthesize content, CMS, blog, forum, media, publish, or SEO rows from generated metadata'
|
|
)
|
|
|
|
for (const text of ['资产维护', '资产维护', 'IT 资产', '工单排程', '备件领用', '维护记录']) {
|
|
assert.match(assetMaintenanceWorkspaceVue, new RegExp(text), `asset maintenance workspace must expose maintenance operations: ${text}`)
|
|
}
|
|
for (const entityName of [
|
|
'FixedAssetMaint',
|
|
'FixedAsset',
|
|
'WorkEffort',
|
|
'WorkEffortAndPartyAssign',
|
|
'WorkEffortAndTimeEntry',
|
|
'WorkEffortNote',
|
|
'ItemIssuance',
|
|
'ProductMaint'
|
|
]) {
|
|
assert.match(assetMaintenanceWorkspaceVue, new RegExp(`getEntityRows\\('${entityName}'`), `asset maintenance workspace must load real ${entityName} rows from the entity API`)
|
|
}
|
|
assert.match(assetMaintenanceWorkspaceVue, /资产维护业务数据/, 'asset maintenance workspace must describe entity-backed asset maintenance data')
|
|
assert.doesNotMatch(
|
|
assetMaintenanceWorkspaceVue,
|
|
/fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|\bindex \+ 1\b|Demo[A-Za-z0-9_]*|ASSET-\d+|HW-\d+|SW-\d+|WO-\d+/,
|
|
'asset maintenance workspace must not synthesize maintenance, hardware, software, work effort, or issuance rows from generated metadata'
|
|
)
|
|
|
|
assert.match(commerceSurfaceVue, /getEntityRows\('Product'/, 'commerce surface must load real Product rows from the entity API')
|
|
assert.match(commerceSurfaceVue, /getEntityRows\('ProductCategory'/, 'commerce surface must load real ProductCategory rows from the entity API')
|
|
assert.match(commerceSurfaceVue, /getEntityRows\('ProductPromo'/, 'commerce surface must load real ProductPromo rows from the entity API')
|
|
assert.match(commerceSurfaceVue, /电商业务数据/, 'commerce surface must describe entity-backed storefront data')
|
|
assert.doesNotMatch(
|
|
commerceSurfaceVue,
|
|
/fieldRows\.value\.map|actionRows\.value\.map|capabilityRows\.value|fieldRows\.value\.slice|\bindex \+ 1\b|428 : 36/,
|
|
'commerce surface must not synthesize carts, product cards, trees, or contact/community counts from generated metadata'
|
|
)
|
|
|
|
for (const text of ['业务队列', '交接记录', '处理流', '执行动作', '处理事项', '复核交接', '异常交接']) {
|
|
assert.match(moduleVue, new RegExp(text), `module workspaces must behave like business consoles: ${text}`)
|
|
}
|
|
|
|
assert.match(appVue, /showLoginGate/, 'app should keep login/session gate before the administrator site')
|
|
assert.match(appVue, /showLoginGate\s*=\s*computed\(\(\)\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))
|