Files
everything-is-suitable/everything-is-suitable-test/e2e/uniapp/test-reporter.ts
T
张翔 08ea5fbe98 feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
2026-03-28 14:37:29 +08:00

455 lines
12 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
interface TestResult {
testName: string;
status: 'passed' | 'failed' | 'skipped';
duration: number;
error?: string;
screenshot?: string;
video?: string;
}
interface TestSuite {
suiteName: string;
tests: TestResult[];
totalTests: number;
passedTests: number;
failedTests: number;
skippedTests: number;
totalDuration: number;
}
interface TestReport {
timestamp: string;
testSuites: TestSuite[];
summary: {
totalSuites: number;
totalTests: number;
totalPassed: number;
totalFailed: number;
totalSkipped: number;
totalDuration: number;
passRate: number;
};
}
export class UniappTestReporter {
private results: TestSuite[] = [];
private startTime: number = Date.now();
addTestSuite(suiteName: string, tests: TestResult[]): void {
const passedTests = tests.filter(t => t.status === 'passed').length;
const failedTests = tests.filter(t => t.status === 'failed').length;
const skippedTests = tests.filter(t => t.status === 'skipped').length;
const totalDuration = tests.reduce((sum, t) => sum + t.duration, 0);
this.results.push({
suiteName,
tests,
totalTests: tests.length,
passedTests,
failedTests,
skippedTests,
totalDuration,
});
}
generateReport(): TestReport {
const totalSuites = this.results.length;
const totalTests = this.results.reduce((sum, s) => sum + s.totalTests, 0);
const totalPassed = this.results.reduce((sum, s) => sum + s.passedTests, 0);
const totalFailed = this.results.reduce((sum, s) => sum + s.failedTests, 0);
const totalSkipped = this.results.reduce((sum, s) => sum + s.skippedTests, 0);
const totalDuration = this.results.reduce((sum, s) => sum + s.totalDuration, 0);
const passRate = totalTests > 0 ? (totalPassed / totalTests) * 100 : 0;
return {
timestamp: new Date().toISOString(),
testSuites: this.results,
summary: {
totalSuites,
totalTests,
totalPassed,
totalFailed,
totalSkipped,
totalDuration,
passRate,
},
};
}
async generateJSONReport(outputPath: string): Promise<void> {
const report = this.generateReport();
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8');
console.log(`JSON report generated: ${outputPath}`);
}
async generateHTMLReport(outputPath: string): Promise<void> {
const report = this.generateReport();
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const html = this.generateHTMLContent(report);
fs.writeFileSync(outputPath, html, 'utf-8');
console.log(`HTML report generated: ${outputPath}`);
}
private generateHTMLContent(report: TestReport): string {
const { summary, testSuites } = report;
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Uniapp E2E测试报告</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header .timestamp {
font-size: 14px;
opacity: 0.9;
}
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
}
.summary-card .label {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.summary-card .value {
font-size: 32px;
font-weight: bold;
color: #333;
}
.summary-card.passed .value {
color: #52c41a;
}
.summary-card.failed .value {
color: #f5222d;
}
.summary-card.rate .value {
color: #1890ff;
}
.test-suite {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
overflow: hidden;
}
.test-suite-header {
background: #f0f0f0;
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
}
.test-suite-header h2 {
font-size: 18px;
color: #333;
}
.test-suite-summary {
display: flex;
gap: 20px;
margin-top: 10px;
font-size: 14px;
color: #666;
}
.test-suite-summary span {
display: flex;
align-items: center;
gap: 5px;
}
.test-suite-summary .passed {
color: #52c41a;
}
.test-suite-summary .failed {
color: #f5222d;
}
.test-suite-summary .skipped {
color: #faad14;
}
.test-list {
padding: 0;
list-style: none;
}
.test-item {
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
gap: 15px;
}
.test-item:last-child {
border-bottom: none;
}
.test-status {
width: 80px;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
text-align: center;
}
.test-status.passed {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.test-status.failed {
background-color: #fff1f0;
color: #f5222d;
border: 1px solid #ffa39e;
}
.test-status.skipped {
background-color: #fffbe6;
color: #faad14;
border: 1px solid #ffe58f;
}
.test-name {
flex: 1;
font-size: 14px;
color: #333;
}
.test-duration {
font-size: 12px;
color: #999;
min-width: 80px;
text-align: right;
}
.test-error {
margin-top: 10px;
padding: 10px;
background-color: #fff1f0;
border: 1px solid #ffa39e;
border-radius: 4px;
font-size: 12px;
color: #f5222d;
}
.test-links {
display: flex;
gap: 10px;
margin-top: 10px;
}
.test-link {
font-size: 12px;
color: #1890ff;
text-decoration: none;
}
.test-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Uniapp E2E测试报告</h1>
<div class="timestamp">生成时间: ${report.timestamp}</div>
</div>
<div class="summary">
<div class="summary-card">
<div class="label">总测试数</div>
<div class="value">${summary.totalTests}</div>
</div>
<div class="summary-card passed">
<div class="label">通过</div>
<div class="value">${summary.totalPassed}</div>
</div>
<div class="summary-card failed">
<div class="label">失败</div>
<div class="value">${summary.totalFailed}</div>
</div>
<div class="summary-card">
<div class="label">跳过</div>
<div class="value">${summary.totalSkipped}</div>
</div>
<div class="summary-card rate">
<div class="label">通过率</div>
<div class="value">${summary.passRate.toFixed(1)}%</div>
</div>
<div class="summary-card">
<div class="label">总耗时</div>
<div class="value">${(summary.totalDuration / 1000).toFixed(2)}s</div>
</div>
</div>
${testSuites.map(suite => `
<div class="test-suite">
<div class="test-suite-header">
<h2>${suite.suiteName}</h2>
<div class="test-suite-summary">
<span class="passed">✓ 通过: ${suite.passedTests}</span>
<span class="failed">✗ 失败: ${suite.failedTests}</span>
<span class="skipped">○ 跳过: ${suite.skippedTests}</span>
<span>⏱ 耗时: ${(suite.totalDuration / 1000).toFixed(2)}s</span>
</div>
</div>
<ul class="test-list">
${suite.tests.map(test => `
<li class="test-item">
<div class="test-status ${test.status}">${test.status.toUpperCase()}</div>
<div class="test-name">${test.testName}</div>
<div class="test-duration">${(test.duration / 1000).toFixed(2)}s</div>
${test.error ? `<div class="test-error">${test.error}</div>` : ''}
${test.screenshot || test.video ? `
<div class="test-links">
${test.screenshot ? `<a href="${test.screenshot}" class="test-link" target="_blank">查看截图</a>` : ''}
${test.video ? `<a href="${test.video}" class="test-link" target="_blank">查看视频</a>` : ''}
</div>
` : ''}
</li>
`).join('')}
</ul>
</div>
`).join('')}
</div>
</body>
</html>
`;
}
async generateMarkdownReport(outputPath: string): Promise<void> {
const report = this.generateReport();
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const markdown = this.generateMarkdownContent(report);
fs.writeFileSync(outputPath, markdown, 'utf-8');
console.log(`Markdown report generated: ${outputPath}`);
}
private generateMarkdownContent(report: TestReport): string {
const { summary, testSuites } = report;
let markdown = `# Uniapp E2E测试报告\n\n`;
markdown += `**生成时间**: ${report.timestamp}\n\n`;
markdown += `## 测试摘要\n\n`;
markdown += `| 指标 | 数值 |\n`;
markdown += `|------|------|\n`;
markdown += `| 总测试数 | ${summary.totalTests} |\n`;
markdown += `| 通过 | ${summary.totalPassed} |\n`;
markdown += `| 失败 | ${summary.totalFailed} |\n`;
markdown += `| 跳过 | ${summary.totalSkipped} |\n`;
markdown += `| 通过率 | ${summary.passRate.toFixed(1)}% |\n`;
markdown += `| 总耗时 | ${(summary.totalDuration / 1000).toFixed(2)}s |\n\n`;
for (const suite of testSuites) {
markdown += `## ${suite.suiteName}\n\n`;
markdown += `**测试数**: ${suite.totalTests} | **通过**: ${suite.passedTests} | **失败**: ${suite.failedTests} | **跳过**: ${suite.skippedTests} | **耗时**: ${(suite.totalDuration / 1000).toFixed(2)}s\n\n`;
for (const test of suite.tests) {
markdown += `### ${test.testName}\n\n`;
markdown += `- **状态**: ${test.status.toUpperCase()}\n`;
markdown += `- **耗时**: ${(test.duration / 1000).toFixed(2)}s\n`;
if (test.error) {
markdown += `- **错误**: ${test.error}\n`;
}
if (test.screenshot) {
markdown += `- **截图**: [查看](${test.screenshot})\n`;
}
if (test.video) {
markdown += `- **视频**: [查看](${test.video})\n`;
}
markdown += `\n`;
}
}
return markdown;
}
reset(): void {
this.results = [];
this.startTime = Date.now();
}
}