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 ` -
-
- -
-

${test.isBdd ? `Scenario: ${test.title}` : test.title}

-
- ${test.isBdd ? 'Feature: ' : ''}${feature} - ${test.uid ? `${test.uid}` : ''} - ${formatDuration(test.duration)} - ${test.retryAttempts > 0 ? `${test.retryAttempts} retries` : ''} - ${test.isBdd ? 'Gherkin' : ''} -
+
+

+ ${escapeHtml(feature)} + +

+
+ ${tests + .map(test => { + const statusClass = test.state || 'unknown' + 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, test.state) : '' + const notes = test.notes && test.notes.length > 0 ? generateNotesHtml(test.notes) : '' + + // Worker badge - show worker index if test has worker info + const workerBadge = test.workerIndex !== undefined ? `Worker ${test.workerIndex}` : '' + + return ` +
+
+ +
+

${test.isBdd ? `Scenario: ${test.title}` : test.title}

+
+ ${workerBadge} + ${test.uid ? `${test.uid}` : ''} + ${formatDuration(test.duration)} + ${test.retryAttempts > 0 ? `${test.retryAttempts} retries` : ''} + ${test.isBdd ? 'Gherkin' : ''} +
+
+
+
+ ${test.err ? `
${escapeHtml(getErrorMessage(test))}
` : ''} + ${featureDetails} + ${tags} + ${metadata} + ${retries} + ${notes} + ${hooks} + ${steps} + ${artifacts} +
+
+ ` + }) + .join('')}
-
-
- ${test.err ? `
${escapeHtml(getErrorMessage(test))}
` : ''} - ${featureDetails} - ${tags} - ${metadata} - ${retries} - ${notes} - ${hooks} - ${steps} - ${artifacts} -
-
- ` + + ` }) .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}} + -
-

{{title}}

-
- Generated: {{timestamp}} - Duration: {{duration}} -
-
+
+

{{title}}

+
+ Generated: {{timestamp}} + Duration: {{duration}} +
+
-
- {{systemInfoHtml}} +
+ {{systemInfoHtml}} -
-

Test Statistics

- {{statsHtml}} +
+

Test Statistics

+ {{statsHtml}} +
+ +
+

Test Performance Analysis

+
+
+

⏱️ Longest Running Tests

+
+
+
+

⚡ Fastest Tests

+
+
+
-

Test History

+

Test Execution History

+
+
@@ -1654,10 +1788,10 @@ module.exports = function (config) {
-
-

Test Retries

+ @@ -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(/