codeceptjs 3.7.6-beta.4 → 3.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/codecept.js +15 -2
- package/lib/helper/Playwright.js +5 -1
- package/lib/listener/globalTimeout.js +19 -4
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/htmlReporter.js +741 -88
- package/lib/plugin/retryFailedStep.js +1 -0
- package/lib/result.js +8 -3
- package/lib/step/base.js +1 -1
- package/lib/step/meta.js +1 -1
- package/package.json +17 -17
- package/typings/types.d.ts +14 -3
|
@@ -183,12 +183,21 @@ module.exports = function (config) {
|
|
|
183
183
|
if (hook.htmlReporterStartTime) {
|
|
184
184
|
hook.duration = Date.now() - hook.htmlReporterStartTime
|
|
185
185
|
}
|
|
186
|
+
// Enhanced hook info: include type, name, location, error, and context
|
|
186
187
|
const hookInfo = {
|
|
187
188
|
title: hook.title,
|
|
188
189
|
type: hook.type || 'unknown', // before, after, beforeSuite, afterSuite
|
|
189
190
|
status: hook.err ? 'failed' : 'passed',
|
|
190
191
|
duration: hook.duration || 0,
|
|
191
|
-
error: hook.err ? hook.err.message : null,
|
|
192
|
+
error: hook.err ? hook.err.message || hook.err.toString() : null,
|
|
193
|
+
location: hook.file || hook.location || (hook.ctx && hook.ctx.test && hook.ctx.test.file) || null,
|
|
194
|
+
context: hook.ctx
|
|
195
|
+
? {
|
|
196
|
+
testTitle: hook.ctx.test?.title,
|
|
197
|
+
suiteTitle: hook.ctx.test?.parent?.title,
|
|
198
|
+
feature: hook.ctx.test?.parent?.feature?.name,
|
|
199
|
+
}
|
|
200
|
+
: null,
|
|
192
201
|
}
|
|
193
202
|
currentTestHooks.push(hookInfo)
|
|
194
203
|
reportData.hooks.push(hookInfo)
|
|
@@ -212,6 +221,43 @@ module.exports = function (config) {
|
|
|
212
221
|
})
|
|
213
222
|
})
|
|
214
223
|
|
|
224
|
+
// Collect skipped tests
|
|
225
|
+
event.dispatcher.on(event.test.skipped, test => {
|
|
226
|
+
const testId = generateTestId(test)
|
|
227
|
+
|
|
228
|
+
// Detect if this is a BDD/Gherkin test
|
|
229
|
+
const suite = test.parent || test.suite || currentSuite
|
|
230
|
+
const isBddTest = isBddGherkinTest(test, suite)
|
|
231
|
+
const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null
|
|
232
|
+
|
|
233
|
+
// Extract parent/suite title
|
|
234
|
+
const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null
|
|
235
|
+
const suiteTitle = test.suite?.title || (suite && suite.title) || null
|
|
236
|
+
|
|
237
|
+
const testData = {
|
|
238
|
+
...test,
|
|
239
|
+
id: testId,
|
|
240
|
+
state: 'pending', // Use 'pending' as the state for skipped tests
|
|
241
|
+
duration: 0,
|
|
242
|
+
steps: [],
|
|
243
|
+
hooks: [],
|
|
244
|
+
artifacts: [],
|
|
245
|
+
tags: test.tags || [],
|
|
246
|
+
meta: test.meta || {},
|
|
247
|
+
opts: test.opts || {},
|
|
248
|
+
notes: test.notes || [],
|
|
249
|
+
retryAttempts: 0,
|
|
250
|
+
uid: test.uid,
|
|
251
|
+
isBdd: isBddTest,
|
|
252
|
+
feature: featureInfo,
|
|
253
|
+
parentTitle: parentTitle,
|
|
254
|
+
suiteTitle: suiteTitle,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
reportData.tests.push(testData)
|
|
258
|
+
output.debug(`HTML Reporter: Added skipped test - ${test.title}`)
|
|
259
|
+
})
|
|
260
|
+
|
|
215
261
|
// Collect test results
|
|
216
262
|
event.dispatcher.on(event.test.finished, test => {
|
|
217
263
|
const testId = generateTestId(test)
|
|
@@ -373,8 +419,14 @@ module.exports = function (config) {
|
|
|
373
419
|
// Calculate stats from our collected test data instead of using result.stats
|
|
374
420
|
const passedTests = reportData.tests.filter(t => t.state === 'passed').length
|
|
375
421
|
const failedTests = reportData.tests.filter(t => t.state === 'failed').length
|
|
376
|
-
|
|
377
|
-
const
|
|
422
|
+
// Combine pending and skipped tests (both represent tests that were not run)
|
|
423
|
+
const pendingTests = reportData.tests.filter(t => t.state === 'pending' || t.state === 'skipped').length
|
|
424
|
+
|
|
425
|
+
// Calculate flaky tests (passed but had retries)
|
|
426
|
+
const flakyTests = reportData.tests.filter(t => t.state === 'passed' && t.retryAttempts > 0).length
|
|
427
|
+
|
|
428
|
+
// Count total artifacts
|
|
429
|
+
const totalArtifacts = reportData.tests.reduce((sum, t) => sum + (t.artifacts?.length || 0), 0)
|
|
378
430
|
|
|
379
431
|
// Populate failures from our collected test data with enhanced details
|
|
380
432
|
reportData.failures = reportData.tests
|
|
@@ -418,9 +470,10 @@ module.exports = function (config) {
|
|
|
418
470
|
passes: passedTests,
|
|
419
471
|
failures: failedTests,
|
|
420
472
|
pending: pendingTests,
|
|
421
|
-
skipped: skippedTests,
|
|
422
473
|
duration: reportData.duration,
|
|
423
474
|
failedHooks: result.stats?.failedHooks || 0,
|
|
475
|
+
flaky: flakyTests,
|
|
476
|
+
artifacts: totalArtifacts,
|
|
424
477
|
}
|
|
425
478
|
|
|
426
479
|
// Debug logging for final stats
|
|
@@ -803,6 +856,10 @@ module.exports = function (config) {
|
|
|
803
856
|
try {
|
|
804
857
|
const workerData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
|
805
858
|
|
|
859
|
+
// Extract worker ID from filename (e.g., "worker-0-results.json" -> 0)
|
|
860
|
+
const workerIdMatch = jsonFile.match(/worker-(\d+)-results\.json/)
|
|
861
|
+
const workerIndex = workerIdMatch ? parseInt(workerIdMatch[1], 10) : undefined
|
|
862
|
+
|
|
806
863
|
// Merge stats
|
|
807
864
|
if (workerData.stats) {
|
|
808
865
|
consolidatedData.stats.passes += workerData.stats.passes || 0
|
|
@@ -814,8 +871,14 @@ module.exports = function (config) {
|
|
|
814
871
|
consolidatedData.stats.failedHooks += workerData.stats.failedHooks || 0
|
|
815
872
|
}
|
|
816
873
|
|
|
817
|
-
// Merge tests and
|
|
818
|
-
if (workerData.tests)
|
|
874
|
+
// Merge tests and add worker index
|
|
875
|
+
if (workerData.tests) {
|
|
876
|
+
const testsWithWorkerIndex = workerData.tests.map(test => ({
|
|
877
|
+
...test,
|
|
878
|
+
workerIndex: workerIndex,
|
|
879
|
+
}))
|
|
880
|
+
consolidatedData.tests.push(...testsWithWorkerIndex)
|
|
881
|
+
}
|
|
819
882
|
if (workerData.failures) consolidatedData.failures.push(...workerData.failures)
|
|
820
883
|
if (workerData.hooks) consolidatedData.hooks.push(...workerData.hooks)
|
|
821
884
|
if (workerData.retries) consolidatedData.retries.push(...workerData.retries)
|
|
@@ -932,6 +995,10 @@ module.exports = function (config) {
|
|
|
932
995
|
const failed = stats.failures || 0
|
|
933
996
|
const pending = stats.pending || 0
|
|
934
997
|
const total = stats.tests || 0
|
|
998
|
+
const flaky = stats.flaky || 0
|
|
999
|
+
const artifactCount = stats.artifacts || 0
|
|
1000
|
+
const passRate = total > 0 ? ((passed / total) * 100).toFixed(1) : '0.0'
|
|
1001
|
+
const failRate = total > 0 ? ((failed / total) * 100).toFixed(1) : '0.0'
|
|
935
1002
|
|
|
936
1003
|
return `
|
|
937
1004
|
<div class="stats-cards">
|
|
@@ -948,9 +1015,21 @@ module.exports = function (config) {
|
|
|
948
1015
|
<span class="stat-number">${failed}</span>
|
|
949
1016
|
</div>
|
|
950
1017
|
<div class="stat-card pending">
|
|
951
|
-
<h3>
|
|
1018
|
+
<h3>Skipped</h3>
|
|
952
1019
|
<span class="stat-number">${pending}</span>
|
|
953
1020
|
</div>
|
|
1021
|
+
<div class="stat-card flaky">
|
|
1022
|
+
<h3>Flaky</h3>
|
|
1023
|
+
<span class="stat-number">${flaky}</span>
|
|
1024
|
+
</div>
|
|
1025
|
+
<div class="stat-card artifacts">
|
|
1026
|
+
<h3>Artifacts</h3>
|
|
1027
|
+
<span class="stat-number">${artifactCount}</span>
|
|
1028
|
+
</div>
|
|
1029
|
+
</div>
|
|
1030
|
+
<div class="metrics-summary">
|
|
1031
|
+
<span>Pass Rate: <strong>${passRate}%</strong></span>
|
|
1032
|
+
<span>Fail Rate: <strong>${failRate}%</strong></span>
|
|
954
1033
|
</div>
|
|
955
1034
|
<div class="pie-chart-container">
|
|
956
1035
|
<canvas id="statsChart" width="300" height="300"></canvas>
|
|
@@ -971,49 +1050,73 @@ module.exports = function (config) {
|
|
|
971
1050
|
return '<p>No tests found.</p>'
|
|
972
1051
|
}
|
|
973
1052
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : ''
|
|
982
|
-
const hooks = test.hooks && test.hooks.length > 0 ? generateHooksHtml(test.hooks) : ''
|
|
983
|
-
const artifacts = config.includeArtifacts && test.artifacts ? generateArtifactsHtml(test.artifacts, test.state === 'failed') : ''
|
|
984
|
-
const metadata = config.showMetadata && (test.meta || test.opts) ? generateMetadataHtml(test.meta, test.opts) : ''
|
|
985
|
-
const tags = config.showTags && test.tags && test.tags.length > 0 ? generateTagsHtml(test.tags) : ''
|
|
986
|
-
const retries = config.showRetries && test.retryAttempts > 0 ? generateTestRetryHtml(test.retryAttempts) : ''
|
|
987
|
-
const notes = test.notes && test.notes.length > 0 ? generateNotesHtml(test.notes) : ''
|
|
1053
|
+
// Group tests by feature name
|
|
1054
|
+
const grouped = {}
|
|
1055
|
+
tests.forEach(test => {
|
|
1056
|
+
const feature = test.isBdd && test.feature ? test.feature.name : test.parentTitle || test.suiteTitle || test.parent?.title || test.suite?.title || 'Unknown Feature'
|
|
1057
|
+
if (!grouped[feature]) grouped[feature] = []
|
|
1058
|
+
grouped[feature].push(test)
|
|
1059
|
+
})
|
|
988
1060
|
|
|
1061
|
+
// Render each feature section
|
|
1062
|
+
return Object.entries(grouped)
|
|
1063
|
+
.map(([feature, tests]) => {
|
|
1064
|
+
const featureId = feature.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()
|
|
989
1065
|
return `
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1066
|
+
<section class="feature-group">
|
|
1067
|
+
<h3 class="feature-group-title" onclick="toggleFeatureGroup('${featureId}')">
|
|
1068
|
+
${escapeHtml(feature)}
|
|
1069
|
+
<span class="toggle-icon">▼</span>
|
|
1070
|
+
</h3>
|
|
1071
|
+
<div class="feature-tests" id="feature-${featureId}">
|
|
1072
|
+
${tests
|
|
1073
|
+
.map(test => {
|
|
1074
|
+
const statusClass = test.state || 'unknown'
|
|
1075
|
+
const steps = config.showSteps && test.steps ? (test.isBdd ? generateBddStepsHtml(test.steps) : generateStepsHtml(test.steps)) : ''
|
|
1076
|
+
const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : ''
|
|
1077
|
+
const hooks = test.hooks && test.hooks.length > 0 ? generateHooksHtml(test.hooks) : ''
|
|
1078
|
+
const artifacts = config.includeArtifacts && test.artifacts ? generateArtifactsHtml(test.artifacts, test.state === 'failed') : ''
|
|
1079
|
+
const metadata = config.showMetadata && (test.meta || test.opts) ? generateMetadataHtml(test.meta, test.opts) : ''
|
|
1080
|
+
const tags = config.showTags && test.tags && test.tags.length > 0 ? generateTagsHtml(test.tags) : ''
|
|
1081
|
+
const retries = config.showRetries && test.retryAttempts > 0 ? generateTestRetryHtml(test.retryAttempts, test.state) : ''
|
|
1082
|
+
const notes = test.notes && test.notes.length > 0 ? generateNotesHtml(test.notes) : ''
|
|
1083
|
+
|
|
1084
|
+
// Worker badge - show worker index if test has worker info
|
|
1085
|
+
const workerBadge = test.workerIndex !== undefined ? `<span class="worker-badge worker-${test.workerIndex}">Worker ${test.workerIndex}</span>` : ''
|
|
1086
|
+
|
|
1087
|
+
return `
|
|
1088
|
+
<div class="test-item ${statusClass}${test.isBdd ? ' bdd-test' : ''}" id="test-${test.id}" data-feature="${escapeHtml(feature)}" data-status="${statusClass}" data-tags="${(test.tags || []).join(',')}" data-retries="${test.retryAttempts || 0}" data-type="${test.isBdd ? 'bdd' : 'regular'}">
|
|
1089
|
+
<div class="test-header" onclick="toggleTestDetails('test-${test.id}')">
|
|
1090
|
+
<span class="test-status ${statusClass}">●</span>
|
|
1091
|
+
<div class="test-info">
|
|
1092
|
+
<h3 class="test-title">${test.isBdd ? `Scenario: ${test.title}` : test.title}</h3>
|
|
1093
|
+
<div class="test-meta-line">
|
|
1094
|
+
${workerBadge}
|
|
1095
|
+
${test.uid ? `<span class="test-uid">${test.uid}</span>` : ''}
|
|
1096
|
+
<span class="test-duration">${formatDuration(test.duration)}</span>
|
|
1097
|
+
${test.retryAttempts > 0 ? `<span class="retry-badge">${test.retryAttempts} retries</span>` : ''}
|
|
1098
|
+
${test.isBdd ? '<span class="bdd-badge">Gherkin</span>' : ''}
|
|
1099
|
+
</div>
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
<div class="test-details" id="details-test-${test.id}">
|
|
1103
|
+
${test.err ? `<div class="error-message"><pre>${escapeHtml(getErrorMessage(test))}</pre></div>` : ''}
|
|
1104
|
+
${featureDetails}
|
|
1105
|
+
${tags}
|
|
1106
|
+
${metadata}
|
|
1107
|
+
${retries}
|
|
1108
|
+
${notes}
|
|
1109
|
+
${hooks}
|
|
1110
|
+
${steps}
|
|
1111
|
+
${artifacts}
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
`
|
|
1115
|
+
})
|
|
1116
|
+
.join('')}
|
|
1002
1117
|
</div>
|
|
1003
|
-
</
|
|
1004
|
-
|
|
1005
|
-
${test.err ? `<div class="error-message"><pre>${escapeHtml(getErrorMessage(test))}</pre></div>` : ''}
|
|
1006
|
-
${featureDetails}
|
|
1007
|
-
${tags}
|
|
1008
|
-
${metadata}
|
|
1009
|
-
${retries}
|
|
1010
|
-
${notes}
|
|
1011
|
-
${hooks}
|
|
1012
|
-
${steps}
|
|
1013
|
-
${artifacts}
|
|
1014
|
-
</div>
|
|
1015
|
-
</div>
|
|
1016
|
-
`
|
|
1118
|
+
</section>
|
|
1119
|
+
`
|
|
1017
1120
|
})
|
|
1018
1121
|
.join('')
|
|
1019
1122
|
}
|
|
@@ -1103,13 +1206,19 @@ module.exports = function (config) {
|
|
|
1103
1206
|
const statusClass = hook.status || 'unknown'
|
|
1104
1207
|
const hookType = hook.type || 'hook'
|
|
1105
1208
|
const hookTitle = hook.title || `${hookType} hook`
|
|
1209
|
+
const location = hook.location ? `<div class="hook-location">Location: ${escapeHtml(hook.location)}</div>` : ''
|
|
1210
|
+
const context = hook.context ? `<div class="hook-context">Test: ${escapeHtml(hook.context.testTitle || 'N/A')}, Suite: ${escapeHtml(hook.context.suiteTitle || 'N/A')}</div>` : ''
|
|
1106
1211
|
|
|
1107
1212
|
return `
|
|
1108
1213
|
<div class="hook-item ${statusClass}">
|
|
1109
1214
|
<span class="hook-status ${statusClass}">●</span>
|
|
1110
|
-
<
|
|
1111
|
-
|
|
1112
|
-
|
|
1215
|
+
<div class="hook-content">
|
|
1216
|
+
<span class="hook-title">${hookType}: ${hookTitle}</span>
|
|
1217
|
+
<span class="hook-duration">${formatDuration(hook.duration)}</span>
|
|
1218
|
+
${location}
|
|
1219
|
+
${context}
|
|
1220
|
+
${hook.error ? `<div class="hook-error">${escapeHtml(hook.error)}</div>` : ''}
|
|
1221
|
+
</div>
|
|
1113
1222
|
</div>
|
|
1114
1223
|
`
|
|
1115
1224
|
})
|
|
@@ -1169,12 +1278,21 @@ module.exports = function (config) {
|
|
|
1169
1278
|
`
|
|
1170
1279
|
}
|
|
1171
1280
|
|
|
1172
|
-
function generateTestRetryHtml(retryAttempts) {
|
|
1281
|
+
function generateTestRetryHtml(retryAttempts, testState) {
|
|
1282
|
+
// Enhanced retry history display showing whether test eventually passed or failed
|
|
1283
|
+
const statusBadge = testState === 'passed' ? '<span class="retry-status-badge passed">✓ Eventually Passed</span>' : '<span class="retry-status-badge failed">✗ Eventually Failed</span>'
|
|
1284
|
+
|
|
1173
1285
|
return `
|
|
1174
1286
|
<div class="retry-section">
|
|
1175
|
-
<h4>Retry
|
|
1287
|
+
<h4>Retry History:</h4>
|
|
1176
1288
|
<div class="retry-info">
|
|
1177
|
-
<
|
|
1289
|
+
<div class="retry-summary">
|
|
1290
|
+
<span class="retry-count">Total retry attempts: <strong>${retryAttempts}</strong></span>
|
|
1291
|
+
${statusBadge}
|
|
1292
|
+
</div>
|
|
1293
|
+
<div class="retry-description">
|
|
1294
|
+
This test was retried <strong>${retryAttempts}</strong> time${retryAttempts > 1 ? 's' : ''} before ${testState === 'passed' ? 'passing' : 'failing'}.
|
|
1295
|
+
</div>
|
|
1178
1296
|
</div>
|
|
1179
1297
|
</div>
|
|
1180
1298
|
`
|
|
@@ -1420,6 +1538,30 @@ module.exports = function (config) {
|
|
|
1420
1538
|
|
|
1421
1539
|
function escapeHtml(text) {
|
|
1422
1540
|
if (!text) return ''
|
|
1541
|
+
// Convert non-string values to strings before escaping
|
|
1542
|
+
if (typeof text !== 'string') {
|
|
1543
|
+
// Handle arrays by recursively flattening and joining with commas
|
|
1544
|
+
if (Array.isArray(text)) {
|
|
1545
|
+
// Recursive helper to flatten deeply nested arrays with depth limit to prevent stack overflow
|
|
1546
|
+
const flattenArray = (arr, depth = 0, maxDepth = 100) => {
|
|
1547
|
+
if (depth >= maxDepth) {
|
|
1548
|
+
// Safety limit reached, return string representation
|
|
1549
|
+
return String(arr)
|
|
1550
|
+
}
|
|
1551
|
+
return arr
|
|
1552
|
+
.map(item => {
|
|
1553
|
+
if (Array.isArray(item)) {
|
|
1554
|
+
return flattenArray(item, depth + 1, maxDepth)
|
|
1555
|
+
}
|
|
1556
|
+
return String(item)
|
|
1557
|
+
})
|
|
1558
|
+
.join(', ')
|
|
1559
|
+
}
|
|
1560
|
+
text = flattenArray(text)
|
|
1561
|
+
} else {
|
|
1562
|
+
text = String(text)
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1423
1565
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
1424
1566
|
}
|
|
1425
1567
|
|
|
@@ -1535,8 +1677,12 @@ module.exports = function (config) {
|
|
|
1535
1677
|
if (!systemInfo) return ''
|
|
1536
1678
|
|
|
1537
1679
|
const formatInfo = (key, value) => {
|
|
1680
|
+
// Handle array values (e.g., ['Node', '22.14.0', 'path'])
|
|
1538
1681
|
if (Array.isArray(value) && value.length > 1) {
|
|
1539
|
-
|
|
1682
|
+
// value[1] might be an array itself (e.g., edgeInfo: ['Edge', ['Chromium (140.0.3485.54)'], 'N/A'])
|
|
1683
|
+
// escapeHtml now handles this, but we can also flatten it here for clarity
|
|
1684
|
+
const displayValue = value[1]
|
|
1685
|
+
return `<div class="info-item"><span class="info-key">${key}:</span> <span class="info-value">${escapeHtml(displayValue)}</span></div>`
|
|
1540
1686
|
} else if (typeof value === 'string' && value !== 'N/A' && value !== 'undefined') {
|
|
1541
1687
|
return `<div class="info-item"><span class="info-key">${key}:</span> <span class="info-value">${escapeHtml(value)}</span></div>`
|
|
1542
1688
|
}
|
|
@@ -1578,30 +1724,46 @@ module.exports = function (config) {
|
|
|
1578
1724
|
<!DOCTYPE html>
|
|
1579
1725
|
<html lang="en">
|
|
1580
1726
|
<head>
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1727
|
+
<meta charset="UTF-8">
|
|
1728
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1729
|
+
<title>{{title}}</title>
|
|
1730
|
+
<style>{{cssStyles}}</style>
|
|
1585
1731
|
</head>
|
|
1586
1732
|
<body>
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1733
|
+
<header class="report-header">
|
|
1734
|
+
<h1>{{title}}</h1>
|
|
1735
|
+
<div class="report-meta">
|
|
1736
|
+
<span>Generated: {{timestamp}}</span>
|
|
1737
|
+
<span>Duration: {{duration}}</span>
|
|
1738
|
+
</div>
|
|
1739
|
+
</header>
|
|
1594
1740
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1741
|
+
<main class="report-content">
|
|
1742
|
+
{{systemInfoHtml}}
|
|
1597
1743
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1744
|
+
<section class="stats-section">
|
|
1745
|
+
<h2>Test Statistics</h2>
|
|
1746
|
+
{{statsHtml}}
|
|
1747
|
+
</section>
|
|
1748
|
+
|
|
1749
|
+
<section class="test-performance-section">
|
|
1750
|
+
<h2>Test Performance Analysis</h2>
|
|
1751
|
+
<div class="performance-container">
|
|
1752
|
+
<div class="performance-group">
|
|
1753
|
+
<h3>⏱️ Longest Running Tests</h3>
|
|
1754
|
+
<div id="longestTests" class="performance-list"></div>
|
|
1755
|
+
</div>
|
|
1756
|
+
<div class="performance-group">
|
|
1757
|
+
<h3>⚡ Fastest Tests</h3>
|
|
1758
|
+
<div id="fastestTests" class="performance-list"></div>
|
|
1759
|
+
</div>
|
|
1760
|
+
</div>
|
|
1601
1761
|
</section>
|
|
1602
1762
|
|
|
1603
1763
|
<section class="history-section" style="display: {{showHistory}};">
|
|
1604
|
-
<h2>Test History</h2>
|
|
1764
|
+
<h2>Test Execution History</h2>
|
|
1765
|
+
<div class="history-stats" id="historyStats"></div>
|
|
1766
|
+
<div class="history-timeline" id="historyTimeline"></div>
|
|
1605
1767
|
<div class="history-chart-container">
|
|
1606
1768
|
<canvas id="historyChart" width="1600" height="600"></canvas>
|
|
1607
1769
|
</div>
|
|
@@ -1654,10 +1816,10 @@ module.exports = function (config) {
|
|
|
1654
1816
|
</div>
|
|
1655
1817
|
</section>
|
|
1656
1818
|
|
|
1657
|
-
<section class="retries-section" style="display:
|
|
1658
|
-
<h2>Test Retries</h2>
|
|
1819
|
+
<section class="retries-section" style="display: none;">
|
|
1820
|
+
<h2>Test Retries (Moved to Test Details)</h2>
|
|
1659
1821
|
<div class="retries-container">
|
|
1660
|
-
|
|
1822
|
+
<p>Retry information is now shown in each test's details section.</p>
|
|
1661
1823
|
</div>
|
|
1662
1824
|
</section>
|
|
1663
1825
|
|
|
@@ -1722,7 +1884,7 @@ body {
|
|
|
1722
1884
|
padding: 0 1rem;
|
|
1723
1885
|
}
|
|
1724
1886
|
|
|
1725
|
-
.stats-section, .tests-section, .retries-section, .filters-section, .history-section, .system-info-section {
|
|
1887
|
+
.stats-section, .tests-section, .retries-section, .filters-section, .history-section, .system-info-section, .test-performance-section {
|
|
1726
1888
|
background: white;
|
|
1727
1889
|
margin-bottom: 2rem;
|
|
1728
1890
|
border-radius: 8px;
|
|
@@ -1730,7 +1892,7 @@ body {
|
|
|
1730
1892
|
overflow: hidden;
|
|
1731
1893
|
}
|
|
1732
1894
|
|
|
1733
|
-
.stats-section h2, .tests-section h2, .retries-section h2, .filters-section h2, .history-section h2 {
|
|
1895
|
+
.stats-section h2, .tests-section h2, .retries-section h2, .filters-section h2, .history-section h2, .test-performance-section h2 {
|
|
1734
1896
|
background: #34495e;
|
|
1735
1897
|
color: white;
|
|
1736
1898
|
padding: 1rem;
|
|
@@ -1757,6 +1919,23 @@ body {
|
|
|
1757
1919
|
.stat-card.passed { background: #27ae60; }
|
|
1758
1920
|
.stat-card.failed { background: #e74c3c; }
|
|
1759
1921
|
.stat-card.pending { background: #f39c12; }
|
|
1922
|
+
.stat-card.flaky { background: #e67e22; }
|
|
1923
|
+
.stat-card.artifacts { background: #9b59b6; }
|
|
1924
|
+
|
|
1925
|
+
.metrics-summary {
|
|
1926
|
+
display: flex;
|
|
1927
|
+
justify-content: center;
|
|
1928
|
+
gap: 2rem;
|
|
1929
|
+
padding: 1rem;
|
|
1930
|
+
background: #f8f9fa;
|
|
1931
|
+
border-radius: 6px;
|
|
1932
|
+
margin: 1rem 0;
|
|
1933
|
+
font-size: 1rem;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
.metrics-summary span {
|
|
1937
|
+
color: #34495e;
|
|
1938
|
+
}
|
|
1760
1939
|
|
|
1761
1940
|
.stat-card h3 {
|
|
1762
1941
|
font-size: 0.9rem;
|
|
@@ -1784,6 +1963,54 @@ body {
|
|
|
1784
1963
|
height: auto;
|
|
1785
1964
|
}
|
|
1786
1965
|
|
|
1966
|
+
.feature-group {
|
|
1967
|
+
margin-bottom: 2.5rem;
|
|
1968
|
+
border: 2px solid #3498db;
|
|
1969
|
+
border-radius: 12px;
|
|
1970
|
+
overflow: hidden;
|
|
1971
|
+
background: white;
|
|
1972
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
.feature-group-title {
|
|
1976
|
+
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
|
1977
|
+
color: white;
|
|
1978
|
+
padding: 1.2rem 1.5rem;
|
|
1979
|
+
margin: 0;
|
|
1980
|
+
font-size: 1.4rem;
|
|
1981
|
+
font-weight: 600;
|
|
1982
|
+
display: flex;
|
|
1983
|
+
align-items: center;
|
|
1984
|
+
justify-content: space-between;
|
|
1985
|
+
cursor: pointer;
|
|
1986
|
+
transition: all 0.3s ease;
|
|
1987
|
+
user-select: none;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
.feature-group-title:hover {
|
|
1991
|
+
background: linear-gradient(135deg, #2980b9 0%, #21618c 100%);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
.feature-group-title .toggle-icon {
|
|
1995
|
+
font-size: 1.2rem;
|
|
1996
|
+
transition: transform 0.3s ease;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
.feature-group-title .toggle-icon.rotated {
|
|
2000
|
+
transform: rotate(180deg);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
.feature-tests {
|
|
2004
|
+
padding: 0;
|
|
2005
|
+
transition: max-height 0.3s ease, opacity 0.3s ease;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
.feature-tests.collapsed {
|
|
2009
|
+
max-height: 0;
|
|
2010
|
+
opacity: 0;
|
|
2011
|
+
overflow: hidden;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1787
2014
|
.test-item {
|
|
1788
2015
|
border-bottom: 1px solid #eee;
|
|
1789
2016
|
margin: 0;
|
|
@@ -1861,9 +2088,64 @@ body {
|
|
|
1861
2088
|
font-weight: bold;
|
|
1862
2089
|
}
|
|
1863
2090
|
|
|
2091
|
+
.worker-badge {
|
|
2092
|
+
background: #16a085;
|
|
2093
|
+
color: white;
|
|
2094
|
+
padding: 0.25rem 0.5rem;
|
|
2095
|
+
border-radius: 4px;
|
|
2096
|
+
font-size: 0.7rem;
|
|
2097
|
+
font-weight: bold;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/* Different colors for each worker index */
|
|
2101
|
+
.worker-badge.worker-0 {
|
|
2102
|
+
background: #3498db; /* Blue */
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
.worker-badge.worker-1 {
|
|
2106
|
+
background: #e74c3c; /* Red */
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
.worker-badge.worker-2 {
|
|
2110
|
+
background: #2ecc71; /* Green */
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
.worker-badge.worker-3 {
|
|
2114
|
+
background: #f39c12; /* Orange */
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
.worker-badge.worker-4 {
|
|
2118
|
+
background: #9b59b6; /* Purple */
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
.worker-badge.worker-5 {
|
|
2122
|
+
background: #1abc9c; /* Turquoise */
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
.worker-badge.worker-6 {
|
|
2126
|
+
background: #e67e22; /* Carrot */
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
.worker-badge.worker-7 {
|
|
2130
|
+
background: #34495e; /* Dark Blue-Gray */
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
.worker-badge.worker-8 {
|
|
2134
|
+
background: #16a085; /* Teal */
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
.worker-badge.worker-9 {
|
|
2138
|
+
background: #c0392b; /* Dark Red */
|
|
2139
|
+
}
|
|
2140
|
+
|
|
1864
2141
|
.test-duration {
|
|
1865
|
-
font-size: 0.
|
|
1866
|
-
|
|
2142
|
+
font-size: 0.85rem;
|
|
2143
|
+
font-weight: 600;
|
|
2144
|
+
color: #2c3e50;
|
|
2145
|
+
background: #ecf0f1;
|
|
2146
|
+
padding: 0.25rem 0.5rem;
|
|
2147
|
+
border-radius: 4px;
|
|
2148
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
1867
2149
|
}
|
|
1868
2150
|
|
|
1869
2151
|
.test-details {
|
|
@@ -1901,27 +2183,39 @@ body {
|
|
|
1901
2183
|
|
|
1902
2184
|
.hook-item {
|
|
1903
2185
|
display: flex;
|
|
1904
|
-
align-items:
|
|
1905
|
-
padding: 0.
|
|
1906
|
-
border
|
|
2186
|
+
align-items: flex-start;
|
|
2187
|
+
padding: 0.75rem;
|
|
2188
|
+
border: 1px solid #ecf0f1;
|
|
2189
|
+
border-radius: 4px;
|
|
2190
|
+
margin-bottom: 0.5rem;
|
|
2191
|
+
background: #fafafa;
|
|
1907
2192
|
}
|
|
1908
2193
|
|
|
1909
2194
|
.hook-item:last-child {
|
|
1910
|
-
|
|
2195
|
+
margin-bottom: 0;
|
|
1911
2196
|
}
|
|
1912
2197
|
|
|
1913
2198
|
.hook-status {
|
|
1914
|
-
margin-right: 0.
|
|
2199
|
+
margin-right: 0.75rem;
|
|
2200
|
+
flex-shrink: 0;
|
|
2201
|
+
margin-top: 0.2rem;
|
|
1915
2202
|
}
|
|
1916
2203
|
|
|
1917
2204
|
.hook-status.passed { color: #27ae60; }
|
|
1918
2205
|
.hook-status.failed { color: #e74c3c; }
|
|
1919
2206
|
|
|
1920
|
-
.hook-
|
|
2207
|
+
.hook-content {
|
|
1921
2208
|
flex: 1;
|
|
2209
|
+
display: flex;
|
|
2210
|
+
flex-direction: column;
|
|
2211
|
+
gap: 0.25rem;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
.hook-title {
|
|
1922
2215
|
font-family: 'Courier New', monospace;
|
|
1923
2216
|
font-size: 0.9rem;
|
|
1924
2217
|
font-weight: bold;
|
|
2218
|
+
color: #2c3e50;
|
|
1925
2219
|
}
|
|
1926
2220
|
|
|
1927
2221
|
.hook-duration {
|
|
@@ -1929,8 +2223,13 @@ body {
|
|
|
1929
2223
|
color: #7f8c8d;
|
|
1930
2224
|
}
|
|
1931
2225
|
|
|
2226
|
+
.hook-location, .hook-context {
|
|
2227
|
+
font-size: 0.8rem;
|
|
2228
|
+
color: #6c757d;
|
|
2229
|
+
font-style: italic;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
1932
2232
|
.hook-error {
|
|
1933
|
-
width: 100%;
|
|
1934
2233
|
margin-top: 0.5rem;
|
|
1935
2234
|
padding: 0.5rem;
|
|
1936
2235
|
background: #fee;
|
|
@@ -2219,11 +2518,22 @@ body {
|
|
|
2219
2518
|
}
|
|
2220
2519
|
|
|
2221
2520
|
/* Retry Information */
|
|
2521
|
+
.retry-section {
|
|
2522
|
+
margin-top: 1rem;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2222
2525
|
.retry-info {
|
|
2223
|
-
padding:
|
|
2224
|
-
background: #
|
|
2526
|
+
padding: 1rem;
|
|
2527
|
+
background: #fff9e6;
|
|
2225
2528
|
border-radius: 4px;
|
|
2226
|
-
border-left:
|
|
2529
|
+
border-left: 4px solid #f39c12;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
.retry-summary {
|
|
2533
|
+
display: flex;
|
|
2534
|
+
align-items: center;
|
|
2535
|
+
gap: 1rem;
|
|
2536
|
+
margin-bottom: 0.5rem;
|
|
2227
2537
|
}
|
|
2228
2538
|
|
|
2229
2539
|
.retry-count {
|
|
@@ -2231,6 +2541,29 @@ body {
|
|
|
2231
2541
|
font-weight: 500;
|
|
2232
2542
|
}
|
|
2233
2543
|
|
|
2544
|
+
.retry-status-badge {
|
|
2545
|
+
padding: 0.25rem 0.75rem;
|
|
2546
|
+
border-radius: 4px;
|
|
2547
|
+
font-size: 0.85rem;
|
|
2548
|
+
font-weight: bold;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
.retry-status-badge.passed {
|
|
2552
|
+
background: #27ae60;
|
|
2553
|
+
color: white;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
.retry-status-badge.failed {
|
|
2557
|
+
background: #e74c3c;
|
|
2558
|
+
color: white;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
.retry-description {
|
|
2562
|
+
font-size: 0.9rem;
|
|
2563
|
+
color: #6c757d;
|
|
2564
|
+
font-style: italic;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2234
2567
|
/* Retries Section */
|
|
2235
2568
|
.retry-item {
|
|
2236
2569
|
padding: 1rem;
|
|
@@ -2276,6 +2609,92 @@ body {
|
|
|
2276
2609
|
}
|
|
2277
2610
|
|
|
2278
2611
|
/* History Chart */
|
|
2612
|
+
.history-stats {
|
|
2613
|
+
padding: 1.5rem;
|
|
2614
|
+
background: #f8f9fa;
|
|
2615
|
+
border-bottom: 1px solid #e9ecef;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
.history-stats-grid {
|
|
2619
|
+
display: grid;
|
|
2620
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
2621
|
+
gap: 1rem;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
.history-stat-item {
|
|
2625
|
+
background: white;
|
|
2626
|
+
padding: 1rem;
|
|
2627
|
+
border-radius: 6px;
|
|
2628
|
+
border-left: 4px solid #3498db;
|
|
2629
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
.history-stat-item h4 {
|
|
2633
|
+
margin: 0 0 0.5rem 0;
|
|
2634
|
+
font-size: 0.9rem;
|
|
2635
|
+
color: #7f8c8d;
|
|
2636
|
+
text-transform: uppercase;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
.history-stat-item .value {
|
|
2640
|
+
font-size: 1.5rem;
|
|
2641
|
+
font-weight: bold;
|
|
2642
|
+
color: #2c3e50;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
.history-timeline {
|
|
2646
|
+
padding: 1.5rem;
|
|
2647
|
+
background: white;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
.timeline-item {
|
|
2651
|
+
display: flex;
|
|
2652
|
+
align-items: center;
|
|
2653
|
+
padding: 0.75rem;
|
|
2654
|
+
border-left: 3px solid #3498db;
|
|
2655
|
+
margin-left: 1rem;
|
|
2656
|
+
margin-bottom: 0.5rem;
|
|
2657
|
+
background: #f8f9fa;
|
|
2658
|
+
border-radius: 0 6px 6px 0;
|
|
2659
|
+
transition: all 0.2s;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
.timeline-item:hover {
|
|
2663
|
+
background: #e9ecef;
|
|
2664
|
+
transform: translateX(4px);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
.timeline-time {
|
|
2668
|
+
min-width: 150px;
|
|
2669
|
+
font-weight: 600;
|
|
2670
|
+
color: #2c3e50;
|
|
2671
|
+
font-family: 'Courier New', monospace;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
.timeline-result {
|
|
2675
|
+
flex: 1;
|
|
2676
|
+
display: flex;
|
|
2677
|
+
gap: 1rem;
|
|
2678
|
+
align-items: center;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
.timeline-badge {
|
|
2682
|
+
padding: 0.25rem 0.5rem;
|
|
2683
|
+
border-radius: 4px;
|
|
2684
|
+
font-size: 0.85rem;
|
|
2685
|
+
font-weight: 600;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
.timeline-badge.success {
|
|
2689
|
+
background: #d4edda;
|
|
2690
|
+
color: #155724;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
.timeline-badge.failure {
|
|
2694
|
+
background: #f8d7da;
|
|
2695
|
+
color: #721c24;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2279
2698
|
.history-chart-container {
|
|
2280
2699
|
padding: 2rem 1rem;
|
|
2281
2700
|
display: flex;
|
|
@@ -2287,6 +2706,87 @@ body {
|
|
|
2287
2706
|
height: auto;
|
|
2288
2707
|
}
|
|
2289
2708
|
|
|
2709
|
+
/* Test Performance Section */
|
|
2710
|
+
.performance-container {
|
|
2711
|
+
display: grid;
|
|
2712
|
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
2713
|
+
gap: 2rem;
|
|
2714
|
+
padding: 1.5rem;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
.performance-group h3 {
|
|
2718
|
+
margin: 0 0 1rem 0;
|
|
2719
|
+
color: #2c3e50;
|
|
2720
|
+
font-size: 1.1rem;
|
|
2721
|
+
padding-bottom: 0.5rem;
|
|
2722
|
+
border-bottom: 2px solid #3498db;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
.performance-list {
|
|
2726
|
+
display: flex;
|
|
2727
|
+
flex-direction: column;
|
|
2728
|
+
gap: 0.75rem;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
.performance-item {
|
|
2732
|
+
display: flex;
|
|
2733
|
+
align-items: center;
|
|
2734
|
+
justify-content: space-between;
|
|
2735
|
+
padding: 0.75rem 1rem;
|
|
2736
|
+
background: #f8f9fa;
|
|
2737
|
+
border-radius: 6px;
|
|
2738
|
+
border-left: 4px solid #3498db;
|
|
2739
|
+
transition: all 0.2s;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
.performance-item:hover {
|
|
2743
|
+
background: #e9ecef;
|
|
2744
|
+
transform: translateX(4px);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
.performance-item:nth-child(1) .performance-rank {
|
|
2748
|
+
background: #f39c12;
|
|
2749
|
+
color: white;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
.performance-item:nth-child(2) .performance-rank {
|
|
2753
|
+
background: #95a5a6;
|
|
2754
|
+
color: white;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
.performance-item:nth-child(3) .performance-rank {
|
|
2758
|
+
background: #cd7f32;
|
|
2759
|
+
color: white;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
.performance-rank {
|
|
2763
|
+
display: flex;
|
|
2764
|
+
align-items: center;
|
|
2765
|
+
justify-content: center;
|
|
2766
|
+
width: 28px;
|
|
2767
|
+
height: 28px;
|
|
2768
|
+
background: #3498db;
|
|
2769
|
+
color: white;
|
|
2770
|
+
border-radius: 50%;
|
|
2771
|
+
font-weight: bold;
|
|
2772
|
+
font-size: 0.9rem;
|
|
2773
|
+
margin-right: 1rem;
|
|
2774
|
+
flex-shrink: 0;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
.performance-name {
|
|
2778
|
+
flex: 1;
|
|
2779
|
+
font-weight: 500;
|
|
2780
|
+
color: #2c3e50;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
.performance-duration {
|
|
2784
|
+
font-weight: 600;
|
|
2785
|
+
color: #7f8c8d;
|
|
2786
|
+
font-family: 'Courier New', monospace;
|
|
2787
|
+
font-size: 0.9rem;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2290
2790
|
/* Hidden items for filtering */
|
|
2291
2791
|
.test-item.filtered-out {
|
|
2292
2792
|
display: none !important;
|
|
@@ -2508,6 +3008,21 @@ body {
|
|
|
2508
3008
|
function scrollToTop() {
|
|
2509
3009
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
2510
3010
|
}
|
|
3011
|
+
|
|
3012
|
+
function toggleFeatureGroup(featureId) {
|
|
3013
|
+
const featureTests = document.getElementById('feature-' + featureId);
|
|
3014
|
+
const titleElement = featureTests.previousElementSibling;
|
|
3015
|
+
const icon = titleElement.querySelector('.toggle-icon');
|
|
3016
|
+
|
|
3017
|
+
if (featureTests.classList.contains('collapsed')) {
|
|
3018
|
+
featureTests.classList.remove('collapsed');
|
|
3019
|
+
icon.classList.remove('rotated');
|
|
3020
|
+
} else {
|
|
3021
|
+
featureTests.classList.add('collapsed');
|
|
3022
|
+
icon.classList.add('rotated');
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2511
3026
|
function toggleTestDetails(testId) {
|
|
2512
3027
|
const details = document.getElementById('details-' + testId);
|
|
2513
3028
|
if (details.style.display === 'none' || details.style.display === '') {
|
|
@@ -2993,6 +3508,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
2993
3508
|
// Draw charts
|
|
2994
3509
|
drawPieChart();
|
|
2995
3510
|
drawHistoryChart();
|
|
3511
|
+
renderTestPerformance();
|
|
3512
|
+
renderHistoryTimeline();
|
|
3513
|
+
|
|
2996
3514
|
// Add Go to Top button
|
|
2997
3515
|
const goTopBtn = document.createElement('button');
|
|
2998
3516
|
goTopBtn.innerText = '↑ Top';
|
|
@@ -3018,6 +3536,141 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
3018
3536
|
document.getElementById('retryFilter').addEventListener('change', applyFilters);
|
|
3019
3537
|
document.getElementById('typeFilter').addEventListener('change', applyFilters);
|
|
3020
3538
|
});
|
|
3539
|
+
|
|
3540
|
+
// Render test performance analysis
|
|
3541
|
+
function renderTestPerformance() {
|
|
3542
|
+
const tests = Array.from(document.querySelectorAll('.test-item'));
|
|
3543
|
+
const testsWithDuration = tests.map(testEl => {
|
|
3544
|
+
const title = testEl.querySelector('.test-title')?.textContent || 'Unknown';
|
|
3545
|
+
const durationText = testEl.querySelector('.test-duration')?.textContent || '0ms';
|
|
3546
|
+
const durationMs = parseDuration(durationText);
|
|
3547
|
+
const status = testEl.dataset.status;
|
|
3548
|
+
return { title, duration: durationMs, durationText, status };
|
|
3549
|
+
}); // Don't filter out 0ms tests
|
|
3550
|
+
|
|
3551
|
+
// Sort by duration
|
|
3552
|
+
const longest = [...testsWithDuration].sort((a, b) => b.duration - a.duration).slice(0, 5);
|
|
3553
|
+
const fastest = [...testsWithDuration].sort((a, b) => a.duration - b.duration).slice(0, 5);
|
|
3554
|
+
|
|
3555
|
+
// Render longest tests
|
|
3556
|
+
const longestContainer = document.getElementById('longestTests');
|
|
3557
|
+
if (longestContainer && longest.length > 0) {
|
|
3558
|
+
longestContainer.innerHTML = longest.map((test, index) => \`
|
|
3559
|
+
<div class="performance-item">
|
|
3560
|
+
<span class="performance-rank">\${index + 1}</span>
|
|
3561
|
+
<span class="performance-name" title="\${test.title}">\${test.title.length > 60 ? test.title.substring(0, 60) + '...' : test.title}</span>
|
|
3562
|
+
<span class="performance-duration">\${test.durationText}</span>
|
|
3563
|
+
</div>
|
|
3564
|
+
\`).join('');
|
|
3565
|
+
} else if (longestContainer) {
|
|
3566
|
+
longestContainer.innerHTML = '<p style="color: #7f8c8d; padding: 1rem;">No test data available</p>';
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
// Render fastest tests
|
|
3570
|
+
const fastestContainer = document.getElementById('fastestTests');
|
|
3571
|
+
if (fastestContainer && fastest.length > 0) {
|
|
3572
|
+
fastestContainer.innerHTML = fastest.map((test, index) => \`
|
|
3573
|
+
<div class="performance-item">
|
|
3574
|
+
<span class="performance-rank">\${index + 1}</span>
|
|
3575
|
+
<span class="performance-name" title="\${test.title}">\${test.title.length > 60 ? test.title.substring(0, 60) + '...' : test.title}</span>
|
|
3576
|
+
<span class="performance-duration">\${test.durationText}</span>
|
|
3577
|
+
</div>
|
|
3578
|
+
\`).join('');
|
|
3579
|
+
} else if (fastestContainer) {
|
|
3580
|
+
fastestContainer.innerHTML = '<p style="color: #7f8c8d; padding: 1rem;">No test data available</p>';
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// Render history timeline
|
|
3585
|
+
function renderHistoryTimeline() {
|
|
3586
|
+
if (!window.testData || !window.testData.history || window.testData.history.length === 0) {
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
const history = window.testData.history.slice().reverse(); // Most recent last
|
|
3591
|
+
|
|
3592
|
+
// Render stats
|
|
3593
|
+
const statsContainer = document.getElementById('historyStats');
|
|
3594
|
+
if (statsContainer) {
|
|
3595
|
+
const totalRuns = history.length;
|
|
3596
|
+
const avgDuration = history.reduce((sum, run) => sum + (run.duration || 0), 0) / totalRuns;
|
|
3597
|
+
const avgTests = Math.round(history.reduce((sum, run) => sum + (run.stats.tests || 0), 0) / totalRuns);
|
|
3598
|
+
const avgPassRate = history.reduce((sum, run) => {
|
|
3599
|
+
const total = run.stats.tests || 0;
|
|
3600
|
+
const passed = run.stats.passes || 0;
|
|
3601
|
+
return sum + (total > 0 ? (passed / total) * 100 : 0);
|
|
3602
|
+
}, 0) / totalRuns;
|
|
3603
|
+
|
|
3604
|
+
statsContainer.innerHTML = \`
|
|
3605
|
+
<div class="history-stats-grid">
|
|
3606
|
+
<div class="history-stat-item">
|
|
3607
|
+
<h4>Total Runs</h4>
|
|
3608
|
+
<div class="value">\${totalRuns}</div>
|
|
3609
|
+
</div>
|
|
3610
|
+
<div class="history-stat-item">
|
|
3611
|
+
<h4>Avg Duration</h4>
|
|
3612
|
+
<div class="value">\${formatDuration(avgDuration)}</div>
|
|
3613
|
+
</div>
|
|
3614
|
+
<div class="history-stat-item">
|
|
3615
|
+
<h4>Avg Tests</h4>
|
|
3616
|
+
<div class="value">\${avgTests}</div>
|
|
3617
|
+
</div>
|
|
3618
|
+
<div class="history-stat-item">
|
|
3619
|
+
<h4>Avg Pass Rate</h4>
|
|
3620
|
+
<div class="value">\${avgPassRate.toFixed(1)}%</div>
|
|
3621
|
+
</div>
|
|
3622
|
+
</div>
|
|
3623
|
+
\`;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// Render timeline
|
|
3627
|
+
const timelineContainer = document.getElementById('historyTimeline');
|
|
3628
|
+
if (timelineContainer) {
|
|
3629
|
+
const recentHistory = history.slice(-10).reverse(); // Last 10 runs, most recent first
|
|
3630
|
+
timelineContainer.innerHTML = '<h3 style="margin: 0 0 1rem 0; color: #2c3e50;">Recent Execution Timeline</h3>' +
|
|
3631
|
+
recentHistory.map(run => {
|
|
3632
|
+
const timestamp = new Date(run.timestamp);
|
|
3633
|
+
const timeStr = timestamp.toLocaleString();
|
|
3634
|
+
const total = run.stats.tests || 0;
|
|
3635
|
+
const passed = run.stats.passes || 0;
|
|
3636
|
+
const failed = run.stats.failures || 0;
|
|
3637
|
+
const badgeClass = failed > 0 ? 'failure' : 'success';
|
|
3638
|
+
const badgeText = failed > 0 ? \`\${failed} Failed\` : \`All Passed\`;
|
|
3639
|
+
|
|
3640
|
+
return \`
|
|
3641
|
+
<div class="timeline-item">
|
|
3642
|
+
<div class="timeline-time">\${timeStr}</div>
|
|
3643
|
+
<div class="timeline-result">
|
|
3644
|
+
<span class="timeline-badge \${badgeClass}">\${badgeText}</span>
|
|
3645
|
+
<span>\${passed}/\${total} passed</span>
|
|
3646
|
+
<span>·</span>
|
|
3647
|
+
<span>\${formatDuration(run.duration || 0)}</span>
|
|
3648
|
+
</div>
|
|
3649
|
+
</div>
|
|
3650
|
+
\`;
|
|
3651
|
+
}).join('');
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
// Helper to parse duration text to milliseconds
|
|
3656
|
+
function parseDuration(durationText) {
|
|
3657
|
+
if (!durationText) return 0;
|
|
3658
|
+
const match = durationText.match(/(\\d+(?:\\.\\d+)?)(ms|s|m)/);
|
|
3659
|
+
if (!match) return 0;
|
|
3660
|
+
const value = parseFloat(match[1]);
|
|
3661
|
+
const unit = match[2];
|
|
3662
|
+
if (unit === 'ms') return value;
|
|
3663
|
+
if (unit === 's') return value * 1000;
|
|
3664
|
+
if (unit === 'm') return value * 60000;
|
|
3665
|
+
return 0;
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// Helper to format duration
|
|
3669
|
+
function formatDuration(ms) {
|
|
3670
|
+
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
3671
|
+
if (ms < 60000) return (ms / 1000).toFixed(2) + 's';
|
|
3672
|
+
return (ms / 60000).toFixed(2) + 'm';
|
|
3673
|
+
}
|
|
3021
3674
|
`
|
|
3022
3675
|
}
|
|
3023
3676
|
}
|