08ea5fbe98
添加用户管理视图、API和状态管理文件
455 lines
12 KiB
TypeScript
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();
|
|
}
|
|
}
|