/** * 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; }