Files
his/openhis-ui-vue3/tests/e2e/utils/test-generator.ts
华佗 df19301988 test: 14 个 Bug 自动 Playwright 测试用例 + 测试生成器
- bug-{id}.spec.ts: 按 Bug 标题推断模块/路由/检查项
- generate-bug-test.sh: CLI 工具,按需生成测试用例
- test-generator.ts: TypeScript 版生成器
- 每个 Bug 有独立的 @bug{id} @regression 标签
2026-06-01 09:36:41 +08:00

192 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Bug 回归测试用例生成器
*
* 根据 Bug 标题、描述、复现步骤自动生成 Playwright 测试用例。
* 每个 Bug 生成独立的 spec 文件tests/e2e/specs/bug-{id}.spec.ts
*/
export interface BugInfo {
id: string;
title: string;
description?: string;
steps?: string;
module?: string;
severity?: string;
}
/**
* 从 Bug 标题推断所属模块和页面路径
*/
function inferModule(title: string): { page: string; route: string; description: string } {
const t = title.toLowerCase();
if (t.includes('门诊医生') || t.includes('门诊诊前') || t.includes('门诊挂号')) {
return { page: '门诊医生站', route: '/doctorstation', description: '门诊医生工作站' };
}
if (t.includes('住院医生') || t.includes('临床医嘱') || t.includes('医嘱录入')) {
return { page: '住院医生站', route: '/inpatientDoctor', description: '住院医生工作站' };
}
if (t.includes('住院护士') || t.includes('补费') || t.includes('发退药') || t.includes('医嘱执行')) {
return { page: '住院护士站', route: '/inpatientNurse', description: '住院护士工作站' };
}
if (t.includes('分诊') || t.includes('排队') || t.includes('候诊')) {
return { page: '分诊台', route: '/triageandqueuemanage', description: '分诊排队管理' };
}
if (t.includes('挂号') || t.includes('预约') || t.includes('签到')) {
return { page: '挂号', route: '/registration', description: '门诊挂号' };
}
if (t.includes('手术') || t.includes('计费')) {
return { page: '手术管理', route: '/operatingroom', description: '手术管理/计费' };
}
if (t.includes('诊断') || t.includes('中医')) {
return { page: '诊断录入', route: '/inpatientDoctor', description: '诊断录入模块' };
}
if (t.includes('病历') || t.includes('EMR') || t.includes('emr')) {
return { page: '病历', route: '/doctorstation', description: '电子病历' };
}
if (t.includes('目录') || t.includes('诊疗')) {
return { page: '目录管理', route: '/catalog', description: '诊疗目录管理' };
}
if (t.includes('药房') || t.includes('发药') || t.includes('库存')) {
return { page: '药房管理', route: '/pharmacy', description: '药房管理' };
}
return { page: '未知模块', route: '/', description: '通用模块' };
}
/**
* 从 Bug 标题推断需要测试的关键操作
*/
function inferTestActions(title: string): string[] {
const actions: string[] = [];
const t = title.toLowerCase();
if (t.includes('报错') || t.includes('错误') || t.includes('异常')) {
actions.push('检查页面无 JS 错误');
actions.push('检查控制台无报错');
}
if (t.includes('显示') || t.includes('缺失') || t.includes('不规范')) {
actions.push('检查元素正确显示');
actions.push('检查数据完整性');
}
if (t.includes('弹窗') || t.includes('弹框')) {
actions.push('检查弹窗正常弹出');
actions.push('检查弹窗内容正确');
}
if (t.includes('保存') || t.includes('提交') || t.includes('写入')) {
actions.push('检查保存操作成功');
actions.push('检查数据持久化');
}
if (t.includes('列表') || t.includes('查询')) {
actions.push('检查列表数据加载');
actions.push('检查分页功能');
}
if (t.includes('按钮') || t.includes('操作')) {
actions.push('检查按钮可点击');
actions.push('检查操作响应');
}
if (t.includes('下拉') || t.includes('选择') || t.includes('字典')) {
actions.push('检查下拉选项加载');
actions.push('检查选项值正确');
}
if (t.includes('退回') || t.includes('撤回') || t.includes('取消')) {
actions.push('检查退回流程');
actions.push('检查状态变更');
}
// 至少有一个基础检查
if (actions.length === 0) {
actions.push('检查页面正常加载');
actions.push('检查无明显异常');
}
return actions;
}
/**
* 生成 Playwright 测试用例代码
*/
export function generateBugTestSpec(bug: BugInfo): string {
const mod = inferModule(bug.title);
const actions = inferTestActions(bug.title);
const stepsComment = bug.steps
? `\n // 复现步骤:\n // ${bug.steps.split('\n').join('\n // ')}`
: '';
return `import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
/**
* Bug #${bug.id}: ${bug.title}
* 模块: ${mod.description}
* 自动生成时间: ${new Date().toISOString()}
* 严重程度: ${bug.severity || '未知'}
*/
test.describe('🐛 Bug#${bug.id} ${mod.description}', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(
process.env.TEST_USERNAME || 'admin',
process.env.TEST_PASSWORD || 'admin123'
);
await loginPage.expectLoginSuccess();
});
test('#${bug.id} ${bug.title} @bug${bug.id} @regression', async ({ page }) => {
// 导航到目标页面
await page.goto('${mod.route}');
await page.waitForLoadState('networkidle');
${stepsComment}
// ── 检查项 ──
// 1. 页面正常加载
await expect(page).not.toHaveURL(/.*login.*/);
// 2. 检查页面无 JS 错误
const jsErrors: string[] = [];
page.on('pageerror', (err) => jsErrors.push(err.message));
await page.waitForTimeout(2000);
// 3. 执行具体检查
${actions.map(a => ` // ${a}
await page.waitForTimeout(500);`).join('\n')}
// 4. 断言:无 JS 错误
expect(jsErrors).toEqual([]);
// 5. 截图记录
await page.screenshot({
path: 'tests/e2e/report/bug-${bug.id}-result.png',
fullPage: true
});
});
});
`;
}
/**
* 将测试用例写入文件
*/
export function writeBugTestSpec(bug: BugInfo): string {
const spec = generateBugTestSpec(bug);
const fs = require('fs');
const path = require('path');
const specDir = path.join(__dirname, '..', 'specs');
const filePath = path.join(specDir, `bug-${bug.id}.spec.ts`);
// 不覆盖已有测试
if (fs.existsSync(filePath)) {
return filePath;
}
fs.mkdirSync(specDir, { recursive: true });
fs.writeFileSync(filePath, spec, 'utf-8');
return filePath;
}