feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,529 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user