更新报告样式
This commit is contained in:
parent
e3ddb271f6
commit
976ac8b5ad
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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);
|
||||
Loading…
Reference in New Issue