08ea5fbe98
添加用户管理视图、API和状态管理文件
530 lines
16 KiB
TypeScript
530 lines
16 KiB
TypeScript
import { test } from '@playwright/test';
|
|
import { testLogger } from '../shared/utils/test-logger';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
export interface TestCoverage {
|
|
totalTests: number;
|
|
passedTests: number;
|
|
failedTests: number;
|
|
skippedTests: number;
|
|
passRate: number;
|
|
testSuites: TestSuiteCoverage[];
|
|
executionTime: number;
|
|
timestamp: string;
|
|
}
|
|
|
|
export interface TestSuiteCoverage {
|
|
name: string;
|
|
totalTests: number;
|
|
passedTests: number;
|
|
failedTests: number;
|
|
skippedTests: number;
|
|
passRate: number;
|
|
tests: TestCaseCoverage[];
|
|
}
|
|
|
|
export interface TestCaseCoverage {
|
|
name: string;
|
|
status: 'passed' | 'failed' | 'skipped';
|
|
duration: number;
|
|
tags: string[];
|
|
file: string;
|
|
}
|
|
|
|
export class TestCoverageReporter {
|
|
private coverageData: TestCoverage;
|
|
private testResults: Map<string, TestCaseCoverage[]> = new Map();
|
|
private suiteResults: Map<string, TestSuiteCoverage> = new Map();
|
|
private startTime: number = 0;
|
|
private endTime: number = 0;
|
|
|
|
constructor() {
|
|
this.coverageData = {
|
|
totalTests: 0,
|
|
passedTests: 0,
|
|
failedTests: 0,
|
|
skippedTests: 0,
|
|
passRate: 0,
|
|
testSuites: [],
|
|
executionTime: 0,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
startCoverage(): void {
|
|
this.startTime = Date.now();
|
|
testLogger.info('开始收集测试覆盖率数据');
|
|
}
|
|
|
|
endCoverage(): void {
|
|
this.endTime = Date.now();
|
|
this.coverageData.executionTime = this.endTime - this.startTime;
|
|
|
|
this.calculateCoverage();
|
|
this.generateReport();
|
|
|
|
testLogger.info('测试覆盖率收集完成');
|
|
testLogger.info(`总测试数: ${this.coverageData.totalTests}`);
|
|
testLogger.info(`通过测试数: ${this.coverageData.passedTests}`);
|
|
testLogger.info(`失败测试数: ${this.coverageData.failedTests}`);
|
|
testLogger.info(`跳过测试数: ${this.coverageData.skippedTests}`);
|
|
testLogger.info(`通过率: ${this.coverageData.passRate.toFixed(2)}%`);
|
|
}
|
|
|
|
recordTestResult(suiteName: string, testName: string, status: 'passed' | 'failed' | 'skipped', duration: number, tags: string[], file: string): void {
|
|
const testCase: TestCaseCoverage = {
|
|
name: testName,
|
|
status,
|
|
duration,
|
|
tags,
|
|
file
|
|
};
|
|
|
|
if (!this.testResults.has(suiteName)) {
|
|
this.testResults.set(suiteName, []);
|
|
}
|
|
|
|
this.testResults.get(suiteName)!.push(testCase);
|
|
}
|
|
|
|
private calculateCoverage(): void {
|
|
let totalTests = 0;
|
|
let passedTests = 0;
|
|
let failedTests = 0;
|
|
let skippedTests = 0;
|
|
|
|
for (const [suiteName, testCases] of this.testResults.entries()) {
|
|
const suiteCoverage = this.calculateSuiteCoverage(suiteName, testCases);
|
|
this.suiteResults.set(suiteName, suiteCoverage);
|
|
|
|
totalTests += suiteCoverage.totalTests;
|
|
passedTests += suiteCoverage.passedTests;
|
|
failedTests += suiteCoverage.failedTests;
|
|
skippedTests += suiteCoverage.skippedTests;
|
|
}
|
|
|
|
this.coverageData.totalTests = totalTests;
|
|
this.coverageData.passedTests = passedTests;
|
|
this.coverageData.failedTests = failedTests;
|
|
this.coverageData.skippedTests = skippedTests;
|
|
this.coverageData.passRate = totalTests > 0 ? (passedTests / totalTests) * 100 : 0;
|
|
this.coverageData.testSuites = Array.from(this.suiteResults.values());
|
|
}
|
|
|
|
private calculateSuiteCoverage(suiteName: string, testCases: TestCaseCoverage[]): TestSuiteCoverage {
|
|
const totalTests = testCases.length;
|
|
const passedTests = testCases.filter(tc => tc.status === 'passed').length;
|
|
const failedTests = testCases.filter(tc => tc.status === 'failed').length;
|
|
const skippedTests = testCases.filter(tc => tc.status === 'skipped').length;
|
|
const passRate = totalTests > 0 ? (passedTests / totalTests) * 100 : 0;
|
|
|
|
return {
|
|
name: suiteName,
|
|
totalTests,
|
|
passedTests,
|
|
failedTests,
|
|
skippedTests,
|
|
passRate,
|
|
tests: testCases
|
|
};
|
|
}
|
|
|
|
private generateReport(): void {
|
|
const reportDir = path.join(process.cwd(), 'test-results', 'coverage');
|
|
|
|
if (!fs.existsSync(reportDir)) {
|
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
}
|
|
|
|
this.generateJSONReport(reportDir);
|
|
this.generateHTMLReport(reportDir);
|
|
this.generateMarkdownReport(reportDir);
|
|
this.generateConsoleReport();
|
|
}
|
|
|
|
private generateJSONReport(reportDir: string): void {
|
|
const jsonPath = path.join(reportDir, 'coverage.json');
|
|
fs.writeFileSync(jsonPath, JSON.stringify(this.coverageData, null, 2), 'utf-8');
|
|
testLogger.info(`JSON覆盖率报告已生成: ${jsonPath}`);
|
|
}
|
|
|
|
private generateHTMLReport(reportDir: string): void {
|
|
const htmlPath = path.join(reportDir, 'coverage.html');
|
|
const html = this.generateHTMLContent();
|
|
fs.writeFileSync(htmlPath, html, 'utf-8');
|
|
testLogger.info(`HTML覆盖率报告已生成: ${htmlPath}`);
|
|
}
|
|
|
|
private generateMarkdownReport(reportDir: string): void {
|
|
const mdPath = path.join(reportDir, 'coverage.md');
|
|
const markdown = this.generateMarkdownContent();
|
|
fs.writeFileSync(mdPath, markdown, 'utf-8');
|
|
testLogger.info(`Markdown覆盖率报告已生成: ${mdPath}`);
|
|
}
|
|
|
|
private generateHTMLContent(): string {
|
|
const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime, testSuites, timestamp } = this.coverageData;
|
|
|
|
const passRateColor = passRate >= 80 ? '#52c41a' : passRate >= 60 ? '#faad14' : '#f5222d';
|
|
const passRateClass = passRate >= 80 ? 'success' : passRate >= 60 ? 'warning' : 'danger';
|
|
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>测试覆盖率报告</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
|
background-color: #f5f5f5;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
}
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
text-align: center;
|
|
}
|
|
.header h1 {
|
|
margin: 0;
|
|
font-size: 32px;
|
|
font-weight: 600;
|
|
}
|
|
.header p {
|
|
margin: 10px 0 0 0;
|
|
font-size: 14px;
|
|
opacity: 0.9;
|
|
}
|
|
.summary {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
padding: 30px;
|
|
background-color: #fafafa;
|
|
}
|
|
.summary-card {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
text-align: center;
|
|
}
|
|
.summary-card .label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
margin-bottom: 8px;
|
|
}
|
|
.summary-card .value {
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
.summary-card .value.${passRateClass} {
|
|
color: ${passRateColor};
|
|
}
|
|
.suites {
|
|
padding: 30px;
|
|
}
|
|
.suites h2 {
|
|
margin: 0 0 20px 0;
|
|
font-size: 24px;
|
|
color: #333;
|
|
}
|
|
.suite {
|
|
margin-bottom: 30px;
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.suite-header {
|
|
background-color: #f8f9fa;
|
|
padding: 15px 20px;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.suite-header h3 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
color: #333;
|
|
}
|
|
.suite-stats {
|
|
display: flex;
|
|
gap: 20px;
|
|
font-size: 14px;
|
|
}
|
|
.suite-stats span {
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-weight: 500;
|
|
}
|
|
.suite-stats .passed {
|
|
background-color: #e6f7ff;
|
|
color: #4a5568;
|
|
}
|
|
.suite-stats .failed {
|
|
background-color: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
.suite-stats .skipped {
|
|
background-color: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
.suite-stats .rate {
|
|
background-color: ${passRateColor};
|
|
color: white;
|
|
}
|
|
.test-cases {
|
|
padding: 20px;
|
|
}
|
|
.test-case {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 15px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
.test-case:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.test-case .name {
|
|
flex: 1;
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
.test-case .status {
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
.test-case .status.passed {
|
|
background-color: #d4edda;
|
|
color: #0f5132;
|
|
}
|
|
.test-case .status.failed {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
.test-case .status.skipped {
|
|
background-color: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
.test-case .duration {
|
|
font-size: 12px;
|
|
color: #999;
|
|
min-width: 80px;
|
|
text-align: right;
|
|
}
|
|
.test-case .tags {
|
|
display: flex;
|
|
gap: 5px;
|
|
}
|
|
.test-case .tag {
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
background-color: #e9ecef;
|
|
color: #495057;
|
|
}
|
|
.footer {
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: #999;
|
|
font-size: 12px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🧪 测试覆盖率报告</h1>
|
|
<p>生成时间: ${timestamp}</p>
|
|
<p>执行时间: ${(executionTime / 1000).toFixed(2)}秒</p>
|
|
</div>
|
|
|
|
<div class="summary">
|
|
<div class="summary-card">
|
|
<div class="label">总测试数</div>
|
|
<div class="value">${totalTests}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="label">通过测试</div>
|
|
<div class="value" style="color: #52c41a">${passedTests}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="label">失败测试</div>
|
|
<div class="value" style="color: #f5222d">${failedTests}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="label">跳过测试</div>
|
|
<div class="value" style="color: #faad14">${skippedTests}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="label">通过率</div>
|
|
<div class="value ${passRateClass}">${passRate.toFixed(2)}%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="suites">
|
|
<h2>📊 测试套件详情</h2>
|
|
${testSuites.map(suite => `
|
|
<div class="suite">
|
|
<div class="suite-header">
|
|
<h3>${suite.name}</h3>
|
|
<div class="suite-stats">
|
|
<span class="passed">✓ ${suite.passedTests}</span>
|
|
<span class="failed">✗ ${suite.failedTests}</span>
|
|
<span class="skipped">⊘ ${suite.skippedTests}</span>
|
|
<span class="rate">${suite.passRate.toFixed(1)}%</span>
|
|
</div>
|
|
</div>
|
|
<div class="test-cases">
|
|
${suite.tests.map(testCase => `
|
|
<div class="test-case">
|
|
<div class="name">${testCase.name}</div>
|
|
<div class="status ${testCase.status}">${testCase.status}</div>
|
|
<div class="duration">${testCase.duration}ms</div>
|
|
<div class="tags">
|
|
${testCase.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p>Generated by TestCoverageReporter</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
private generateMarkdownContent(): string {
|
|
const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime, testSuites, timestamp } = this.coverageData;
|
|
|
|
return `# 测试覆盖率报告
|
|
|
|
## 概要
|
|
|
|
- **生成时间**: ${timestamp}
|
|
- **执行时间**: ${(executionTime / 1000).toFixed(2)}秒
|
|
- **总测试数**: ${totalTests}
|
|
- **通过测试**: ${passedTests}
|
|
- **失败测试**: ${failedTests}
|
|
- **跳过测试**: ${skippedTests}
|
|
- **通过率**: ${passRate.toFixed(2)}%
|
|
|
|
## 测试套件详情
|
|
|
|
${testSuites.map(suite => `
|
|
### ${suite.name}
|
|
|
|
- **总测试数**: ${suite.totalTests}
|
|
- **通过测试**: ${suite.passedTests}
|
|
- **失败测试**: ${suite.failedTests}
|
|
- **跳过测试**: ${suite.skippedTests}
|
|
- **通过率**: ${suite.passRate.toFixed(2)}%
|
|
|
|
#### 测试用例
|
|
|
|
| 测试用例 | 状态 | 耗时 | 标签 |
|
|
|---------|------|------|------|
|
|
${suite.tests.map(testCase => `
|
|
| ${testCase.name} | ${testCase.status} | ${testCase.duration}ms | ${testCase.tags.join(', ')} |
|
|
`).join('')}
|
|
`).join('')}
|
|
|
|
## 总结
|
|
|
|
${passRate >= 80 ? '✅ 测试覆盖率优秀' : passRate >= 60 ? '⚠️ 测试覆盖率良好' : '❌ 测试覆盖率需要改进'}
|
|
|
|
---
|
|
*Generated by TestCoverageReporter*
|
|
`;
|
|
}
|
|
|
|
private generateConsoleReport(): void {
|
|
const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime } = this.coverageData;
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log('📊 测试覆盖率报告');
|
|
console.log('='.repeat(60));
|
|
console.log(`生成时间: ${this.coverageData.timestamp}`);
|
|
console.log(`执行时间: ${(executionTime / 1000).toFixed(2)}秒`);
|
|
console.log('');
|
|
console.log('📈 总体统计:');
|
|
console.log(` 总测试数: ${totalTests}`);
|
|
console.log(` 通过测试: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`);
|
|
console.log(` 失败测试: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`);
|
|
console.log(` 跳过测试: ${skippedTests} (${(skippedTests / totalTests * 100).toFixed(2)}%)`);
|
|
console.log(` 通过率: ${passRate.toFixed(2)}%`);
|
|
console.log('');
|
|
console.log('📋 测试套件详情:');
|
|
|
|
for (const suite of this.coverageData.testSuites) {
|
|
console.log(`\n ${suite.name}:`);
|
|
console.log(` 总测试数: ${suite.totalTests}`);
|
|
console.log(` 通过测试: ${suite.passedTests} (${suite.passRate.toFixed(2)}%)`);
|
|
console.log(` 失败测试: ${suite.failedTests}`);
|
|
console.log(` 跳过测试: ${suite.skippedTests}`);
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
|
|
if (passRate >= 80) {
|
|
console.log('✅ 测试覆盖率优秀');
|
|
} else if (passRate >= 60) {
|
|
console.log('⚠️ 测试覆盖率良好');
|
|
} else {
|
|
console.log('❌ 测试覆盖率需要改进');
|
|
}
|
|
|
|
console.log('='.repeat(60) + '\n');
|
|
}
|
|
|
|
getCoverage(): TestCoverage {
|
|
return this.coverageData;
|
|
}
|
|
|
|
getSuiteCoverage(suiteName: string): TestSuiteCoverage | undefined {
|
|
return this.suiteResults.get(suiteName);
|
|
}
|
|
|
|
exportCoverage(format: 'json' | 'html' | 'markdown' = 'json'): string {
|
|
switch (format) {
|
|
case 'json':
|
|
return JSON.stringify(this.coverageData, null, 2);
|
|
case 'html':
|
|
return this.generateHTMLContent();
|
|
case 'markdown':
|
|
return this.generateMarkdownContent();
|
|
default:
|
|
return JSON.stringify(this.coverageData, null, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const testCoverageReporter = new TestCoverageReporter();
|