更新报告样式

This commit is contained in:
xiahb 2025-11-06 15:08:36 +08:00
parent e3ddb271f6
commit 976ac8b5ad
1 changed files with 335 additions and 185 deletions

View File

@ -2,10 +2,10 @@
import fs from 'fs'
import path from 'path'
// 确保test-results目录存在
const testResultsDir = path.join(process.cwd(), 'test-results')
if (!fs.existsSync(testResultsDir)) {
fs.mkdirSync(testResultsDir, { recursive: true })
// 确保html目录存在
const htmlDir = path.join(process.cwd(), 'test-results')
if (!fs.existsSync(htmlDir)) {
fs.mkdirSync(htmlDir, { recursive: true })
}
// 读取JSON测试报告
@ -36,8 +36,24 @@ try {
// 生成HTML报告
const generateHtmlReport = (reportData) => {
let html = `
<!DOCTYPE html>
// 获取文件名(不包含路径)
const getFileName = (filePath) => {
// 先按反斜杠分割,再按正斜杠分割,确保在不同系统下都能正确获取文件名
const parts = filePath.split('\\');
const lastPart = parts[parts.length - 1];
const subParts = lastPart.split('/');
return subParts[subParts.length - 1];
};
// 转换为北京时间
const formatBeijingTime = (timestamp) => {
const date = new Date(timestamp);
// 使用东八区时间UTC+8
const beijingTime = new Date(date.getTime() + 8 * 60 * 60 * 1000);
return beijingTime.toISOString().replace('T', ' ').substring(0, 19);
};
let html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
@ -47,82 +63,84 @@ const generateHtmlReport = (reportData) => {
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 100%;
height: 100vh;
margin: 0;
background-color: white;
display: flex;
flex-direction: column;
height: 100vh;
}
.sidebar {
width: 300px;
background-color: #fff;
border-right: 1px solid #ddd;
overflow-y: auto;
padding: 20px 0;
}
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
h1 {
color: #333;
text-align: center;
margin: 0;
padding: 15px;
margin-top: 0;
padding: 20px 0;
background-color: #fff;
border-bottom: 1px solid #ddd;
font-size: 1.5em;
}
.summary {
background-color: #e9f7ef;
padding: 10px 15px;
margin: 10px 20px;
border-radius: 4px;
font-size: 0.9em;
padding: 15px 20px;
border-radius: 5px;
margin: 10px 20px 30px 20px;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 5px;
gap: 10px;
}
.summary-item {
margin: 2px 0;
margin: 5px 0;
}
.content {
display: flex;
flex: 1;
overflow: hidden;
.file-list-item {
padding: 10px 20px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.file-list {
width: 300px;
border-right: 1px solid #ddd;
overflow-y: auto;
.file-list-item:hover {
background-color: #f8f9fa;
}
.test-details {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.file-item {
padding: 12px 20px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #e9ecef;
}
.file-item.active {
.file-list-item.active {
background-color: #007bff;
color: white;
}
.file-header {
font-weight: bold;
padding: 10px 20px;
background-color: #e9ecef;
border-bottom: 1px solid #ddd;
.test-file {
margin-bottom: 30px;
border: 1px solid #ddd;
border-radius: 5px;
overflow: hidden;
}
.test-case {
padding: 15px 20px;
border-bottom: 1px solid #eee;
.file-header {
background-color: #f8f9fa;
padding: 15px;
font-weight: bold;
border-bottom: 1px solid #ddd;
}
.test-case:last-child {
border-bottom: none;
.test-suite {
margin: 10px 0;
}
.test-case {
padding: 10px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
}
.test-case:last-child {
border-bottom: none;
}
.passed {
background-color: #d4edda;
@ -142,7 +160,6 @@ const generateHtmlReport = (reportData) => {
border-radius: 3px;
color: white;
font-weight: bold;
margin-right: 10px;
}
.status.passed {
background-color: #28a745;
@ -150,159 +167,292 @@ const generateHtmlReport = (reportData) => {
.status.failed {
background-color: #dc3545;
}
.hidden {
display: none;
.chart-container {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
margin: 20px 0;
}
.coverage-link {
padding: 12px 20px;
border-top: 2px solid #007bff;
background-color: #e9f7ef;
.chart {
width: 200px;
height: 200px;
border-radius: 50%;
position: relative;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
background-color: #dc3545;
}
.chart-fill {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #28a745;
clip-path: polygon(50% 50%, 50% 0, 100% 0, 100% 100%, 0% 100%, 0% 0%, 50% 0%);
}
.chart-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
border-radius: 50%;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 14px;
font-weight: bold;
}
.coverage-link a {
.chart-title {
font-size: 16px;
margin-bottom: 5px;
}
.chart-value {
font-size: 20px;
}
.legend {
display: flex;
justify-content: center;
margin-top: 20px;
}
.legend-item {
display: flex;
align-items: center;
margin: 0 15px;
}
.legend-color {
width: 20px;
height: 20px;
margin-right: 5px;
}
.legend-passed {
background-color: #28a745;
}
.legend-failed {
background-color: #dc3545;
}
.coverage-link {
display: block;
padding: 10px 20px;
color: #007bff;
text-decoration: none;
border-bottom: 1px solid #eee;
}
.coverage-link a:hover {
text-decoration: underline;
.coverage-link:hover {
background-color: #f8f9fa;
}
.failure-details {
margin-top: 10px;
}
.failure-message {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 10px;
margin: 5px 0;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
color: #721c24;
}
</style>
</head>
<body>
<div class="container">
<h1>测试用例详细报告</h1>
<div class="summary">
<div class="summary-grid">
<div class="summary-item">测试套件: ${reportData.numTotalTestSuites} 总计, ${reportData.numPassedTestSuites} 通过, ${reportData.numFailedTestSuites} 失败</div>
<div class="summary-item">测试用例: ${reportData.numTotalTests} 总计, ${reportData.numPassedTests} 通过, ${reportData.numFailedTests} 失败</div>
<div class="summary-item">执行时间: ${new Date(reportData.startTime).toLocaleString()}</div>
</div>
<h1>检测流程信息化系统自动化测试报告</h1>
<div class="summary">
<div class="summary-grid">
<div class="summary-item">测试套件总数: ${reportData.numTotalTestSuites}</div>
<div class="summary-item">通过的测试套件: ${reportData.numPassedTestSuites}</div>
<div class="summary-item">失败的测试套件: ${reportData.numFailedTestSuites}</div>
<div class="summary-item">测试用例总数: ${reportData.numTotalTests}</div>
<div class="summary-item">通过的测试用例: ${reportData.numPassedTests}</div>
<div class="summary-item">失败的测试用例: ${reportData.numFailedTests}</div>
<div class="summary-item">执行时间: ${formatBeijingTime(reportData.startTime)}</div>
</div>
<div class="content">
<!-- 左侧文件列表 -->
<div class="file-list">
<div class="file-header">测试文件列表</div>
`
// 生成文件列表
if (reportData.testResults && reportData.testResults.length > 0) {
reportData.testResults.forEach((testFile, index) => {
const fileName = path.basename(testFile.name);
html += `
<div class="file-item ${index === 0 ? 'active' : ''}" data-file-index="${index}">
${fileName}
</div>
`
});
} else {
html += `
<div class="file-item">
暂无测试文件
</div>
`
}
// 添加覆盖率报告链接
html += `
<div class="coverage-link">
<a href="./coverage/index.html" target="_blank">查看覆盖率报告</a>
</div>
<div class="container">
<div class="sidebar">
<div id="file-list">
<!-- 测试文件列表将在这里生成 -->
</div>
<a href="./coverage/lcov-report/index.html" class="coverage-link" target="_blank">查看覆盖率报告</a>
</div>
<div class="main-content" id="test-details">
<div class="chart-container">
<div>
<div class="chart">
<div class="chart-fill" id="chart-fill"></div>
<div class="chart-center">
<div class="chart-value" id="chart-value">0%</div>
<div id="chart-ratio">0/0</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color legend-passed"></div>
<span>通过: <span id="passed-count">0</span></span>
</div>
<div class="legend-item">
<div class="legend-color legend-failed"></div>
<span>失败: <span id="failed-count">0</span></span>
</div>
</div>
</div>
</div>
<!-- 右侧测试详情 -->
<div class="test-details">
`
// 生成每个文件的测试详情
if (reportData.testResults && reportData.testResults.length > 0) {
reportData.testResults.forEach((testFile, index) => {
const fileName = path.basename(testFile.name);
html += `
<div id="file-${index}" class="file-details ${index !== 0 ? 'hidden' : ''}">
<h2>${fileName}</h2>
`
// 为每个测试用例生成详细信息
if (testFile.assertionResults && testFile.assertionResults.length > 0) {
testFile.assertionResults.forEach(testCase => {
const statusClass = testCase.status === 'passed' ? 'passed' : 'failed'
html += `
<div class="test-case ${statusClass}">
<div class="test-info">
<p>
<span class="status ${testCase.status}">${testCase.status === 'passed' ? '通过' : '失败'}</span>
<strong>${testCase.title}</strong>
</p>
<p>完整名称: ${testCase.fullName}</p>
${testCase.ancestorTitles && testCase.ancestorTitles.length > 0 ? `<p>测试套件: ${testCase.ancestorTitles.join(' > ')}</p>` : ''}
<p class="test-duration">执行时间: ${testCase.duration || 0}ms</p>
</div>
</div>
`
})
} else {
html += `
<div class="test-case">
<div class="test-info">
<p>该文件中没有测试用例</p>
</div>
</div>
`
}
html += `
</div>
`
});
} else {
html += `
<div class="file-details">
<h2>暂无测试结果</h2>
<p>尚未运行任何测试或测试结果为空</p>
</div>
`
}
html += `
<div style="text-align: center; color: #666; margin-top: 30px;">
<p>请从左侧选择一个测试文件查看详细信息</p>
</div>
</div>
</div>
<script>
// 添加文件列表点击事件
document.querySelectorAll('.file-item').forEach(item => {
item.addEventListener('click', function() {
// 移除所有active类
document.querySelectorAll('.file-item').forEach(el => {
el.classList.remove('active');
const testData = ${JSON.stringify(reportData)};
// 获取文件名(不包含路径)
function getFileName(filePath) {
// 先按反斜杠分割,再按正斜杠分割,确保在不同系统下都能正确获取文件名
const parts = filePath.split('\\\\');
const lastPart = parts[parts.length - 1];
const subParts = lastPart.split('/');
return subParts[subParts.length - 1];
}
// 生成测试文件列表
function generateFileList() {
const fileListContainer = document.getElementById('file-list');
if (testData.testResults && testData.testResults.length > 0) {
testData.testResults.forEach((testFile, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-list-item';
fileItem.textContent = getFileName(testFile.name);
fileItem.onclick = () => showTestDetails(index);
// 检查是否有失败的测试用例,如果有则将文件名标记为红色
const hasFailedTests = testFile.assertionResults &&
testFile.assertionResults.some(testCase => testCase.status === 'failed');
if (hasFailedTests) {
fileItem.style.color = '#dc3545'; // 红色
fileItem.style.fontWeight = 'bold';
}
fileListContainer.appendChild(fileItem);
});
// 为当前点击的元素添加active类
this.classList.add('active');
// 隐藏所有文件详情
document.querySelectorAll('.file-details').forEach(el => {
el.classList.add('hidden');
});
// 显示对应的文件详情
const fileIndex = this.getAttribute('data-file-index');
document.getElementById('file-' + fileIndex).classList.remove('hidden');
} else {
const emptyItem = document.createElement('div');
emptyItem.className = 'file-list-item';
emptyItem.textContent = '暂无测试结果';
fileListContainer.appendChild(emptyItem);
}
}
// 显示测试详情
function showTestDetails(fileIndex) {
// 更新活动状态
document.querySelectorAll('.file-list-item').forEach((item, index) => {
if (index === fileIndex) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// 显示测试详情
const testFile = testData.testResults[fileIndex];
const detailsContainer = document.getElementById('test-details');
let html = '';
if (testFile) {
html += '<div class="test-file"><div class="file-header">文件: ' + (testFile.name || '') + '</div>';
// 为每个测试用例生成详细信息
if (testFile.assertionResults && testFile.assertionResults.length > 0) {
testFile.assertionResults.forEach(testCase => {
const statusClass = testCase.status === 'passed' ? 'passed' : 'failed';
html += '<div class="test-case ' + statusClass + '">';
html += '<div class="test-info">';
html += '<span class="status ' + (testCase.status || '') + '">' + (testCase.status === 'passed' ? '通过' : '失败') + '</span>';
html += '<strong>' + (testCase.title || '') + '</strong>';
html += '<div>完整名称: ' + (testCase.fullName || '') + '</div>';
if (testCase.ancestorTitles && testCase.ancestorTitles.length > 0) {
html += '<div>测试套件: ' + testCase.ancestorTitles.join(' > ') + '</div>';
}
// 显示失败用例的错误详情
if (testCase.status === 'failed' && testCase.failureMessages && testCase.failureMessages.length > 0) {
html += '<div class="failure-details">';
html += '<div style="color: #dc3545; font-weight: bold; margin: 10px 0 5px 0;">错误详情:</div>';
testCase.failureMessages.forEach(message => {
// 转义HTML特殊字符并保留换行
const escapedMessage = message
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(new RegExp('\\n', 'g'), '<br>');
html += '<div class="failure-message">' + escapedMessage + '</div>';
});
html += '</div>';
}
html += '</div>';
html += '<div class="test-duration">执行时间: ' + (testCase.duration || 0) + 'ms</div>';
html += '</div>';
});
}
html += '</div>';
}
detailsContainer.innerHTML = html;
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
generateFileList();
// 设置图表数据
const passedTests = testData.numPassedTests || 0;
const totalTests = testData.numTotalTests || 0;
const failedTests = testData.numFailedTests || 0;
// 更新图表
const percentage = totalTests > 0 ? ((passedTests / totalTests) * 100).toFixed(1) : 0;
// 使用更简单的背景渐变方式实现饼图
const chartFill = document.getElementById('chart-fill');
const angle = totalTests > 0 ? (passedTests / totalTests) * 360 : 0;
if (angle === 360) {
// 全部通过
chartFill.style.background = '#28a745';
} else if (angle === 0) {
// 全部失败
chartFill.style.background = '#dc3545';
} else {
// 混合情况,使用锥形渐变
const gradientAngle = 90 - angle;
chartFill.style.background = 'conic-gradient(#28a745 0deg, #28a745 ' + angle + 'deg, #dc3545 ' + angle + 'deg, #dc3545 360deg)';
}
document.getElementById('chart-value').textContent = percentage + '%';
document.getElementById('chart-ratio').textContent = passedTests + '/' + totalTests;
document.getElementById('passed-count').textContent = passedTests;
document.getElementById('failed-count').textContent = failedTests;
});
</script>
</body>
</html>
`
</html>`;
return html
}
return html;
};
// 生成并保存HTML报告
const htmlReport = generateHtmlReport(jsonReport)
const outputPath = path.join(process.cwd(), 'test-results', 'detailed-report.html')
fs.writeFileSync(outputPath, htmlReport)
const htmlReport = generateHtmlReport(jsonReport);
const outputPath = path.join(process.cwd(), 'test-results', 'detailed-report.html');
fs.writeFileSync(outputPath, htmlReport);
console.log(`详细测试报告已生成: ${outputPath}`)
console.log('详细测试报告已生成: ' + outputPath);