#!/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. */ export const globalAdminForbiddenText = [ 'Element Plus 定制 ERP 管理界面', 'Generated Element Plus preview', 'Migration Foundation', 'Page Definition', 'Storybook', '不是页面预览', '业务回归记录', '业务等价', '原应用清单', '技术预览', '旧 URL', '旧入口', '界面方案', '组件展示页', '组件展厅', '组件规范', '覆盖台账', '迁移', '页面标识', '页面清单', '待验收', '历史地址' ] const requiredDefaultRuntimeCaseIds = [ 'login-door', 'admin-workbench', 'order-admin', 'party-admin', 'product-admin', 'accounting-admin', 'inventory-admin', 'manufacturing-admin', 'humanres-admin', 'sales-admin', 'procurement-admin', 'scrum-admin', 'operations-admin', 'content-admin', 'marketing-admin', 'commerce-admin', 'pos-admin', 'marketplace-admin', 'analytics-admin', 'extension-admin', 'global-business-center', 'route-unavailable', 'system-maintenance', 'security-admin', 'system-operations' ] // The professional shell drives navigation from a grouped left sidebar tree // (module = el-sub-menu, its quick pages = leaf items). These maps express the // expected active module label (the sub-menu title) or the expected active // top-level item for routes that are not nested under a module. const moduleNavLabelByPath = new Map([ ['#/orders', '订单'], ['#/catalog/products', '商品'], ['#/parties', '客户'], ['#/sales', '销售'], ['#/procurement', '采购'], ['#/accounting', '财务'], ['#/facility', '库存'], ['#/manufacturing', '生产'], ['#/humanres', '人事'], ['#/scrum', '敏捷'], ['#/operations', '运营'], ['#/content', '内容'], ['#/marketing', '营销'], ['#/commerce', '电商'], ['#/marketplace', '店铺'], ['#/pos', 'POS'], ['#/analytics', '报表'], ['#/extensions', '配置'] ]) const moduleNavLabelById = new Map([ ['order', '订单'], ['catalog', '商品'], ['party', '客户'], ['sales', '销售'], ['procurement', '采购'], ['accounting', '财务'], ['facility', '库存'], ['manufacturing', '生产'], ['humanres', '人事'], ['scrum', '敏捷'], ['operations', '运营'], ['content', '内容'], ['marketing', '营销'], ['commerce', '电商'], ['marketplace', '店铺'], ['pos', 'POS'], ['analytics', '报表'], ['system-admin', '运维'], ['extensions', '配置'] ]) const topLevelNavByPath = new Map([ ['#/', '运营工作台'], ['#/system', '系统管理'], ['#/system/security', '账号与角色'], ['#/system/operations', '运行监控'] ]) const procurementPageIds = new Set([ 'order__FindRequirements', 'order__ApproveRequirements', 'order__ApprovedProductRequirementsByVendor', 'ap__FindVendors', 'catalog__EditSupplierProduct', 'facility__ReceiveInventoryAgainstPurchaseOrder' ]) // Page id prefix -> owning module nav label, mirroring moduleCatalog prefixes. const pagePrefixNavLabel = [ ['SalesForceAutomation__', '销售'], ['accounting__', '财务'], ['ap__', '财务'], ['ar__', '财务'], ['order__', '订单'], ['catalog__', '商品'], ['party__', '客户'], ['facility__', '库存'], ['manufacturing__', '生产'], ['humanres__', '人事'], ['scrum__', '敏捷'], ['workeffort__', '运营'], ['projectmgr__', '运营'], ['content__', '内容'], ['marketing__', '营销'], ['ecommerce__', '电商'], ['ebay__', '店铺'], ['ebaystore__', '店铺'], ['webpos__', 'POS'], ['bi__', '报表'], ['birt__', '报表'], ['webtools__', '运维'], ['ofbizsetup__', '运维'], ['assetmaint__', '运维'], ['myportal__', '运维'], ['example__', '配置'], ['exampleext__', '配置'], ['pricat__', '配置'], ['pricatdemo__', '配置'], ['firstdata__', '配置'], ['msggateway__', '配置'], ['ismgr__', '配置'], ['scrumdemo__', '配置'] ] export function expectedRouteHash(route) { const value = String(route || '#/') const hashIndex = value.indexOf('#') if (hashIndex >= 0) return value.slice(hashIndex) return value.startsWith('/') ? `#${value}` : `#/${value.replace(/^\/+/, '')}` } function pathOnly(route) { return expectedRouteHash(route).replace(/\?.*$/, '') } function pageIdForRoute(route) { const path = pathOnly(route) if (!path.startsWith('#/pages/')) return '' return decodeURIComponent(path.slice('#/pages/'.length)) } /** * The expected active navigation target for a route: * - { topLevel } when a top-level sidebar item should be active (workbench, system) * - { moduleLabel } when a module sub-menu should be the active/open group * - null when the route has no dedicated sidebar entry (business center, unknown) */ export function expectedSideNavForRoute(route) { const path = pathOnly(route) if (topLevelNavByPath.has(path)) return { topLevel: topLevelNavByPath.get(path) } if (moduleNavLabelByPath.has(path)) return { moduleLabel: moduleNavLabelByPath.get(path) } if (path.startsWith('#/module/')) { const moduleId = decodeURIComponent(path.slice('#/module/'.length)) const label = moduleNavLabelById.get(moduleId) return label ? { moduleLabel: label } : null } const pageId = pageIdForRoute(route) if (pageId) { if (procurementPageIds.has(pageId)) return { moduleLabel: '采购' } const match = pagePrefixNavLabel.find(([prefix]) => pageId.startsWith(prefix)) if (match) return { moduleLabel: match[1] } } return null } // Back-compat export: returns the single expected nav label for a route, or ''. export function expectedTopMenuTextForRoute(route) { const expected = expectedSideNavForRoute(route) if (!expected) return '' return expected.topLevel || expected.moduleLabel || '' } function normalizeText(value) { return String(value || '').replace(/\s+/g, '').trim() } function hasNormalizedText(items, expected) { const normalizedExpected = normalizeText(expected) return items.some((item) => normalizeText(item.text).includes(normalizedExpected)) } export function runtimeRouteHashWaitExpression(route) { return `location.hash === ${JSON.stringify(expectedRouteHash(route))}` } export function adminRuntimePolicySnapshotExpression(route) { return `(() => { const forbiddenTerms = ${JSON.stringify(globalAdminForbiddenText)} const linkSnapshot = (selector) => Array.from(document.querySelectorAll(selector)).map((node) => ({ text: String(node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim(), href: node.getAttribute('href') || '', className: String(node.className || '') })) const text = document.body ? document.body.innerText : '' const commandInput = document.querySelector('.erp-command input') const activeSubMenu = document.querySelector('.erp-nav .el-sub-menu.is-active > .el-sub-menu__title') || document.querySelector('.erp-nav .el-sub-menu.is-opened > .el-sub-menu__title') return { expectedHash: ${JSON.stringify(expectedRouteHash(route))}, hash: location.hash, href: location.href, textLength: text.trim().length, shellText: text.slice(0, 2000), forbiddenVisible: forbiddenTerms.filter((term) => text.includes(term)), hasApp: Boolean(document.querySelector('#app')), hasAdminShell: Boolean(document.querySelector('.erp-shell')), hasAside: Boolean(document.querySelector('.erp-shell__aside')), hasTopbar: Boolean(document.querySelector('.erp-shell__topbar')), hasMain: Boolean(document.querySelector('.erp-shell__main')), hasCommand: Boolean(document.querySelector('.erp-command')), commandPlaceholder: commandInput ? commandInput.getAttribute('placeholder') || '' : '', hasQuickActions: Boolean(document.querySelector('.erp-quick-actions')), hasSession: Boolean(document.querySelector('.erp-session')), sessionText: document.querySelector('.erp-session')?.innerText || '', hasSidebarNav: Boolean(document.querySelector('.erp-nav')), sideActive: linkSnapshot('.erp-nav .el-menu-item.is-active'), sideActiveModule: activeSubMenu ? String(activeSubMenu.innerText || '').replace(/\\s+/g, ' ').trim() : '', loginDoorVisible: text.includes('登录 ERP 后台') || text.includes('使用 OFBiz 账号继续') } })()` } export function assertAdminRuntimeCaseCoverage(cases, options = {}) { const selectedIds = new Set((cases || []).map((testCase) => testCase.id)) const missing = requiredDefaultRuntimeCaseIds.filter((id) => !selectedIds.has(id)) if (options.enforceDefaultCoverage && missing.length) { throw new Error(`Default admin runtime coverage is missing required case(s): ${missing.join(', ')}`) } return { id: 'admin-runtime-default-coverage', status: missing.length ? 'skipped' : 'passed', required: requiredDefaultRuntimeCaseIds, missing } } export function assertAdminRuntimePolicy(snapshot, testCase = {}) { const route = testCase.route || snapshot?.expectedHash || '#/' const session = testCase.session || 'admin' const failures = [] const expectedHash = expectedRouteHash(route) if (!snapshot || typeof snapshot !== 'object') { failures.push('runtime policy snapshot is missing') } else { if (!snapshot.hasApp) failures.push('Vue app root is missing') if (snapshot.hash !== expectedHash) { failures.push(`route hash mismatch: expected ${expectedHash}, got ${snapshot.hash || '-'}`) } if (!snapshot.textLength) failures.push('document body has no visible text') } if (snapshot && session === 'admin') { const requiredFlags = [ ['hasAdminShell', 'administrator shell'], ['hasAside', 'administrator sidebar'], ['hasSidebarNav', 'administrator sidebar navigation tree'], ['hasTopbar', 'administrator topbar'], ['hasMain', 'administrator main content'], ['hasCommand', 'administrator command search'], ['hasQuickActions', 'administrator quick actions'], ['hasSession', 'administrator session area'] ] for (const [flag, label] of requiredFlags) { if (!snapshot[flag]) failures.push(`${label} is missing`) } for (const text of ['OFBiz ERP', '统一运营管理平台', '运营工作台', '核心业务', '渠道与扩展', '系统治理', '新建', '待办', '在线']) { if (!String(snapshot.shellText || '').includes(text)) failures.push(`administrator shell text is missing: ${text}`) } if (!String(snapshot.commandPlaceholder || '').includes('搜索订单、商品、客户、发票')) { failures.push('administrator command search placeholder is missing business-object wording') } if (snapshot.loginDoorVisible) failures.push('administrator route rendered the login door') if (snapshot.forbiddenVisible?.length) { failures.push(`forbidden preview/migration copy is visible: ${snapshot.forbiddenVisible.join(', ')}`) } const expectedNav = expectedSideNavForRoute(route) const sideActive = Array.isArray(snapshot.sideActive) ? snapshot.sideActive : [] if (expectedNav?.topLevel) { if (sideActive.length !== 1) { failures.push(`sidebar should have exactly one active item for ${expectedHash}; active=${sideActive.map((item) => item.text).join(', ') || '-'}`) } if (!hasNormalizedText(sideActive, expectedNav.topLevel)) { failures.push(`sidebar active item should be ${expectedNav.topLevel}; active=${sideActive.map((item) => item.text).join(', ') || '-'}`) } } else if (expectedNav?.moduleLabel) { if (sideActive.length !== 1) { failures.push(`sidebar should have exactly one active leaf for ${expectedHash}; active=${sideActive.map((item) => item.text).join(', ') || '-'}`) } if (!normalizeText(snapshot.sideActiveModule).includes(normalizeText(expectedNav.moduleLabel))) { failures.push(`sidebar active module should be ${expectedNav.moduleLabel}; activeModule=${snapshot.sideActiveModule || '-'}`) } } } if (snapshot && session !== 'admin' && snapshot.hasAdminShell) { failures.push('guest route rendered the administrator shell') } return { id: 'administrator-runtime-policy', status: failures.length ? 'failed' : 'passed', route: expectedHash, session, expectedNav: session === 'admin' ? expectedSideNavForRoute(route) : null, sideActive: Array.isArray(snapshot?.sideActive) ? snapshot.sideActive : [], sideActiveModule: snapshot?.sideActiveModule || '', forbiddenVisible: Array.isArray(snapshot?.forbiddenVisible) ? snapshot.forbiddenVisible : [], failures } }