背景
ApprovalTreeMenu 作为审批中心核心组件,历史上多次出现菜单乱跳、节点重复、切换误匹配等问题(参见 steedos/steedos-plugins#491、#594)。为方便回归验证和生产环境现场定位,需要将经过验证的浏览器控制台 stress test 脚本纳入仓库管理。
脚本说明
- 使用方式:在审批中心页面打开浏览器 DevTools Console,粘贴脚本并回车执行
- 不依赖特定数据库/环境:脚本只操作当前页面已渲染的 DOM 元素(点击树节点、检查选中态、检测重复分类),任何环境打开审批中心页面即可运行
- 不破坏数据:仅做 DOM 读取和点击操作,不调用后端 API、不修改任何数据
- 已验证:50 次随机点击全部通过,0 异常
建议存放路径
packages/@steedos-widgets/amis-object/src/components/__tests__/approval-tree-menu-stress.js
任务
将下面的脚本内容创建为上述路径的文件,文件头部需要加上注释说明(用途、使用方式、注意事项)
脚本内容
/**
* ApprovalTreeMenu 左侧审批菜单 Stress Test 脚本
*
* 用途:随机批量点击审批中心左侧树菜单节点,自动检测以下异常:
* 1. 重复分类节点(同一根节点下同名分类出现多次)
* 2. 选中态是否正确
* 3. URL 是否合理变化
* 4. 随机展开/折叠节点
*
* 使用方式:
* 1. 在浏览器中打开审批中心页面(如 /app/approve_workflow/...)
* 2. 按 F12 打开 DevTools → Console 标签页
* 3. 粘贴本脚本全部内容并按回车执行
* 4. 等待执行完毕,查看控制台输出的测试报告
*
* 注意事项:
* - 本脚本仅操�� DOM,不调用后端 API、不修改任何数据
* - 不依赖特定数据库或环境,任何有审批中心页面的环境均可运行
* - 不适用于 Node.js / Jest / Vitest 等测试框架直接运行
* - 如需 E2E 自动化,建议后续迁移为 Playwright 测试用例
* - 可通过修改 CLICK_COUNT、MIN_DELAY、MAX_DELAY 调整测试强度
*/
(async () => {
const CLICK_COUNT = 50;
const MIN_DELAY = 300;
const MAX_DELAY = 1500;
const errors = [];
const delay = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => delay(MIN_DELAY + Math.random() * (MAX_DELAY - MIN_DELAY));
const getTreeNodes = () => {
return Array.from(document.querySelectorAll('.approval-tree-menu .ant-tree-treenode'));
};
const getSelectedTitle = () => {
const sel = document.querySelector('.approval-tree-menu .ant-tree-treenode-selected .approval-tree-menu__label');
return sel ? sel.textContent.trim() : '(none)';
};
// 修正:按根节点分组检测重复分类
// 跨根节点的同名分类是合法的(如"待审核"和"监控箱"下都有"信息管理部")
// 只有同一个根节点下出现同名分类才是 bug
const checkDuplicateCategories = () => {
const dupes = [];
const treeListInner = document.querySelector('.approval-tree-menu .ant-tree-list-holder-inner');
if (!treeListInner) return dupes;
const allTreeNodes = Array.from(treeListInner.children);
let currentRootLabel = '';
const rootGroupMap = {};
allTreeNodes.forEach(node => {
const indent = node.querySelectorAll('.ant-tree-indent-unit').length;
const label = node.querySelector('.approval-tree-menu__label')?.textContent?.trim() || '';
if (indent === 0) {
currentRootLabel = label;
rootGroupMap[currentRootLabel] = [];
} else if (indent === 1 && currentRootLabel) {
const isGroup = node.querySelector('.approval-tree-menu__label--group');
if (isGroup) {
rootGroupMap[currentRootLabel].push(label);
}
}
});
Object.entries(rootGroupMap).forEach(([root, labels]) => {
const seen = {};
labels.forEach(label => {
seen[label] = (seen[label] || 0) + 1;
if (seen[label] > 1) {
dupes.push(`[${root}] ${label}`);
}
});
});
return [...new Set(dupes)];
};
const getListInfo = () => {
const listTitle = document.querySelector('.cxd-Page-title')?.textContent?.trim() || '';
const rowCount = document.querySelectorAll('.cxd-Table-row, .steedos-object-listview-row, tr[data-index]').length;
return { listTitle, rowCount };
};
console.log(`%c🚀 开始自动化测试:${CLICK_COUNT} 次随机点击`, 'color: #1677ff; font-size: 14px; font-weight: bold;');
let prevUrl = location.href;
for (let i = 0; i < CLICK_COUNT; i++) {
const nodes = getTreeNodes();
if (nodes.length === 0) {
console.warn(`[${i+1}] ⚠️ 找不到树节点,等待重试...`);
await delay(2000);
continue;
}
const randomIdx = Math.floor(Math.random() * nodes.length);
const targetNode = nodes[randomIdx];
const titleEl = targetNode.querySelector('.approval-tree-menu__label');
const clickTarget = targetNode.querySelector('.ant-tree-title') || targetNode.querySelector('.ant-tree-node-content-wrapper');
const targetTitle = titleEl ? titleEl.textContent.trim() : '(unknown)';
if (clickTarget) {
clickTarget.click();
}
await randomDelay();
const selectedTitle = getSelectedTitle();
const currentUrl = location.href;
const dupes = checkDuplicateCategories();
const listInfo = getListInfo();
const selectionMismatch = titleEl && !targetNode.closest('.ant-tree-treenode')?.classList.contains('ant-tree-treenode-selected')
&& clickTarget && targetNode.querySelector('.approval-tree-menu__label--item');
if (dupes.length > 0) {
const errMsg = `重复分类节点: ${dupes.join(', ')}`;
errors.push({ click: i + 1, target: targetTitle, error: errMsg });
console.error(`[${i+1}] ❌ ${errMsg}`);
}
const urlChanged = currentUrl !== prevUrl;
const status = dupes.length > 0 ? '❌' : '✅';
console.log(
`[${i+1}/${CLICK_COUNT}] ${status} 点击: "${targetTitle}" | 选中: "${selectedTitle}" | URL变化: ${urlChanged} | 列表行数: ${listInfo.rowCount}`
);
prevUrl = currentUrl;
if (Math.random() < 0.2) {
const switchers = Array.from(document.querySelectorAll('.approval-tree-menu .ant-tree-switcher:not(.ant-tree-switcher-noop)'));
if (switchers.length > 0) {
const randomSwitcher = switchers[Math.floor(Math.random() * switchers.length)];
randomSwitcher.click();
await delay(200);
console.log(` ↳ 展开/折叠了一个节点`);
}
}
}
console.log('\n%c📊 测试报告', 'color: #1677ff; font-size: 16px; font-weight: bold;');
console.log(`总点击次数: ${CLICK_COUNT}`);
console.log(`发现异常数: ${errors.length}`);
if (errors.length > 0) {
console.table(errors);
} else {
console.log('%c✅ 全部通过,未发现异常', 'color: #52c41a; font-size: 14px;');
}
})();
背景
ApprovalTreeMenu 作为审批中心核心组件,历史上多次出现菜单乱跳、节点重复、切换误匹配等问题(参见 steedos/steedos-plugins#491、#594)。为方便回归验证和生产环境现场定位,需要将经过验证的浏览器控制台 stress test 脚本纳入仓库管理。
脚本说明
建议存放路径
任务
将下面的脚本内容创建为上述路径的文件,文件头部需要加上注释说明(用途、使用方式、注意事项)
脚本内容