testgenius-ai
Version:
🚀 TestGenius AI - The Ultimate E2E Testing Framework for Everyone | No Coding Required • AI-Powered Automation • Beautiful Reports • Zero Complexity
851 lines (770 loc) • 24.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HTMLReportGenerator = void 0;
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
const chalk_1 = __importDefault(require("chalk"));
class HTMLReportGenerator {
constructor(options = {}) {
this.options = {
outputDir: 'reports/html',
title: 'TestGenius AI Test Report',
includeScreenshots: true,
includeConsoleLogs: true,
interactive: true,
theme: 'auto',
...options
};
this.outputDir = this.options.outputDir;
}
/**
* Generate interactive HTML report
*/
async generateReport(results) {
await fs_extra_1.default.ensureDir(this.outputDir);
const reportData = this.prepareReportData(results);
const htmlContent = this.generateHTML(reportData);
const cssContent = this.generateCSS();
const jsContent = this.generateJavaScript();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const reportFileName = `test-report-${timestamp}.html`;
const reportPath = path_1.default.join(this.outputDir, reportFileName);
// Create complete HTML with embedded CSS and JS
const fullHTML = this.createFullHTML(htmlContent, cssContent, jsContent);
await fs_extra_1.default.writeFile(reportPath, fullHTML);
console.log(chalk_1.default.green(`✅ HTML report generated: ${reportPath}`));
return reportPath;
}
/**
* Generate summary report
*/
async generateSummaryReport(results) {
await fs_extra_1.default.ensureDir(this.outputDir);
const reportData = this.prepareReportData(results);
const htmlContent = this.generateSummaryHTML(reportData);
const cssContent = this.generateSummaryCSS();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const reportFileName = `summary-report-${timestamp}.html`;
const reportPath = path_1.default.join(this.outputDir, reportFileName);
const fullHTML = this.createFullHTML(htmlContent, cssContent, '');
await fs_extra_1.default.writeFile(reportPath, fullHTML);
console.log(chalk_1.default.green(`✅ Summary report generated: ${reportPath}`));
return reportPath;
}
/**
* Open report in browser
*/
async openReport(reportPath) {
const open = require('open');
if (!reportPath) {
// Find the latest report
const files = await fs_extra_1.default.readdir(this.outputDir);
const htmlFiles = files.filter(f => f.endsWith('.html')).sort().reverse();
if (htmlFiles.length === 0) {
throw new Error('No HTML reports found. Generate a report first.');
}
reportPath = path_1.default.join(this.outputDir, htmlFiles[0]);
}
await open(reportPath);
console.log(chalk_1.default.blue(`🌐 Opened report: ${reportPath}`));
}
/**
* Clean old reports
*/
async cleanupReports(daysToKeep = 30) {
const files = await fs_extra_1.default.readdir(this.outputDir);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
let deletedCount = 0;
for (const file of files) {
if (file.endsWith('.html')) {
const filePath = path_1.default.join(this.outputDir, file);
const stats = await fs_extra_1.default.stat(filePath);
if (stats.mtime < cutoffDate) {
await fs_extra_1.default.remove(filePath);
deletedCount++;
}
}
}
console.log(chalk_1.default.green(`✅ Cleaned up ${deletedCount} old reports`));
}
/**
* Prepare report data
*/
prepareReportData(results) {
let testResults;
let summary;
if (Array.isArray(results)) {
testResults = results;
summary = this.calculateSummary(testResults);
}
else {
testResults = results.results;
summary = {
total: results.totalTests,
passed: results.passed,
failed: results.failed,
duration: results.totalDuration,
successRate: results.successRate
};
}
return {
summary,
tests: testResults,
environment: 'Test Environment',
timestamp: new Date(),
version: '1.2.2'
};
}
/**
* Calculate summary statistics
*/
calculateSummary(results) {
const total = results.length;
const passed = results.filter(r => r.success).length;
const failed = total - passed;
const duration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
const successRate = total > 0 ? (passed / total) * 100 : 0;
return {
total,
passed,
failed,
duration,
successRate
};
}
/**
* Generate main HTML content
*/
generateHTML(data) {
return `
<div class="report-container">
<header class="report-header">
<h1>${this.options.title}</h1>
<div class="report-meta">
<span class="timestamp">Generated: ${data.timestamp.toLocaleString()}</span>
<span class="version">Version: ${data.version}</span>
<span class="environment">Environment: ${data.environment}</span>
</div>
</header>
<div class="summary-section">
<div class="summary-grid">
<div class="summary-card total">
<div class="summary-number">${data.summary.total}</div>
<div class="summary-label">Total Tests</div>
</div>
<div class="summary-card passed">
<div class="summary-number">${data.summary.passed}</div>
<div class="summary-label">Passed</div>
</div>
<div class="summary-card failed">
<div class="summary-number">${data.summary.failed}</div>
<div class="summary-label">Failed</div>
</div>
<div class="summary-card success-rate">
<div class="summary-number">${data.summary.successRate.toFixed(1)}%</div>
<div class="summary-label">Success Rate</div>
</div>
<div class="summary-card duration">
<div class="summary-number">${this.formatDuration(data.summary.duration)}</div>
<div class="summary-label">Total Duration</div>
</div>
</div>
</div>
<div class="controls-section">
<div class="search-box">
<input type="text" id="testSearch" placeholder="Search tests..." />
</div>
<div class="filter-controls">
<select id="statusFilter">
<option value="">All Status</option>
<option value="passed">Passed</option>
<option value="failed">Failed</option>
</select>
<select id="priorityFilter">
<option value="">All Priorities</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
</div>
</div>
<div class="tests-section">
<h2>Test Results</h2>
<div class="tests-table-container">
<table class="tests-table">
<thead>
<tr>
<th>Test Name</th>
<th>Status</th>
<th>Priority</th>
<th>Duration</th>
<th>Tags</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${data.tests.map(test => this.generateTestRow(test)).join('')}
</tbody>
</table>
</div>
</div>
<div class="details-section" id="testDetails" style="display: none;">
<h3>Test Details</h3>
<div id="testDetailsContent"></div>
</div>
</div>
`;
}
/**
* Generate summary HTML content
*/
generateSummaryHTML(data) {
return `
<div class="summary-report">
<header class="report-header">
<h1>${this.options.title} - Summary</h1>
<div class="report-meta">
<span class="timestamp">Generated: ${data.timestamp.toLocaleString()}</span>
<span class="version">Version: ${data.version}</span>
</div>
</header>
<div class="summary-section">
<div class="summary-grid">
<div class="summary-card total">
<div class="summary-number">${data.summary.total}</div>
<div class="summary-label">Total Tests</div>
</div>
<div class="summary-card passed">
<div class="summary-number">${data.summary.passed}</div>
<div class="summary-label">Passed</div>
</div>
<div class="summary-card failed">
<div class="summary-number">${data.summary.failed}</div>
<div class="summary-label">Failed</div>
</div>
<div class="summary-card success-rate">
<div class="summary-number">${data.summary.successRate.toFixed(1)}%</div>
<div class="summary-label">Success Rate</div>
</div>
<div class="summary-card duration">
<div class="summary-number">${this.formatDuration(data.summary.duration)}</div>
<div class="summary-label">Total Duration</div>
</div>
</div>
</div>
<div class="quick-stats">
<h3>Quick Statistics</h3>
<ul>
<li>Average test duration: ${this.formatDuration(data.summary.duration / data.summary.total)}</li>
<li>Failed test percentage: ${((data.summary.failed / data.summary.total) * 100).toFixed(1)}%</li>
<li>Tests per minute: ${this.calculateTestsPerMinute(data.summary.duration, data.summary.total)}</li>
</ul>
</div>
</div>
`;
}
/**
* Generate test row HTML
*/
generateTestRow(test) {
const statusClass = test.success ? 'status-passed' : 'status-failed';
const statusText = test.success ? 'Passed' : 'Failed';
const duration = this.formatDuration(test.duration || 0);
// Handle missing properties gracefully
const testName = test.name || test.testId || test.id;
const testTags = test.tags ? test.tags.join(', ') : '';
const testPriority = test.priority || 'Medium';
return `
<tr class="test-row" data-test-id="${test.id}" data-status="${test.success ? 'passed' : 'failed'}">
<td class="test-name">${testName}</td>
<td class="test-status">
<span class="status-badge ${statusClass}">${statusText}</span>
</td>
<td class="test-priority">${testPriority}</td>
<td class="test-duration">${duration}</td>
<td class="test-tags">${testTags}</td>
<td class="test-actions">
<button class="btn-details" onclick="showTestDetails('${test.id}')">Details</button>
${test.screenshots && test.screenshots.length > 0 ?
`<button class="btn-screenshots" onclick="showScreenshots('${test.id}')">Screenshots</button>` :
''}
</td>
</tr>
`;
}
/**
* Generate CSS styles
*/
generateCSS() {
return `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color:
background-color:
}
.report-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: white;
min-height: 100vh;
}
.report-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid
}
.report-header h1 {
color:
margin-bottom: 10px;
font-size: 2.5em;
}
.report-meta {
display: flex;
justify-content: center;
gap: 20px;
color:
font-size: 0.9em;
}
.summary-section {
margin-bottom: 30px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
border-left: 4px solid
}
.summary-card.passed {
border-left-color:
}
.summary-card.failed {
border-left-color:
}
.summary-card.success-rate {
border-left-color:
}
.summary-number {
font-size: 2.5em;
font-weight: bold;
color:
margin-bottom: 5px;
}
.summary-label {
color:
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 1px;
}
.controls-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 20px;
}
.search-box input {
padding: 10px 15px;
border: 1px solid
border-radius: 5px;
width: 300px;
font-size: 14px;
}
.filter-controls select {
padding: 10px 15px;
border: 1px solid
border-radius: 5px;
margin-left: 10px;
font-size: 14px;
}
.tests-section h2 {
margin-bottom: 20px;
color:
}
.tests-table-container {
overflow-x: auto;
}
.tests-table {
width: 100%;
border-collapse: collapse;
background: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
border-radius: 10px;
overflow: hidden;
}
.tests-table th {
background:
color: white;
padding: 15px;
text-align: left;
font-weight: 600;
}
.tests-table td {
padding: 15px;
border-bottom: 1px solid
}
.tests-table tr:hover {
background-color:
}
.status-badge {
padding: 5px 10px;
border-radius: 15px;
font-size: 0.8em;
font-weight: 600;
text-transform: uppercase;
}
.status-passed {
background:
color:
}
.status-failed {
background:
color:
}
.btn-details, .btn-screenshots {
padding: 5px 10px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.8em;
margin-right: 5px;
}
.btn-details {
background:
color: white;
}
.btn-screenshots {
background:
color: white;
}
.btn-details:hover, .btn-screenshots:hover {
opacity: 0.8;
}
.details-section {
margin-top: 30px;
padding: 20px;
background:
border-radius: 10px;
border: 1px solid
}
.test-details {
background: white;
padding: 20px;
border-radius: 10px;
margin-top: 15px;
}
.screenshots-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.screenshot-item {
text-align: center;
}
.screenshot-item img {
max-width: 100%;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
cursor: pointer;
}
.screenshot-item img:hover {
transform: scale(1.05);
transition: transform 0.2s;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
}
.modal-content {
margin: auto;
display: block;
max-width: 90%;
max-height: 90%;
margin-top: 5%;
}
.close {
position: absolute;
top: 15px;
right: 35px;
color:
font-size: 40px;
font-weight: bold;
cursor: pointer;
}
@media (max-width: 768px) {
.controls-section {
flex-direction: column;
align-items: stretch;
}
.search-box input {
width: 100%;
}
.filter-controls select {
margin-left: 0;
margin-top: 10px;
}
.summary-grid {
grid-template-columns: 1fr;
}
}
`;
}
/**
* Generate summary CSS styles
*/
generateSummaryCSS() {
return `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color:
background-color:
}
.summary-report {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: white;
min-height: 100vh;
}
.report-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid
}
.report-header h1 {
color:
margin-bottom: 10px;
font-size: 2.5em;
}
.report-meta {
display: flex;
justify-content: center;
gap: 20px;
color:
font-size: 0.9em;
}
.summary-section {
margin-bottom: 30px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
border-left: 4px solid
}
.summary-card.passed {
border-left-color:
}
.summary-card.failed {
border-left-color:
}
.summary-card.success-rate {
border-left-color:
}
.summary-number {
font-size: 2.5em;
font-weight: bold;
color:
margin-bottom: 5px;
}
.summary-label {
color:
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 1px;
}
.quick-stats {
background:
padding: 20px;
border-radius: 10px;
border: 1px solid
}
.quick-stats h3 {
margin-bottom: 15px;
color:
}
.quick-stats ul {
list-style: none;
}
.quick-stats li {
padding: 8px 0;
border-bottom: 1px solid
}
.quick-stats li:last-child {
border-bottom: none;
}
`;
}
/**
* Generate JavaScript for interactivity
*/
generateJavaScript() {
return `
// Search functionality
document.getElementById('testSearch').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('.test-row');
rows.forEach(row => {
const testName = row.querySelector('.test-name').textContent.toLowerCase();
const tags = row.querySelector('.test-tags').textContent.toLowerCase();
if (testName.includes(searchTerm) || tags.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
// Status filter
document.getElementById('statusFilter').addEventListener('change', function(e) {
const status = e.target.value;
const rows = document.querySelectorAll('.test-row');
rows.forEach(row => {
const rowStatus = row.getAttribute('data-status');
if (!status || rowStatus === status) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
// Priority filter
document.getElementById('priorityFilter').addEventListener('change', function(e) {
const priority = e.target.value;
const rows = document.querySelectorAll('.test-row');
rows.forEach(row => {
const rowPriority = row.querySelector('.test-priority').textContent;
if (!priority || rowPriority === priority) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
// Show test details
function showTestDetails(testId) {
const detailsSection = document.getElementById('testDetails');
const contentDiv = document.getElementById('testDetailsContent');
// Find test data (in a real implementation, this would come from the server)
const testRow = document.querySelector(\`[data-test-id="\${testId}"]\`);
const testName = testRow.querySelector('.test-name').textContent;
const testStatus = testRow.querySelector('.status-badge').textContent;
const testDuration = testRow.querySelector('.test-duration').textContent;
const testTags = testRow.querySelector('.test-tags').textContent;
contentDiv.innerHTML = \`
<div class="test-details">
<h4>\${testName}</h4>
<p><strong>Status:</strong> \${testStatus}</p>
<p><strong>Duration:</strong> \${testDuration}</p>
<p><strong>Tags:</strong> \${testTags}</p>
<p><strong>Test ID:</strong> \${testId}</p>
</div>
\`;
detailsSection.style.display = 'block';
detailsSection.scrollIntoView({ behavior: 'smooth' });
}
// Show screenshots
function showScreenshots(testId) {
// In a real implementation, this would load screenshots from the server
alert('Screenshots feature would show images for test: ' + testId);
}
// Modal functionality for screenshots
function createModal() {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = \`
<span class="close">×</span>
<img class="modal-content" id="modalImg">
\`;
document.body.appendChild(modal);
modal.querySelector('.close').onclick = function() {
modal.style.display = 'none';
};
return modal;
}
// Initialize modal
const modal = createModal();
`;
}
/**
* Create full HTML document
*/
createFullHTML(htmlContent, cssContent, jsContent) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${this.options.title}</title>
<style>${cssContent}</style>
</head>
<body>
${htmlContent}
<script>${jsContent}</script>
</body>
</html>`;
}
/**
* Format duration in milliseconds to human readable format
*/
formatDuration(ms) {
if (ms < 1000)
return `${ms}ms`;
if (ms < 60000)
return `${(ms / 1000).toFixed(1)}s`;
const minutes = Math.floor(ms / 60000);
const seconds = ((ms % 60000) / 1000).toFixed(1);
return `${minutes}m ${seconds}s`;
}
/**
* Calculate tests per minute
*/
calculateTestsPerMinute(durationMs, testCount) {
if (durationMs === 0)
return '0';
const minutes = durationMs / 60000;
return (testCount / minutes).toFixed(1);
}
}
exports.HTMLReportGenerator = HTMLReportGenerator;
//# sourceMappingURL=HTMLReportGenerator.js.map