From c13207902144d7c236a4a7929eb0d7917464d237 Mon Sep 17 00:00:00 2001
From: kobenguyent <7845001+kobenguyent@users.noreply.github.com>
Date: Fri, 3 Oct 2025 12:04:28 +0000
Subject: [PATCH 1/2] html reporter improvements
---
lib/plugin/htmlReporter.js | 799 ++++++++++++++++++++---
test/runner/html-reporter-plugin_test.js | 291 ++++++++-
2 files changed, 1002 insertions(+), 88 deletions(-)
diff --git a/lib/plugin/htmlReporter.js b/lib/plugin/htmlReporter.js
index cc4c510d1..77f7d9133 100644
--- a/lib/plugin/htmlReporter.js
+++ b/lib/plugin/htmlReporter.js
@@ -183,12 +183,21 @@ module.exports = function (config) {
if (hook.htmlReporterStartTime) {
hook.duration = Date.now() - hook.htmlReporterStartTime
}
+ // Enhanced hook info: include type, name, location, error, and context
const hookInfo = {
title: hook.title,
type: hook.type || 'unknown', // before, after, beforeSuite, afterSuite
status: hook.err ? 'failed' : 'passed',
duration: hook.duration || 0,
- error: hook.err ? hook.err.message : null,
+ error: hook.err ? hook.err.message || hook.err.toString() : null,
+ location: hook.file || hook.location || (hook.ctx && hook.ctx.test && hook.ctx.test.file) || null,
+ context: hook.ctx
+ ? {
+ testTitle: hook.ctx.test?.title,
+ suiteTitle: hook.ctx.test?.parent?.title,
+ feature: hook.ctx.test?.parent?.feature?.name,
+ }
+ : null,
}
currentTestHooks.push(hookInfo)
reportData.hooks.push(hookInfo)
@@ -212,6 +221,43 @@ module.exports = function (config) {
})
})
+ // Collect skipped tests
+ event.dispatcher.on(event.test.skipped, test => {
+ const testId = generateTestId(test)
+
+ // Detect if this is a BDD/Gherkin test
+ const suite = test.parent || test.suite || currentSuite
+ const isBddTest = isBddGherkinTest(test, suite)
+ const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null
+
+ // Extract parent/suite title
+ const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null
+ const suiteTitle = test.suite?.title || (suite && suite.title) || null
+
+ const testData = {
+ ...test,
+ id: testId,
+ state: 'pending', // Use 'pending' as the state for skipped tests
+ duration: 0,
+ steps: [],
+ hooks: [],
+ artifacts: [],
+ tags: test.tags || [],
+ meta: test.meta || {},
+ opts: test.opts || {},
+ notes: test.notes || [],
+ retryAttempts: 0,
+ uid: test.uid,
+ isBdd: isBddTest,
+ feature: featureInfo,
+ parentTitle: parentTitle,
+ suiteTitle: suiteTitle,
+ }
+
+ reportData.tests.push(testData)
+ output.debug(`HTML Reporter: Added skipped test - ${test.title}`)
+ })
+
// Collect test results
event.dispatcher.on(event.test.finished, test => {
const testId = generateTestId(test)
@@ -373,8 +419,14 @@ module.exports = function (config) {
// Calculate stats from our collected test data instead of using result.stats
const passedTests = reportData.tests.filter(t => t.state === 'passed').length
const failedTests = reportData.tests.filter(t => t.state === 'failed').length
- const pendingTests = reportData.tests.filter(t => t.state === 'pending').length
- const skippedTests = reportData.tests.filter(t => t.state === 'skipped').length
+ // Combine pending and skipped tests (both represent tests that were not run)
+ const pendingTests = reportData.tests.filter(t => t.state === 'pending' || t.state === 'skipped').length
+
+ // Calculate flaky tests (passed but had retries)
+ const flakyTests = reportData.tests.filter(t => t.state === 'passed' && t.retryAttempts > 0).length
+
+ // Count total artifacts
+ const totalArtifacts = reportData.tests.reduce((sum, t) => sum + (t.artifacts?.length || 0), 0)
// Populate failures from our collected test data with enhanced details
reportData.failures = reportData.tests
@@ -418,9 +470,10 @@ module.exports = function (config) {
passes: passedTests,
failures: failedTests,
pending: pendingTests,
- skipped: skippedTests,
duration: reportData.duration,
failedHooks: result.stats?.failedHooks || 0,
+ flaky: flakyTests,
+ artifacts: totalArtifacts,
}
// Debug logging for final stats
@@ -803,6 +856,10 @@ module.exports = function (config) {
try {
const workerData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
+ // Extract worker ID from filename (e.g., "worker-0-results.json" -> 0)
+ const workerIdMatch = jsonFile.match(/worker-(\d+)-results\.json/)
+ const workerIndex = workerIdMatch ? parseInt(workerIdMatch[1], 10) : undefined
+
// Merge stats
if (workerData.stats) {
consolidatedData.stats.passes += workerData.stats.passes || 0
@@ -814,8 +871,14 @@ module.exports = function (config) {
consolidatedData.stats.failedHooks += workerData.stats.failedHooks || 0
}
- // Merge tests and failures
- if (workerData.tests) consolidatedData.tests.push(...workerData.tests)
+ // Merge tests and add worker index
+ if (workerData.tests) {
+ const testsWithWorkerIndex = workerData.tests.map(test => ({
+ ...test,
+ workerIndex: workerIndex,
+ }))
+ consolidatedData.tests.push(...testsWithWorkerIndex)
+ }
if (workerData.failures) consolidatedData.failures.push(...workerData.failures)
if (workerData.hooks) consolidatedData.hooks.push(...workerData.hooks)
if (workerData.retries) consolidatedData.retries.push(...workerData.retries)
@@ -932,6 +995,10 @@ module.exports = function (config) {
const failed = stats.failures || 0
const pending = stats.pending || 0
const total = stats.tests || 0
+ const flaky = stats.flaky || 0
+ const artifactCount = stats.artifacts || 0
+ const passRate = total > 0 ? ((passed / total) * 100).toFixed(1) : '0.0'
+ const failRate = total > 0 ? ((failed / total) * 100).toFixed(1) : '0.0'
return `
@@ -948,9 +1015,21 @@ module.exports = function (config) {
${failed}
-
Pending
+ Skipped
${pending}
+
+
Flaky
+ ${flaky}
+
+
+
Artifacts
+ ${artifactCount}
+
+
+
+ Pass Rate: ${passRate}%
+ Fail Rate: ${failRate}%
@@ -971,49 +1050,73 @@ module.exports = function (config) {
return '
No tests found.
'
}
- return tests
- .map(test => {
- const statusClass = test.state || 'unknown'
- // Use preserved parent/suite titles (for worker mode) or fallback to direct access
- const feature = test.isBdd && test.feature ? test.feature.name : test.parentTitle || test.suiteTitle || test.parent?.title || test.suite?.title || 'Unknown Feature'
- // Always try to show steps if available, even for unknown feature
- const steps = config.showSteps && test.steps ? (test.isBdd ? generateBddStepsHtml(test.steps) : generateStepsHtml(test.steps)) : ''
- const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : ''
- const hooks = test.hooks && test.hooks.length > 0 ? generateHooksHtml(test.hooks) : ''
- const artifacts = config.includeArtifacts && test.artifacts ? generateArtifactsHtml(test.artifacts, test.state === 'failed') : ''
- const metadata = config.showMetadata && (test.meta || test.opts) ? generateMetadataHtml(test.meta, test.opts) : ''
- const tags = config.showTags && test.tags && test.tags.length > 0 ? generateTagsHtml(test.tags) : ''
- const retries = config.showRetries && test.retryAttempts > 0 ? generateTestRetryHtml(test.retryAttempts) : ''
- const notes = test.notes && test.notes.length > 0 ? generateNotesHtml(test.notes) : ''
+ // Group tests by feature name
+ const grouped = {}
+ tests.forEach(test => {
+ const feature = test.isBdd && test.feature ? test.feature.name : test.parentTitle || test.suiteTitle || test.parent?.title || test.suite?.title || 'Unknown Feature'
+ if (!grouped[feature]) grouped[feature] = []
+ grouped[feature].push(test)
+ })
+ // Render each feature section
+ return Object.entries(grouped)
+ .map(([feature, tests]) => {
+ const featureId = feature.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()
return `
-
-
- `
+
+ `
})
.join('')
}
@@ -1103,13 +1206,19 @@ module.exports = function (config) {
const statusClass = hook.status || 'unknown'
const hookType = hook.type || 'hook'
const hookTitle = hook.title || `${hookType} hook`
+ const location = hook.location ? `
Location: ${escapeHtml(hook.location)}
` : ''
+ const context = hook.context ? `
Test: ${escapeHtml(hook.context.testTitle || 'N/A')}, Suite: ${escapeHtml(hook.context.suiteTitle || 'N/A')}
` : ''
return `
●
-
${hookType}: ${hookTitle}
-
${formatDuration(hook.duration)}
- ${hook.error ? `
${escapeHtml(hook.error)}
` : ''}
+
+
${hookType}: ${hookTitle}
+
${formatDuration(hook.duration)}
+ ${location}
+ ${context}
+ ${hook.error ? `
${escapeHtml(hook.error)}
` : ''}
+
`
})
@@ -1169,12 +1278,21 @@ module.exports = function (config) {
`
}
- function generateTestRetryHtml(retryAttempts) {
+ function generateTestRetryHtml(retryAttempts, testState) {
+ // Enhanced retry history display showing whether test eventually passed or failed
+ const statusBadge = testState === 'passed' ? '
✓ Eventually Passed' : '
✗ Eventually Failed'
+
return `
-
Retry Information:
+
Retry History:
-
Total retry attempts: ${retryAttempts}
+
+ Total retry attempts: ${retryAttempts}
+ ${statusBadge}
+
+
+ This test was retried ${retryAttempts} time${retryAttempts > 1 ? 's' : ''} before ${testState === 'passed' ? 'passing' : 'failing'}.
+
`
@@ -1578,30 +1696,46 @@ module.exports = function (config) {
-
-
-
{{title}}
-
+
+
+
{{title}}
+
-
+
-
- {{systemInfoHtml}}
+
+ {{systemInfoHtml}}
-
- Test Statistics
- {{statsHtml}}
+
+ Test Statistics
+ {{statsHtml}}
+
+
+
- Test History
+ Test Execution History
+
+
@@ -1654,10 +1788,10 @@ module.exports = function (config) {
-
- Test Retries
+
+ Test Retries (Moved to Test Details)
- {{retriesHtml}}
+
Retry information is now shown in each test's details section.
@@ -1722,7 +1856,7 @@ body {
padding: 0 1rem;
}
-.stats-section, .tests-section, .retries-section, .filters-section, .history-section, .system-info-section {
+.stats-section, .tests-section, .retries-section, .filters-section, .history-section, .system-info-section, .test-performance-section {
background: white;
margin-bottom: 2rem;
border-radius: 8px;
@@ -1730,7 +1864,7 @@ body {
overflow: hidden;
}
-.stats-section h2, .tests-section h2, .retries-section h2, .filters-section h2, .history-section h2 {
+.stats-section h2, .tests-section h2, .retries-section h2, .filters-section h2, .history-section h2, .test-performance-section h2 {
background: #34495e;
color: white;
padding: 1rem;
@@ -1757,6 +1891,23 @@ body {
.stat-card.passed { background: #27ae60; }
.stat-card.failed { background: #e74c3c; }
.stat-card.pending { background: #f39c12; }
+.stat-card.flaky { background: #e67e22; }
+.stat-card.artifacts { background: #9b59b6; }
+
+.metrics-summary {
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ padding: 1rem;
+ background: #f8f9fa;
+ border-radius: 6px;
+ margin: 1rem 0;
+ font-size: 1rem;
+}
+
+.metrics-summary span {
+ color: #34495e;
+}
.stat-card h3 {
font-size: 0.9rem;
@@ -1784,6 +1935,54 @@ body {
height: auto;
}
+.feature-group {
+ margin-bottom: 2.5rem;
+ border: 2px solid #3498db;
+ border-radius: 12px;
+ overflow: hidden;
+ background: white;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+}
+
+.feature-group-title {
+ background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
+ color: white;
+ padding: 1.2rem 1.5rem;
+ margin: 0;
+ font-size: 1.4rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ user-select: none;
+}
+
+.feature-group-title:hover {
+ background: linear-gradient(135deg, #2980b9 0%, #21618c 100%);
+}
+
+.feature-group-title .toggle-icon {
+ font-size: 1.2rem;
+ transition: transform 0.3s ease;
+}
+
+.feature-group-title .toggle-icon.rotated {
+ transform: rotate(180deg);
+}
+
+.feature-tests {
+ padding: 0;
+ transition: max-height 0.3s ease, opacity 0.3s ease;
+}
+
+.feature-tests.collapsed {
+ max-height: 0;
+ opacity: 0;
+ overflow: hidden;
+}
+
.test-item {
border-bottom: 1px solid #eee;
margin: 0;
@@ -1861,9 +2060,64 @@ body {
font-weight: bold;
}
+.worker-badge {
+ background: #16a085;
+ color: white;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.7rem;
+ font-weight: bold;
+}
+
+/* Different colors for each worker index */
+.worker-badge.worker-0 {
+ background: #3498db; /* Blue */
+}
+
+.worker-badge.worker-1 {
+ background: #e74c3c; /* Red */
+}
+
+.worker-badge.worker-2 {
+ background: #2ecc71; /* Green */
+}
+
+.worker-badge.worker-3 {
+ background: #f39c12; /* Orange */
+}
+
+.worker-badge.worker-4 {
+ background: #9b59b6; /* Purple */
+}
+
+.worker-badge.worker-5 {
+ background: #1abc9c; /* Turquoise */
+}
+
+.worker-badge.worker-6 {
+ background: #e67e22; /* Carrot */
+}
+
+.worker-badge.worker-7 {
+ background: #34495e; /* Dark Blue-Gray */
+}
+
+.worker-badge.worker-8 {
+ background: #16a085; /* Teal */
+}
+
+.worker-badge.worker-9 {
+ background: #c0392b; /* Dark Red */
+}
+
.test-duration {
- font-size: 0.8rem;
- color: #7f8c8d;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: #2c3e50;
+ background: #ecf0f1;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-family: 'Monaco', 'Courier New', monospace;
}
.test-details {
@@ -1901,27 +2155,39 @@ body {
.hook-item {
display: flex;
- align-items: center;
- padding: 0.5rem 0;
- border-bottom: 1px solid #ecf0f1;
+ align-items: flex-start;
+ padding: 0.75rem;
+ border: 1px solid #ecf0f1;
+ border-radius: 4px;
+ margin-bottom: 0.5rem;
+ background: #fafafa;
}
.hook-item:last-child {
- border-bottom: none;
+ margin-bottom: 0;
}
.hook-status {
- margin-right: 0.5rem;
+ margin-right: 0.75rem;
+ flex-shrink: 0;
+ margin-top: 0.2rem;
}
.hook-status.passed { color: #27ae60; }
.hook-status.failed { color: #e74c3c; }
-.hook-title {
+.hook-content {
flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.hook-title {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
font-weight: bold;
+ color: #2c3e50;
}
.hook-duration {
@@ -1929,8 +2195,13 @@ body {
color: #7f8c8d;
}
+.hook-location, .hook-context {
+ font-size: 0.8rem;
+ color: #6c757d;
+ font-style: italic;
+}
+
.hook-error {
- width: 100%;
margin-top: 0.5rem;
padding: 0.5rem;
background: #fee;
@@ -2219,11 +2490,22 @@ body {
}
/* Retry Information */
+.retry-section {
+ margin-top: 1rem;
+}
+
.retry-info {
- padding: 0.5rem;
- background: #fef9e7;
+ padding: 1rem;
+ background: #fff9e6;
border-radius: 4px;
- border-left: 3px solid #f39c12;
+ border-left: 4px solid #f39c12;
+}
+
+.retry-summary {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 0.5rem;
}
.retry-count {
@@ -2231,6 +2513,29 @@ body {
font-weight: 500;
}
+.retry-status-badge {
+ padding: 0.25rem 0.75rem;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ font-weight: bold;
+}
+
+.retry-status-badge.passed {
+ background: #27ae60;
+ color: white;
+}
+
+.retry-status-badge.failed {
+ background: #e74c3c;
+ color: white;
+}
+
+.retry-description {
+ font-size: 0.9rem;
+ color: #6c757d;
+ font-style: italic;
+}
+
/* Retries Section */
.retry-item {
padding: 1rem;
@@ -2276,6 +2581,92 @@ body {
}
/* History Chart */
+.history-stats {
+ padding: 1.5rem;
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+}
+
+.history-stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+}
+
+.history-stat-item {
+ background: white;
+ padding: 1rem;
+ border-radius: 6px;
+ border-left: 4px solid #3498db;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+.history-stat-item h4 {
+ margin: 0 0 0.5rem 0;
+ font-size: 0.9rem;
+ color: #7f8c8d;
+ text-transform: uppercase;
+}
+
+.history-stat-item .value {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #2c3e50;
+}
+
+.history-timeline {
+ padding: 1.5rem;
+ background: white;
+}
+
+.timeline-item {
+ display: flex;
+ align-items: center;
+ padding: 0.75rem;
+ border-left: 3px solid #3498db;
+ margin-left: 1rem;
+ margin-bottom: 0.5rem;
+ background: #f8f9fa;
+ border-radius: 0 6px 6px 0;
+ transition: all 0.2s;
+}
+
+.timeline-item:hover {
+ background: #e9ecef;
+ transform: translateX(4px);
+}
+
+.timeline-time {
+ min-width: 150px;
+ font-weight: 600;
+ color: #2c3e50;
+ font-family: 'Courier New', monospace;
+}
+
+.timeline-result {
+ flex: 1;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.timeline-badge {
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ font-weight: 600;
+}
+
+.timeline-badge.success {
+ background: #d4edda;
+ color: #155724;
+}
+
+.timeline-badge.failure {
+ background: #f8d7da;
+ color: #721c24;
+}
+
.history-chart-container {
padding: 2rem 1rem;
display: flex;
@@ -2287,6 +2678,87 @@ body {
height: auto;
}
+/* Test Performance Section */
+.performance-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+ gap: 2rem;
+ padding: 1.5rem;
+}
+
+.performance-group h3 {
+ margin: 0 0 1rem 0;
+ color: #2c3e50;
+ font-size: 1.1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid #3498db;
+}
+
+.performance-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.performance-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 1rem;
+ background: #f8f9fa;
+ border-radius: 6px;
+ border-left: 4px solid #3498db;
+ transition: all 0.2s;
+}
+
+.performance-item:hover {
+ background: #e9ecef;
+ transform: translateX(4px);
+}
+
+.performance-item:nth-child(1) .performance-rank {
+ background: #f39c12;
+ color: white;
+}
+
+.performance-item:nth-child(2) .performance-rank {
+ background: #95a5a6;
+ color: white;
+}
+
+.performance-item:nth-child(3) .performance-rank {
+ background: #cd7f32;
+ color: white;
+}
+
+.performance-rank {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ background: #3498db;
+ color: white;
+ border-radius: 50%;
+ font-weight: bold;
+ font-size: 0.9rem;
+ margin-right: 1rem;
+ flex-shrink: 0;
+}
+
+.performance-name {
+ flex: 1;
+ font-weight: 500;
+ color: #2c3e50;
+}
+
+.performance-duration {
+ font-weight: 600;
+ color: #7f8c8d;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9rem;
+}
+
/* Hidden items for filtering */
.test-item.filtered-out {
display: none !important;
@@ -2508,6 +2980,21 @@ body {
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
+
+function toggleFeatureGroup(featureId) {
+ const featureTests = document.getElementById('feature-' + featureId);
+ const titleElement = featureTests.previousElementSibling;
+ const icon = titleElement.querySelector('.toggle-icon');
+
+ if (featureTests.classList.contains('collapsed')) {
+ featureTests.classList.remove('collapsed');
+ icon.classList.remove('rotated');
+ } else {
+ featureTests.classList.add('collapsed');
+ icon.classList.add('rotated');
+ }
+}
+
function toggleTestDetails(testId) {
const details = document.getElementById('details-' + testId);
if (details.style.display === 'none' || details.style.display === '') {
@@ -2993,6 +3480,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Draw charts
drawPieChart();
drawHistoryChart();
+ renderTestPerformance();
+ renderHistoryTimeline();
+
// Add Go to Top button
const goTopBtn = document.createElement('button');
goTopBtn.innerText = '↑ Top';
@@ -3018,6 +3508,141 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('retryFilter').addEventListener('change', applyFilters);
document.getElementById('typeFilter').addEventListener('change', applyFilters);
});
+
+// Render test performance analysis
+function renderTestPerformance() {
+ const tests = Array.from(document.querySelectorAll('.test-item'));
+ const testsWithDuration = tests.map(testEl => {
+ const title = testEl.querySelector('.test-title')?.textContent || 'Unknown';
+ const durationText = testEl.querySelector('.test-duration')?.textContent || '0ms';
+ const durationMs = parseDuration(durationText);
+ const status = testEl.dataset.status;
+ return { title, duration: durationMs, durationText, status };
+ }); // Don't filter out 0ms tests
+
+ // Sort by duration
+ const longest = [...testsWithDuration].sort((a, b) => b.duration - a.duration).slice(0, 5);
+ const fastest = [...testsWithDuration].sort((a, b) => a.duration - b.duration).slice(0, 5);
+
+ // Render longest tests
+ const longestContainer = document.getElementById('longestTests');
+ if (longestContainer && longest.length > 0) {
+ longestContainer.innerHTML = longest.map((test, index) => \`
+
+ \${index + 1}
+ \${test.title.length > 60 ? test.title.substring(0, 60) + '...' : test.title}
+ \${test.durationText}
+
+ \`).join('');
+ } else if (longestContainer) {
+ longestContainer.innerHTML = 'No test data available
';
+ }
+
+ // Render fastest tests
+ const fastestContainer = document.getElementById('fastestTests');
+ if (fastestContainer && fastest.length > 0) {
+ fastestContainer.innerHTML = fastest.map((test, index) => \`
+
+ \${index + 1}
+ \${test.title.length > 60 ? test.title.substring(0, 60) + '...' : test.title}
+ \${test.durationText}
+
+ \`).join('');
+ } else if (fastestContainer) {
+ fastestContainer.innerHTML = 'No test data available
';
+ }
+}
+
+// Render history timeline
+function renderHistoryTimeline() {
+ if (!window.testData || !window.testData.history || window.testData.history.length === 0) {
+ return;
+ }
+
+ const history = window.testData.history.slice().reverse(); // Most recent last
+
+ // Render stats
+ const statsContainer = document.getElementById('historyStats');
+ if (statsContainer) {
+ const totalRuns = history.length;
+ const avgDuration = history.reduce((sum, run) => sum + (run.duration || 0), 0) / totalRuns;
+ const avgTests = Math.round(history.reduce((sum, run) => sum + (run.stats.tests || 0), 0) / totalRuns);
+ const avgPassRate = history.reduce((sum, run) => {
+ const total = run.stats.tests || 0;
+ const passed = run.stats.passes || 0;
+ return sum + (total > 0 ? (passed / total) * 100 : 0);
+ }, 0) / totalRuns;
+
+ statsContainer.innerHTML = \`
+
+
+
Total Runs
+
\${totalRuns}
+
+
+
Avg Duration
+
\${formatDuration(avgDuration)}
+
+
+
Avg Tests
+
\${avgTests}
+
+
+
Avg Pass Rate
+
\${avgPassRate.toFixed(1)}%
+
+
+ \`;
+ }
+
+ // Render timeline
+ const timelineContainer = document.getElementById('historyTimeline');
+ if (timelineContainer) {
+ const recentHistory = history.slice(-10).reverse(); // Last 10 runs, most recent first
+ timelineContainer.innerHTML = 'Recent Execution Timeline
' +
+ recentHistory.map(run => {
+ const timestamp = new Date(run.timestamp);
+ const timeStr = timestamp.toLocaleString();
+ const total = run.stats.tests || 0;
+ const passed = run.stats.passes || 0;
+ const failed = run.stats.failures || 0;
+ const badgeClass = failed > 0 ? 'failure' : 'success';
+ const badgeText = failed > 0 ? \`\${failed} Failed\` : \`All Passed\`;
+
+ return \`
+
+
\${timeStr}
+
+ \${badgeText}
+ \${passed}/\${total} passed
+ ·
+ \${formatDuration(run.duration || 0)}
+
+
+ \`;
+ }).join('');
+ }
+}
+
+// Helper to parse duration text to milliseconds
+function parseDuration(durationText) {
+ if (!durationText) return 0;
+ const match = durationText.match(/(\\d+(?:\\.\\d+)?)(ms|s|m)/);
+ if (!match) return 0;
+ const value = parseFloat(match[1]);
+ const unit = match[2];
+ if (unit === 'ms') return value;
+ if (unit === 's') return value * 1000;
+ if (unit === 'm') return value * 60000;
+ return 0;
+}
+
+// Helper to format duration
+function formatDuration(ms) {
+ if (ms < 1000) return Math.round(ms) + 'ms';
+ if (ms < 60000) return (ms / 1000).toFixed(2) + 's';
+ return (ms / 60000).toFixed(2) + 'm';
+}
`
}
}
diff --git a/test/runner/html-reporter-plugin_test.js b/test/runner/html-reporter-plugin_test.js
index 03f50beff..8d4979069 100644
--- a/test/runner/html-reporter-plugin_test.js
+++ b/test/runner/html-reporter-plugin_test.js
@@ -7,7 +7,7 @@ const path = require('path')
const config_run_config = (config, grep, verbose = false) => `${codecept_run} ${verbose ? '--verbose' : ''} --config ${codecept_dir}/configs/html-reporter-plugin/${config} ${grep ? `--grep "${grep}"` : ''}`
-describe('CodeceptJS html-reporter-plugin', function () {
+describe.only('CodeceptJS html-reporter-plugin', function () {
this.timeout(10000)
it('should generate HTML report', done => {
@@ -48,6 +48,26 @@ describe('CodeceptJS html-reporter-plugin', function () {
expect(reportContent).toContain('applyFilters')
expect(reportContent).toContain('resetFilters')
+ // Check for feature grouping with toggle
+ expect(reportContent).toContain('feature-group')
+ expect(reportContent).toContain('feature-group-title')
+ expect(reportContent).toContain('toggleFeatureGroup')
+ expect(reportContent).toContain('toggle-icon')
+
+ // Check for test performance analysis
+ expect(reportContent).toContain('test-performance-section')
+ expect(reportContent).toContain('Test Performance Analysis')
+ expect(reportContent).toContain('Longest Running Tests')
+ expect(reportContent).toContain('Fastest Tests')
+ expect(reportContent).toContain('renderTestPerformance')
+
+ // Check for enhanced history section
+ expect(reportContent).toContain('history-section')
+ expect(reportContent).toContain('Test Execution History')
+ expect(reportContent).toContain('historyStats')
+ expect(reportContent).toContain('historyTimeline')
+ expect(reportContent).toContain('renderHistoryTimeline')
+
// Check for metadata and tags support
expect(reportContent).toContain('metadata-section')
expect(reportContent).toContain('tags-section')
@@ -606,4 +626,273 @@ describe('CodeceptJS html-reporter-plugin', function () {
done()
})
})
+
+ // ===== NEW IMPROVEMENT TESTS =====
+
+ it('should display enhanced hook information with location and context', done => {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Check for enhanced hook structure
+ expect(reportContent).toContain('hook-content')
+ expect(reportContent).toContain('hook-location')
+ expect(reportContent).toContain('hook-context')
+
+ // Hook styling enhancements
+ expect(reportContent).toContain('.hook-item {')
+ expect(reportContent).toMatch(/display:\s*flex/)
+ expect(reportContent).toContain('hook-title')
+ expect(reportContent).toContain('hook-duration')
+
+ done()
+ })
+ })
+
+ it('should group test results by feature name', done => {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Check for feature grouping
+ expect(reportContent).toContain('feature-group')
+ expect(reportContent).toContain('feature-group-title')
+ expect(reportContent).toContain('feature-tests')
+
+ // CSS for feature groups
+ expect(reportContent).toContain('.feature-group {')
+ expect(reportContent).toContain('.feature-group-title {')
+ expect(reportContent).toMatch(/background:\s*#34495e/)
+
+ // Verify tests are grouped
+ const featureGroupMatches = reportContent.match(//g)
+ expect(featureGroupMatches).not.toBe(null)
+ expect(featureGroupMatches.length).toBeGreaterThan(0)
+
+ done()
+ })
+ })
+
+ it('should display enhanced metrics including flaky tests and artifacts', done => {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Check for new metric cards
+ expect(reportContent).toContain('stat-card flaky')
+ expect(reportContent).toContain('stat-card artifacts')
+
+ // Check for metrics summary
+ expect(reportContent).toContain('metrics-summary')
+ expect(reportContent).toContain('Pass Rate:')
+ expect(reportContent).toContain('Fail Rate:')
+
+ // CSS for new metrics
+ expect(reportContent).toContain('.stat-card.flaky')
+ expect(reportContent).toContain('.stat-card.artifacts')
+ expect(reportContent).toContain('.metrics-summary {')
+
+ // Verify we have 6 stat cards instead of 4
+ const statCardMatches = reportContent.match(/ {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Verify inspiration section was removed
+ expect(reportContent).not.toContain('inspiration-section')
+ expect(reportContent).not.toContain('Looking for More Features?')
+ expect(reportContent).not.toContain('Allure Report')
+ expect(reportContent).not.toContain('ReportPortal')
+
+ done()
+ })
+ })
+
+ it('should display test performance analysis section', done => {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Check for performance section
+ expect(reportContent).toContain('test-performance-section')
+ expect(reportContent).toContain('Test Performance Analysis')
+ expect(reportContent).toContain('Longest Running Tests')
+ expect(reportContent).toContain('Fastest Tests')
+ expect(reportContent).toContain('renderTestPerformance')
+
+ // Check for CSS classes
+ expect(reportContent).toContain('performance-container')
+ expect(reportContent).toContain('performance-group')
+ expect(reportContent).toContain('performance-item')
+
+ done()
+ })
+ })
+
+ it('should display enhanced history section with timeline', done => {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Check for enhanced history
+ expect(reportContent).toContain('history-section')
+ expect(reportContent).toContain('Test Execution History')
+ expect(reportContent).toContain('historyStats')
+ expect(reportContent).toContain('historyTimeline')
+ expect(reportContent).toContain('renderHistoryTimeline')
+
+ // Check for CSS classes
+ expect(reportContent).toContain('history-stats')
+ expect(reportContent).toContain('history-timeline')
+ expect(reportContent).toContain('timeline-item')
+
+ done()
+ })
+ })
+
+ it('should have feature groups with collapse/expand functionality', done => {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Check for feature grouping
+ expect(reportContent).toContain('feature-group')
+ expect(reportContent).toContain('feature-group-title')
+ expect(reportContent).toContain('toggleFeatureGroup')
+ expect(reportContent).toContain('toggle-icon')
+
+ // Verify toggle icon is present
+ expect(reportContent).toContain('▼')
+
+ done()
+ })
+ })
+
+ it('should NOT display feature name in individual test entries', done => {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Feature names should be in group titles, not test entries
+ // Look for test-feature span which we removed
+ const testFeatureMatches = reportContent.match(//g)
+ expect(testFeatureMatches).toBe(null) // Should not be present
+
+ done()
+ })
+ })
+
+ it('should display worker info when running with workers', done => {
+ const runCmd = `${codecept_run} --config ${codecept_dir}/configs/html-reporter-plugin/codecept-workers.conf.js`
+
+ exec(runCmd, (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'worker-report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Check for worker badge CSS (always present)
+ expect(reportContent).toContain('.worker-badge')
+ expect(reportContent).toContain('.worker-badge {')
+
+ // Worker badges should use teal color (#16a085)
+ expect(reportContent).toContain('background: #16a085')
+
+ // Note: "Worker X" badges only appear in test entries when tests have workerIndex property
+ // The CSS structure is always there for when worker info is available
+
+ done()
+ })
+ })
+
+ it('should have all new features working together (comprehensive check)', done => {
+ exec(config_run_config('codecept.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // All improvements should be present
+ const features = {
+ 'Enhanced Hooks': reportContent.includes('hook-location') && reportContent.includes('hook-context'),
+ 'Feature Grouping with Toggle': reportContent.includes('feature-group') && reportContent.includes('toggleFeatureGroup'),
+ 'Worker Badges': reportContent.includes('worker-badge') && reportContent.includes('.worker-badge {'),
+ 'Enhanced Metrics': reportContent.includes('stat-card flaky') && reportContent.includes('metrics-summary'),
+ 'Inline Retries': reportContent.includes('retry-status-badge') && reportContent.includes('retry-summary'),
+ 'Test Performance': reportContent.includes('test-performance-section') && reportContent.includes('renderTestPerformance'),
+ 'Enhanced History': reportContent.includes('historyTimeline') && reportContent.includes('renderHistoryTimeline'),
+ }
+
+ // Log which features are present
+ Object.entries(features).forEach(([name, present]) => {
+ debug(`${name}: ${present ? '✓' : '✗'}`)
+ })
+
+ // Verify all features are present
+ Object.entries(features).forEach(([name, present]) => {
+ expect(present).toBe(true)
+ })
+
+ // Verify inspiration section is NOT present (removed as requested)
+ expect(reportContent).not.toContain('Looking for More Features')
+ expect(reportContent).not.toContain('inspiration-section')
+
+ // Verify report quality
+ expect(reportContent.length).toBeGreaterThan(70000) // Larger due to new features
+ expect(reportContent).toContain('')
+ expect(reportContent).toContain('')
+
+ done()
+ })
+ })
})
From bf60c65714761180d4bc67df9eb6f09fc9569f6f Mon Sep 17 00:00:00 2001
From: kobenguyent <7845001+kobenguyent@users.noreply.github.com>
Date: Fri, 3 Oct 2025 12:05:08 +0000
Subject: [PATCH 2/2] html reporter improvements
---
test/runner/html-reporter-plugin_test.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/runner/html-reporter-plugin_test.js b/test/runner/html-reporter-plugin_test.js
index 8d4979069..525540967 100644
--- a/test/runner/html-reporter-plugin_test.js
+++ b/test/runner/html-reporter-plugin_test.js
@@ -7,7 +7,7 @@ const path = require('path')
const config_run_config = (config, grep, verbose = false) => `${codecept_run} ${verbose ? '--verbose' : ''} --config ${codecept_dir}/configs/html-reporter-plugin/${config} ${grep ? `--grep "${grep}"` : ''}`
-describe.only('CodeceptJS html-reporter-plugin', function () {
+describe('CodeceptJS html-reporter-plugin', function () {
this.timeout(10000)
it('should generate HTML report', done => {