Files
gym-manage/gym-manage-web/e2e/customReporter.ts
T
张翔 d2cef85187 docs: add test report and database reset scripts
- Add comprehensive test report (TEST_REPORT.md)
- Add database reset scripts for testing
- Update .gitignore to exclude temporary files
- Add frontend e2e test utilities and configuration
2026-04-23 16:36:12 +08:00

430 lines
14 KiB
TypeScript

import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
import * as fs from 'fs';
import * as path from 'path';
class CustomReporter implements Reporter {
private results: Map<string, TestCase[]> = new Map();
private suiteResults: Map<string, Suite> = new Map();
private startTime: number = Date.now();
private testResults: TestResult[] = [];
onBegin(config: FullConfig) {
console.log(`🚀 开始测试执行: ${config.projects.map(p => p.name).join(', ')}`);
this.startTime = Date.now();
}
onTestBegin(test: TestCase, result: TestResult) {
console.log(`📝 开始测试: ${test.title}`);
}
onTestEnd(test: TestCase, result: TestResult) {
console.log(`✅ 测试完成: ${test.title} - ${result.status}`);
this.testResults.push(result);
}
onEnd(result: FullResult) {
const endTime = Date.now();
const duration = endTime - this.startTime;
console.log(`🎉 测试执行完成`);
console.log(`⏱️ 总耗时: ${this.formatDuration(duration)}`);
const stats = this.calculateStats(result);
this.generateConsoleReport(stats);
this.generateHtmlReport(result, stats);
this.generateJsonReport(result, stats);
}
private calculateStats(result: FullResult): TestStats {
const allTests = this.testResults;
if (allTests.length === 0) {
return {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
flaky: 0,
passRate: 0,
failRate: 0,
skipRate: 0,
flakyRate: 0,
totalDuration: 0,
avgDuration: 0,
slowestTests: [],
failedTests: [],
};
}
const passed = allTests.filter(t => t.status === 'passed');
const failed = allTests.filter(t => t.status === 'failed');
const skipped = allTests.filter(t => t.status === 'skipped');
const flaky = allTests.filter(t => t.status === 'passed' && t.retry >= 1);
const totalDuration = allTests.reduce((sum, t) => sum + t.duration, 0);
const avgDuration = totalDuration / allTests.length;
const passRate = (passed.length / allTests.length) * 100;
const failRate = (failed.length / allTests.length) * 100;
const skipRate = (skipped.length / allTests.length) * 100;
const flakyRate = (flaky.length / allTests.length) * 100;
return {
total: allTests.length,
passed: passed.length,
failed: failed.length,
skipped: skipped.length,
flaky: flaky.length,
passRate,
failRate,
skipRate,
flakyRate,
totalDuration,
avgDuration,
slowestTests: allTests
.filter(t => t.duration > 0)
.sort((a, b) => b.duration - a.duration)
.slice(0, 10),
failedTests: failed,
};
}
private generateConsoleReport(stats: TestStats) {
console.log('');
console.log('═══════════════════════════════════════════');
console.log('📊 测试统计报告');
console.log('═══════════════════════════════════════════');
console.log('');
console.log(`📈 总测试数: ${stats.total}`);
console.log(`✅ 通过: ${stats.passed} (${stats.passRate.toFixed(2)}%)`);
console.log(`❌ 失败: ${stats.failed} (${stats.failRate.toFixed(2)}%)`);
console.log(`⏭️ 跳过: ${stats.skipped} (${stats.skipRate.toFixed(2)}%)`);
console.log(`🔄 不稳定: ${stats.flaky} (${stats.flakyRate.toFixed(2)}%)`);
console.log('');
console.log(`⏱️ 总耗时: ${this.formatDuration(stats.totalDuration)}`);
console.log(`⏱️ 平均耗时: ${this.formatDuration(stats.avgDuration)}`);
console.log('');
console.log('🐌 最慢的10个测试:');
stats.slowestTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.title} - ${this.formatDuration(test.duration || 0)}`);
});
console.log('');
if (stats.failedTests.length > 0) {
console.log('❌ 失败的测试:');
stats.failedTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.title || '未命名测试'}`);
if (test.location?.file) {
console.log(` 位置: ${test.location.file}:${test.location.line || 0}`);
}
if (test.error?.message) {
console.log(` 错误: ${test.error.message}`);
}
});
console.log('');
}
}
private generateHtmlReport(result: FullResult, stats: TestStats) {
const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试报告 - Novalon管理系统</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.header h1 {
margin: 0;
color: #333;
font-size: 28px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.stat-card h3 {
margin: 0 0 10px 0;
font-size: 14px;
opacity: 0.9;
}
.stat-card .value {
font-size: 32px;
font-weight: bold;
margin: 0;
}
.stat-card .label {
font-size: 12px;
opacity: 0.8;
margin-top: 5px;
}
.stat-card.passed {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.stat-card.failed {
background: linear-gradient(135deg, #ef4444 0%, #f44336 100%);
}
.stat-card.flaky {
background: linear-gradient(135deg, #f59e0b 0%, #f093fb 100%);
}
.section {
margin-bottom: 30px;
}
.section h2 {
color: #333;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
margin-bottom: 20px;
}
.test-list {
list-style: none;
padding: 0;
}
.test-item {
padding: 15px;
margin-bottom: 10px;
border-left: 4px solid #ddd;
background: #f9f9f9;
border-radius: 5px;
}
.test-item.passed {
border-left-color: #38ef7d;
background: #f0fff4;
}
.test-item.failed {
border-left-color: #ef4444;
background: #fff5f5;
}
.test-item.skipped {
border-left-color: #f59e0b;
background: #fef9c3;
}
.test-item.flaky {
border-left-color: #f093fb;
background: #fef3c7;
}
.test-item .test-name {
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.test-item .test-duration {
color: #666;
font-size: 12px;
}
.test-item .test-error {
color: #ef4444;
font-size: 12px;
margin-top: 5px;
padding: 10px;
background: #fee;
border-radius: 3px;
}
.progress-bar {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
transition: width 0.5s ease;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #666;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 Novalon管理系统测试报告</h1>
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
<div class="stats-grid">
<div class="stat-card passed">
<h3>通过测试</h3>
<div class="value">${stats.passed}</div>
<div class="label">${stats.passRate.toFixed(2)}%</div>
</div>
<div class="stat-card failed">
<h3>失败测试</h3>
<div class="value">${stats.failed}</div>
<div class="label">${stats.failRate.toFixed(2)}%</div>
</div>
<div class="stat-card flaky">
<h3>不稳定测试</h3>
<div class="value">${stats.flaky}</div>
<div class="label">${stats.flakyRate.toFixed(2)}%</div>
</div>
<div class="stat-card">
<h3>总测试数</h3>
<div class="value">${stats.total}</div>
<div class="label">100%</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-bar-fill" style="width: ${stats.passRate}%"></div>
</div>
<div class="section">
<h2>📈 测试统计</h2>
<ul class="test-list">
<li class="test-item">
<div class="test-name">总耗时</div>
<div class="test-duration">${this.formatDuration(stats.totalDuration)}</div>
</li>
<li class="test-item">
<div class="test-name">平均耗时</div>
<div class="test-duration">${this.formatDuration(stats.avgDuration)}</div>
</li>
<li class="test-item">
<div class="test-name">跳过测试</div>
<div class="test-duration">${stats.skipped} (${stats.skipRate.toFixed(2)}%)</div>
</li>
</ul>
</div>
${stats.failedTests.length > 0 ? `
<div class="section">
<h2>❌ 失败测试详情</h2>
<ul class="test-list">
${stats.failedTests.map(test => `
<li class="test-item failed">
<div class="test-name">${test.title}</div>
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
<div class="test-error">
<strong>错误:</strong> ${test.error?.message || '未知错误'}
</div>
</li>
`).join('')}
</ul>
</div>
` : ''}
<div class="section">
<h2>🐌 最慢的10个测试</h2>
<ul class="test-list">
${stats.slowestTests.map((test, index) => `
<li class="test-item ${test.status}">
<div class="test-name">${index + 1}. ${test.title}</div>
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
</li>
`).join('')}
</ul>
</div>
<div class="footer">
<p>🧪 Novalon管理系统 - 自动化测试报告</p>
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
</div>
</body>
</html>
`;
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.html');
fs.writeFileSync(reportPath, html, 'utf-8');
console.log(`📄 HTML报告已生成: ${reportPath}`);
}
private generateJsonReport(result: FullResult, stats: TestStats) {
const report = {
summary: {
timestamp: new Date().toISOString(),
total: stats.total,
passed: stats.passed,
failed: stats.failed,
skipped: stats.skipped,
flaky: stats.flaky,
passRate: stats.passRate,
failRate: stats.failRate,
skipRate: stats.skipRate,
flakyRate: stats.flakyRate,
totalDuration: stats.totalDuration,
avgDuration: stats.avgDuration,
},
failedTests: stats.failedTests.map(test => ({
title: test.title,
location: test.location,
error: test.error?.message,
duration: test.duration,
})),
slowestTests: stats.slowestTests.map(test => ({
title: test.title,
duration: test.duration,
})),
};
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
console.log(`📄 JSON报告已生成: ${reportPath}`);
}
private formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
return `${(ms / 60000).toFixed(1)}m`;
}
}
}
interface TestStats {
total: number;
passed: number;
failed: number;
skipped: number;
flaky: number;
passRate: number;
failRate: number;
skipRate: number;
flakyRate: number;
totalDuration: number;
avgDuration: number;
slowestTests: TestCase[];
}
export default CustomReporter;