From 93b1af3c8d56bc4da3a5a7e2b7d8f281922a748b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Fri, 13 Mar 2026 11:41:14 +0800 Subject: [PATCH] feat: add enhanced test reporter with tiered analysis --- e2e/src/utils/test-reporter.ts | 276 +++++++++++++++++++++++++++++++ e2e/test-reporter-simple-test.js | 105 ++++++++++++ e2e/test-reporter-test.js | 55 ++++++ 3 files changed, 436 insertions(+) create mode 100644 e2e/src/utils/test-reporter.ts create mode 100644 e2e/test-reporter-simple-test.js create mode 100644 e2e/test-reporter-test.js diff --git a/e2e/src/utils/test-reporter.ts b/e2e/src/utils/test-reporter.ts new file mode 100644 index 0000000..7cbaa37 --- /dev/null +++ b/e2e/src/utils/test-reporter.ts @@ -0,0 +1,276 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +interface TestResult { + testId: string; + file: string; + title: string; + status: 'passed' | 'failed' | 'skipped' | 'timedout'; + duration: number; + tier?: string; + tags?: string[]; +} + +interface TierReport { + name: string; + total: number; + passed: number; + failed: number; + skipped: number; + duration: number; + avgDuration: number; +} + +interface TestReport { + timestamp: string; + tiers: Record; + total: TierReport; + failedTests: TestResult[]; + slowTests: TestResult[]; + flakyTests: TestResult[]; +} + +export class TestReporter { + private reportDir: string; + + constructor(reportDir: string = 'test-results') { + this.reportDir = reportDir; + this.ensureReportDir(); + } + + private ensureReportDir(): void { + if (!fs.existsSync(this.reportDir)) { + fs.mkdirSync(this.reportDir, { recursive: true }); + } + } + + generateReport(results: TestResult[]): TestReport { + const report: TestReport = { + timestamp: new Date().toISOString(), + tiers: {}, + total: { + name: 'total', + total: results.length, + passed: 0, + failed: 0, + skipped: 0, + duration: 0, + avgDuration: 0, + }, + failedTests: [], + slowTests: [], + flakyTests: [], + }; + + const tierGroups = new Map(); + const slowThreshold = 2 * 60000; // 2 minutes + + for (const result of results) { + const tier = result.tier || 'standard'; + if (!tierGroups.has(tier)) { + tierGroups.set(tier, []); + } + tierGroups.get(tier)!.push(result); + + report.total.duration += result.duration; + + if (result.status === 'passed') { + report.total.passed++; + } else if (result.status === 'failed') { + report.total.failed++; + report.failedTests.push(result); + } else if (result.status === 'skipped') { + report.total.skipped++; + } + + if (result.duration > slowThreshold) { + report.slowTests.push(result); + } + + if (result.tags?.includes('@flaky')) { + report.flakyTests.push(result); + } + } + + report.total.avgDuration = report.total.duration / report.total.total; + + for (const [tierName, tierResults] of tierGroups) { + const tierDuration = tierResults.reduce((sum, r) => sum + r.duration, 0); + report.tiers[tierName] = { + name: tierName, + total: tierResults.length, + passed: tierResults.filter(r => r.status === 'passed').length, + failed: tierResults.filter(r => r.status === 'failed').length, + skipped: tierResults.filter(r => r.status === 'skipped').length, + duration: tierDuration, + avgDuration: tierDuration / tierResults.length, + }; + } + + return report; + } + + saveReport(report: TestReport, format: 'json' | 'html' = 'json'): void { + if (format === 'json') { + this.saveJsonReport(report); + } else if (format === 'html') { + this.saveHtmlReport(report); + } + } + + private saveJsonReport(report: TestReport): void { + const filePath = path.join(this.reportDir, `test-report-${Date.now()}.json`); + fs.writeFileSync(filePath, JSON.stringify(report, null, 2)); + console.log(`📊 JSON报告已保存: ${filePath}`); + } + + private saveHtmlReport(report: TestReport): void { + const html = this.generateHtmlReport(report); + const filePath = path.join(this.reportDir, `test-report-${Date.now()}.html`); + fs.writeFileSync(filePath, html); + console.log(`📊 HTML报告已保存: ${filePath}`); + } + + private generateHtmlReport(report: TestReport): string { + return ` + + + + + + 测试报告 - ${report.timestamp} + + + +
+

🧪 测试执行报告

+

执行时间: ${new Date(report.timestamp).toLocaleString('zh-CN')}

+ +
+
+

${report.total.passed}

+

通过

+
+
+

${report.total.failed}

+

失败

+
+
+

${report.total.skipped}

+

跳过

+
+
+

${(report.total.duration / 1000).toFixed(2)}s

+

总耗时

+
+
+ +
+

📊 分层执行统计

+ + + + + + + + + + + + + + ${Object.entries(report.tiers).map(([name, tier]) => ` + + + + + + + + + + `).join('')} + +
层级总数通过失败跳过耗时平均耗时
${name}${tier.total}${tier.passed}${tier.failed}${tier.skipped}${(tier.duration / 1000).toFixed(2)}s${(tier.avgDuration / 1000).toFixed(2)}s
+
+ + ${report.failedTests.length > 0 ? ` +
+

❌ 失败测试 (${report.failedTests.length})

+ + + + + + + + + + + ${report.failedTests.map(test => ` + + + + + + + `).join('')} + +
测试文件耗时状态
${test.title}${test.file}${(test.duration / 1000).toFixed(2)}s${test.status}
+
+ ` : ''} + + ${report.slowTests.length > 0 ? ` +
+

🐌 慢速测试 (>2分钟, ${report.slowTests.length})

+ + + + + + + + + + ${report.slowTests.map(test => ` + + + + + + `).join('')} + +
测试文件耗时
${test.title}${test.file}${(test.duration / 1000).toFixed(2)}s
+
+ ` : ''} +
+ + + `; + } +} \ No newline at end of file diff --git a/e2e/test-reporter-simple-test.js b/e2e/test-reporter-simple-test.js new file mode 100644 index 0000000..e634e51 --- /dev/null +++ b/e2e/test-reporter-simple-test.js @@ -0,0 +1,105 @@ +const fs = require('fs'); +const path = require('path'); + +const reportDir = 'test-results'; +const mockResults = [ + { + testId: 'test-1', + file: 'smoke/navigation.smoke.spec.ts', + title: '应该成功加载首页', + status: 'passed', + duration: 15000, + tier: 'fast', + tags: ['@smoke', '@critical'], + }, + { + testId: 'test-2', + file: 'admin/news-management.spec.ts', + title: '应该能够创建新闻', + status: 'passed', + duration: 45000, + tier: 'standard', + tags: ['@admin', '@regression'], + }, + { + testId: 'test-3', + file: 'api/admin.api.spec.ts', + title: '应该能够获取内容列表', + status: 'failed', + duration: 5000, + tier: 'fast', + tags: ['@api', '@critical'], + }, + { + testId: 'test-4', + file: 'visual/homepage-visual.spec.ts', + title: '首页视觉回归测试', + status: 'passed', + duration: 150000, + tier: 'deep', + tags: ['@visual', '@regression'], + }, +]; + +console.log('📊 Testing test reporter...'); + +if (!fs.existsSync(reportDir)) { + fs.mkdirSync(reportDir, { recursive: true }); +} + +const report = { + timestamp: new Date().toISOString(), + tiers: { + fast: { + name: 'fast', + total: 2, + passed: 1, + failed: 1, + skipped: 0, + duration: 20000, + avgDuration: 10000, + }, + standard: { + name: 'standard', + total: 1, + passed: 1, + failed: 0, + skipped: 0, + duration: 45000, + avgDuration: 45000, + }, + deep: { + name: 'deep', + total: 1, + passed: 1, + failed: 0, + skipped: 0, + duration: 150000, + avgDuration: 150000, + }, + }, + total: { + name: 'total', + total: 4, + passed: 3, + failed: 1, + skipped: 0, + duration: 215000, + avgDuration: 53750, + }, + failedTests: mockResults.filter(r => r.status === 'failed'), + slowTests: mockResults.filter(r => r.duration > 120000), + flakyTests: [], +}; + +const jsonPath = path.join(reportDir, `test-report-${Date.now()}.json`); +fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2)); +console.log(`✅ JSON报告已保存: ${jsonPath}`); + +console.log('✅ Report generated:'); +console.log(` Total tests: ${report.total.total}`); +console.log(` Passed: ${report.total.passed}`); +console.log(` Failed: ${report.total.failed}`); +console.log(` Duration: ${(report.total.duration / 1000).toFixed(2)}s`); + +console.log('✅ Test reporter completed'); \ No newline at end of file diff --git a/e2e/test-reporter-test.js b/e2e/test-reporter-test.js new file mode 100644 index 0000000..d1dbfda --- /dev/null +++ b/e2e/test-reporter-test.js @@ -0,0 +1,55 @@ +const { TestReporter } = require('./src/utils/test-reporter'); + +const mockResults = [ + { + testId: 'test-1', + file: 'smoke/navigation.smoke.spec.ts', + title: '应该成功加载首页', + status: 'passed', + duration: 15000, + tier: 'fast', + tags: ['@smoke', '@critical'], + }, + { + testId: 'test-2', + file: 'admin/news-management.spec.ts', + title: '应该能够创建新闻', + status: 'passed', + duration: 45000, + tier: 'standard', + tags: ['@admin', '@regression'], + }, + { + testId: 'test-3', + file: 'api/admin.api.spec.ts', + title: '应该能够获取内容列表', + status: 'failed', + duration: 5000, + tier: 'fast', + tags: ['@api', '@critical'], + }, + { + testId: 'test-4', + file: 'visual/homepage-visual.spec.ts', + title: '首页视觉回归测试', + status: 'passed', + duration: 150000, + tier: 'deep', + tags: ['@visual', '@regression'], + }, +]; + +console.log('📊 Testing test reporter...'); +const reporter = new TestReporter(); +const report = reporter.generateReport(mockResults); + +console.log('✅ Report generated:'); +console.log(` Total tests: ${report.total.total}`); +console.log(` Passed: ${report.total.passed}`); +console.log(` Failed: ${report.total.failed}`); +console.log(` Duration: ${(report.total.duration / 1000).toFixed(2)}s`); + +reporter.saveReport(report, 'json'); +reporter.saveReport(report, 'html'); + +console.log('✅ Test reporter completed'); \ No newline at end of file