codeceptjs 3.7.6-beta.3 → 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/JSONResponse.js +4 -4
- package/lib/helper/Playwright.js +5 -1
- package/lib/helper/WebDriver.js +3 -3
- package/lib/listener/globalTimeout.js +19 -4
- package/lib/listener/steps.js +2 -2
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/htmlReporter.js +855 -134
- 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 +21 -18
- package/typings/types.d.ts +14 -3
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// TypeScript: Import Node.js types for process, fs, path, etc.
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
|
|
1
5
|
const fs = require('fs')
|
|
2
6
|
const path = require('path')
|
|
3
7
|
const mkdirp = require('mkdirp')
|
|
@@ -11,7 +15,7 @@ const output = require('../output')
|
|
|
11
15
|
const Codecept = require('../codecept')
|
|
12
16
|
|
|
13
17
|
const defaultConfig = {
|
|
14
|
-
output: global.output_dir
|
|
18
|
+
output: typeof global !== 'undefined' && global.output_dir ? global.output_dir : './output',
|
|
15
19
|
reportFileName: 'report.html',
|
|
16
20
|
includeArtifacts: true,
|
|
17
21
|
showSteps: true,
|
|
@@ -60,6 +64,9 @@ const defaultConfig = {
|
|
|
60
64
|
*/
|
|
61
65
|
module.exports = function (config) {
|
|
62
66
|
const options = { ...defaultConfig, ...config }
|
|
67
|
+
/**
|
|
68
|
+
* TypeScript: Explicitly type reportData arrays as any[] to avoid 'never' errors
|
|
69
|
+
*/
|
|
63
70
|
let reportData = {
|
|
64
71
|
stats: {},
|
|
65
72
|
tests: [],
|
|
@@ -82,8 +89,8 @@ module.exports = function (config) {
|
|
|
82
89
|
|
|
83
90
|
// Track overall test execution
|
|
84
91
|
event.dispatcher.on(event.all.before, () => {
|
|
85
|
-
reportData.startTime = new Date()
|
|
86
|
-
output.
|
|
92
|
+
reportData.startTime = new Date().toISOString()
|
|
93
|
+
output.print('HTML Reporter: Starting HTML report generation...')
|
|
87
94
|
})
|
|
88
95
|
|
|
89
96
|
// Track test start to initialize steps and hooks collection
|
|
@@ -138,10 +145,30 @@ module.exports = function (config) {
|
|
|
138
145
|
if (step.htmlReporterStartTime) {
|
|
139
146
|
step.duration = Date.now() - step.htmlReporterStartTime
|
|
140
147
|
}
|
|
148
|
+
|
|
149
|
+
// Serialize args immediately to preserve them through worker serialization
|
|
150
|
+
let serializedArgs = []
|
|
151
|
+
if (step.args && Array.isArray(step.args)) {
|
|
152
|
+
serializedArgs = step.args.map(arg => {
|
|
153
|
+
try {
|
|
154
|
+
// Try to convert to JSON-friendly format
|
|
155
|
+
if (typeof arg === 'string') return arg
|
|
156
|
+
if (typeof arg === 'number') return arg
|
|
157
|
+
if (typeof arg === 'boolean') return arg
|
|
158
|
+
if (arg === null || arg === undefined) return arg
|
|
159
|
+
// For objects, try to serialize them
|
|
160
|
+
return JSON.parse(JSON.stringify(arg))
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// If serialization fails, convert to string
|
|
163
|
+
return String(arg)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
141
168
|
currentTestSteps.push({
|
|
142
169
|
name: step.name,
|
|
143
170
|
actor: step.actor,
|
|
144
|
-
args:
|
|
171
|
+
args: serializedArgs,
|
|
145
172
|
status: step.failed ? 'failed' : 'success',
|
|
146
173
|
duration: step.duration || 0,
|
|
147
174
|
})
|
|
@@ -156,12 +183,21 @@ module.exports = function (config) {
|
|
|
156
183
|
if (hook.htmlReporterStartTime) {
|
|
157
184
|
hook.duration = Date.now() - hook.htmlReporterStartTime
|
|
158
185
|
}
|
|
186
|
+
// Enhanced hook info: include type, name, location, error, and context
|
|
159
187
|
const hookInfo = {
|
|
160
188
|
title: hook.title,
|
|
161
189
|
type: hook.type || 'unknown', // before, after, beforeSuite, afterSuite
|
|
162
190
|
status: hook.err ? 'failed' : 'passed',
|
|
163
191
|
duration: hook.duration || 0,
|
|
164
|
-
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,
|
|
165
201
|
}
|
|
166
202
|
currentTestHooks.push(hookInfo)
|
|
167
203
|
reportData.hooks.push(hookInfo)
|
|
@@ -185,6 +221,43 @@ module.exports = function (config) {
|
|
|
185
221
|
})
|
|
186
222
|
})
|
|
187
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
|
+
|
|
188
261
|
// Collect test results
|
|
189
262
|
event.dispatcher.on(event.test.finished, test => {
|
|
190
263
|
const testId = generateTestId(test)
|
|
@@ -211,19 +284,25 @@ module.exports = function (config) {
|
|
|
211
284
|
// Debug logging
|
|
212
285
|
output.debug(`HTML Reporter: Test finished - ${test.title}, State: ${test.state}, Retries: ${retryAttempts}`)
|
|
213
286
|
|
|
214
|
-
// Detect if this is a BDD/Gherkin test
|
|
215
|
-
const
|
|
287
|
+
// Detect if this is a BDD/Gherkin test - use test.parent directly instead of currentSuite
|
|
288
|
+
const suite = test.parent || test.suite || currentSuite
|
|
289
|
+
const isBddTest = isBddGherkinTest(test, suite)
|
|
216
290
|
const steps = isBddTest ? currentBddSteps : currentTestSteps
|
|
217
|
-
const featureInfo = isBddTest ? getBddFeatureInfo(test,
|
|
291
|
+
const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null
|
|
218
292
|
|
|
219
293
|
// Check if this test already exists in reportData.tests (from a previous retry)
|
|
220
294
|
const existingTestIndex = reportData.tests.findIndex(t => t.id === testId)
|
|
221
|
-
const hasFailedBefore = existingTestIndex >= 0 && reportData.tests[existingTestIndex].state === 'failed'
|
|
295
|
+
const hasFailedBefore = existingTestIndex >= 0 && reportData.tests[existingTestIndex] && reportData.tests[existingTestIndex].state === 'failed'
|
|
222
296
|
const currentlyFailed = test.state === 'failed'
|
|
223
297
|
|
|
224
298
|
// Debug artifacts collection (but don't process them yet - screenshots may not be ready)
|
|
225
299
|
output.debug(`HTML Reporter: Test ${test.title} artifacts at test.finished: ${JSON.stringify(test.artifacts)}`)
|
|
226
300
|
|
|
301
|
+
// Extract parent/suite title before serialization (for worker mode)
|
|
302
|
+
// This ensures the feature name is preserved when test data is JSON stringified
|
|
303
|
+
const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null
|
|
304
|
+
const suiteTitle = test.suite?.title || (suite && suite.title) || null
|
|
305
|
+
|
|
227
306
|
const testData = {
|
|
228
307
|
...test,
|
|
229
308
|
id: testId,
|
|
@@ -239,11 +318,14 @@ module.exports = function (config) {
|
|
|
239
318
|
uid: test.uid,
|
|
240
319
|
isBdd: isBddTest,
|
|
241
320
|
feature: featureInfo,
|
|
321
|
+
// Store parent/suite titles as simple strings for worker mode serialization
|
|
322
|
+
parentTitle: parentTitle,
|
|
323
|
+
suiteTitle: suiteTitle,
|
|
242
324
|
}
|
|
243
325
|
|
|
244
326
|
if (existingTestIndex >= 0) {
|
|
245
327
|
// Update existing test with final result (including failed state)
|
|
246
|
-
reportData.tests[existingTestIndex] = testData
|
|
328
|
+
if (existingTestIndex >= 0) reportData.tests[existingTestIndex] = testData
|
|
247
329
|
output.debug(`HTML Reporter: Updated existing test - ${test.title}, Final state: ${test.state}`)
|
|
248
330
|
} else {
|
|
249
331
|
// Add new test
|
|
@@ -288,14 +370,14 @@ module.exports = function (config) {
|
|
|
288
370
|
finalState: test.state,
|
|
289
371
|
duration: test.duration || 0,
|
|
290
372
|
})
|
|
291
|
-
|
|
373
|
+
output.debug(`HTML Reporter: Fallback retry detection for failed test ${test.title}, attempts: ${fallbackAttempts}`)
|
|
292
374
|
}
|
|
293
375
|
})
|
|
294
376
|
|
|
295
377
|
// Generate final report
|
|
296
|
-
event.dispatcher.on(event.all.result, result => {
|
|
297
|
-
reportData.endTime = new Date()
|
|
298
|
-
reportData.duration = reportData.endTime - reportData.startTime
|
|
378
|
+
event.dispatcher.on(event.all.result, async result => {
|
|
379
|
+
reportData.endTime = new Date().toISOString()
|
|
380
|
+
reportData.duration = new Date(reportData.endTime).getTime() - new Date(reportData.startTime).getTime()
|
|
299
381
|
|
|
300
382
|
// Process artifacts now that all async tasks (including screenshots) are complete
|
|
301
383
|
output.debug(`HTML Reporter: Processing artifacts for ${reportData.tests.length} tests after all async tasks complete`)
|
|
@@ -337,15 +419,25 @@ module.exports = function (config) {
|
|
|
337
419
|
// Calculate stats from our collected test data instead of using result.stats
|
|
338
420
|
const passedTests = reportData.tests.filter(t => t.state === 'passed').length
|
|
339
421
|
const failedTests = reportData.tests.filter(t => t.state === 'failed').length
|
|
340
|
-
|
|
341
|
-
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)
|
|
342
430
|
|
|
343
431
|
// Populate failures from our collected test data with enhanced details
|
|
344
432
|
reportData.failures = reportData.tests
|
|
345
433
|
.filter(t => t.state === 'failed')
|
|
346
434
|
.map(t => {
|
|
347
435
|
const testName = t.title || 'Unknown Test'
|
|
348
|
-
|
|
436
|
+
// Try to get feature name from BDD, preserved titles (worker mode), or direct access
|
|
437
|
+
let featureName = t.feature?.name || t.parentTitle || t.suiteTitle || t.parent?.title || t.suite?.title || 'Unknown Feature'
|
|
438
|
+
if (featureName === 'Unknown Feature' && t.suite && t.suite.feature && t.suite.feature.name) {
|
|
439
|
+
featureName = t.suite.feature.name
|
|
440
|
+
}
|
|
349
441
|
|
|
350
442
|
if (t.err) {
|
|
351
443
|
const errorMessage = t.err.message || t.err.toString() || 'Test failed'
|
|
@@ -378,9 +470,10 @@ module.exports = function (config) {
|
|
|
378
470
|
passes: passedTests,
|
|
379
471
|
failures: failedTests,
|
|
380
472
|
pending: pendingTests,
|
|
381
|
-
skipped: skippedTests,
|
|
382
473
|
duration: reportData.duration,
|
|
383
474
|
failedHooks: result.stats?.failedHooks || 0,
|
|
475
|
+
flaky: flakyTests,
|
|
476
|
+
artifacts: totalArtifacts,
|
|
384
477
|
}
|
|
385
478
|
|
|
386
479
|
// Debug logging for final stats
|
|
@@ -410,15 +503,20 @@ module.exports = function (config) {
|
|
|
410
503
|
// Always overwrite the file with the latest complete data from this worker
|
|
411
504
|
// This prevents double-counting when the event is triggered multiple times
|
|
412
505
|
fs.writeFileSync(jsonPath, safeJsonStringify(reportData))
|
|
413
|
-
output.
|
|
506
|
+
output.debug(`HTML Reporter: Generated worker JSON results: ${jsonFileName}`)
|
|
414
507
|
} catch (error) {
|
|
415
|
-
output.
|
|
508
|
+
output.debug(`HTML Reporter: Failed to write worker JSON: ${error.message}`)
|
|
416
509
|
}
|
|
417
510
|
return
|
|
418
511
|
}
|
|
419
512
|
|
|
420
513
|
// Single process mode - generate report normally
|
|
421
|
-
|
|
514
|
+
try {
|
|
515
|
+
await generateHtmlReport(reportData, options)
|
|
516
|
+
} catch (error) {
|
|
517
|
+
output.print(`Failed to generate HTML report: ${error.message}`)
|
|
518
|
+
output.debug(`HTML Reporter error stack: ${error.stack}`)
|
|
519
|
+
}
|
|
422
520
|
|
|
423
521
|
// Export stats if configured
|
|
424
522
|
if (options.exportStats) {
|
|
@@ -542,8 +640,8 @@ module.exports = function (config) {
|
|
|
542
640
|
const testName = replicateTestToFileName(originalTestName)
|
|
543
641
|
const featureName = replicateTestToFileName(originalFeatureName)
|
|
544
642
|
|
|
545
|
-
output.
|
|
546
|
-
output.
|
|
643
|
+
output.debug(`HTML Reporter: Original test title: "${originalTestName}"`)
|
|
644
|
+
output.debug(`HTML Reporter: CodeceptJS filename: "${testName}"`)
|
|
547
645
|
|
|
548
646
|
// Generate possible screenshot names based on CodeceptJS patterns
|
|
549
647
|
const possibleNames = [
|
|
@@ -567,19 +665,19 @@ module.exports = function (config) {
|
|
|
567
665
|
'failure.jpg',
|
|
568
666
|
]
|
|
569
667
|
|
|
570
|
-
output.
|
|
668
|
+
output.debug(`HTML Reporter: Checking ${possibleNames.length} possible screenshot names for "${testName}"`)
|
|
571
669
|
|
|
572
670
|
// Search for screenshots in possible directories
|
|
573
671
|
for (const dir of possibleDirs) {
|
|
574
|
-
output.
|
|
672
|
+
output.debug(`HTML Reporter: Checking directory: ${dir}`)
|
|
575
673
|
if (!fs.existsSync(dir)) {
|
|
576
|
-
output.
|
|
674
|
+
output.debug(`HTML Reporter: Directory does not exist: ${dir}`)
|
|
577
675
|
continue
|
|
578
676
|
}
|
|
579
677
|
|
|
580
678
|
try {
|
|
581
679
|
const files = fs.readdirSync(dir)
|
|
582
|
-
output.
|
|
680
|
+
output.debug(`HTML Reporter: Found ${files.length} files in ${dir}`)
|
|
583
681
|
|
|
584
682
|
// Look for exact matches first
|
|
585
683
|
for (const name of possibleNames) {
|
|
@@ -587,7 +685,7 @@ module.exports = function (config) {
|
|
|
587
685
|
const fullPath = path.join(dir, name)
|
|
588
686
|
if (!screenshots.includes(fullPath)) {
|
|
589
687
|
screenshots.push(fullPath)
|
|
590
|
-
output.
|
|
688
|
+
output.debug(`HTML Reporter: Found screenshot: ${fullPath}`)
|
|
591
689
|
}
|
|
592
690
|
}
|
|
593
691
|
}
|
|
@@ -618,16 +716,16 @@ module.exports = function (config) {
|
|
|
618
716
|
const fullPath = path.join(dir, file)
|
|
619
717
|
if (!screenshots.includes(fullPath)) {
|
|
620
718
|
screenshots.push(fullPath)
|
|
621
|
-
output.
|
|
719
|
+
output.debug(`HTML Reporter: Found related screenshot: ${fullPath}`)
|
|
622
720
|
}
|
|
623
721
|
}
|
|
624
722
|
} catch (error) {
|
|
625
723
|
// Ignore directory read errors
|
|
626
|
-
output.
|
|
724
|
+
output.debug(`HTML Reporter: Could not read directory ${dir}: ${error.message}`)
|
|
627
725
|
}
|
|
628
726
|
}
|
|
629
727
|
} catch (error) {
|
|
630
|
-
output.
|
|
728
|
+
output.debug(`HTML Reporter: Error collecting screenshots: ${error.message}`)
|
|
631
729
|
}
|
|
632
730
|
|
|
633
731
|
return screenshots
|
|
@@ -654,7 +752,7 @@ module.exports = function (config) {
|
|
|
654
752
|
const statsPath = path.resolve(reportDir, config.exportStatsPath)
|
|
655
753
|
|
|
656
754
|
const exportData = {
|
|
657
|
-
timestamp: data.endTime
|
|
755
|
+
timestamp: data.endTime, // Already an ISO string
|
|
658
756
|
duration: data.duration,
|
|
659
757
|
stats: data.stats,
|
|
660
758
|
retries: data.retries,
|
|
@@ -698,7 +796,7 @@ module.exports = function (config) {
|
|
|
698
796
|
|
|
699
797
|
// Add current run to history
|
|
700
798
|
history.unshift({
|
|
701
|
-
timestamp: data.endTime
|
|
799
|
+
timestamp: data.endTime, // Already an ISO string
|
|
702
800
|
duration: data.duration,
|
|
703
801
|
stats: data.stats,
|
|
704
802
|
retries: data.retries.length,
|
|
@@ -725,11 +823,11 @@ module.exports = function (config) {
|
|
|
725
823
|
const jsonFiles = fs.readdirSync(reportDir).filter(file => file.startsWith('worker-') && file.endsWith('-results.json'))
|
|
726
824
|
|
|
727
825
|
if (jsonFiles.length === 0) {
|
|
728
|
-
output.
|
|
826
|
+
output.debug('HTML Reporter: No worker JSON results found to consolidate')
|
|
729
827
|
return
|
|
730
828
|
}
|
|
731
829
|
|
|
732
|
-
output.
|
|
830
|
+
output.debug(`HTML Reporter: Found ${jsonFiles.length} worker JSON files to consolidate`)
|
|
733
831
|
|
|
734
832
|
// Initialize consolidated data structure
|
|
735
833
|
const consolidatedData = {
|
|
@@ -758,6 +856,10 @@ module.exports = function (config) {
|
|
|
758
856
|
try {
|
|
759
857
|
const workerData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
|
760
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
|
+
|
|
761
863
|
// Merge stats
|
|
762
864
|
if (workerData.stats) {
|
|
763
865
|
consolidatedData.stats.passes += workerData.stats.passes || 0
|
|
@@ -769,8 +871,14 @@ module.exports = function (config) {
|
|
|
769
871
|
consolidatedData.stats.failedHooks += workerData.stats.failedHooks || 0
|
|
770
872
|
}
|
|
771
873
|
|
|
772
|
-
// Merge tests and
|
|
773
|
-
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
|
+
}
|
|
774
882
|
if (workerData.failures) consolidatedData.failures.push(...workerData.failures)
|
|
775
883
|
if (workerData.hooks) consolidatedData.hooks.push(...workerData.hooks)
|
|
776
884
|
if (workerData.retries) consolidatedData.retries.push(...workerData.retries)
|
|
@@ -821,9 +929,9 @@ module.exports = function (config) {
|
|
|
821
929
|
saveTestHistory(consolidatedData, config)
|
|
822
930
|
}
|
|
823
931
|
|
|
824
|
-
output.
|
|
932
|
+
output.debug(`HTML Reporter: Successfully consolidated ${jsonFiles.length} worker reports`)
|
|
825
933
|
} catch (error) {
|
|
826
|
-
output.
|
|
934
|
+
output.debug(`HTML Reporter: Failed to consolidate worker reports: ${error.message}`)
|
|
827
935
|
}
|
|
828
936
|
}
|
|
829
937
|
|
|
@@ -844,7 +952,7 @@ module.exports = function (config) {
|
|
|
844
952
|
|
|
845
953
|
// Add current run to history for chart display (before saving to file)
|
|
846
954
|
const currentRun = {
|
|
847
|
-
timestamp: data.endTime
|
|
955
|
+
timestamp: data.endTime, // Already an ISO string
|
|
848
956
|
duration: data.duration,
|
|
849
957
|
stats: data.stats,
|
|
850
958
|
retries: data.retries.length,
|
|
@@ -863,7 +971,7 @@ module.exports = function (config) {
|
|
|
863
971
|
|
|
864
972
|
const html = template(getHtmlTemplate(), {
|
|
865
973
|
title: `CodeceptJS Test Report v${Codecept.version()}`,
|
|
866
|
-
timestamp: data.endTime
|
|
974
|
+
timestamp: data.endTime, // Already an ISO string
|
|
867
975
|
duration: formatDuration(data.duration),
|
|
868
976
|
stats: JSON.stringify(data.stats),
|
|
869
977
|
history: JSON.stringify(history),
|
|
@@ -887,6 +995,10 @@ module.exports = function (config) {
|
|
|
887
995
|
const failed = stats.failures || 0
|
|
888
996
|
const pending = stats.pending || 0
|
|
889
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'
|
|
890
1002
|
|
|
891
1003
|
return `
|
|
892
1004
|
<div class="stats-cards">
|
|
@@ -903,9 +1015,21 @@ module.exports = function (config) {
|
|
|
903
1015
|
<span class="stat-number">${failed}</span>
|
|
904
1016
|
</div>
|
|
905
1017
|
<div class="stat-card pending">
|
|
906
|
-
<h3>
|
|
1018
|
+
<h3>Skipped</h3>
|
|
907
1019
|
<span class="stat-number">${pending}</span>
|
|
908
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>
|
|
909
1033
|
</div>
|
|
910
1034
|
<div class="pie-chart-container">
|
|
911
1035
|
<canvas id="statsChart" width="300" height="300"></canvas>
|
|
@@ -926,47 +1050,73 @@ module.exports = function (config) {
|
|
|
926
1050
|
return '<p>No tests found.</p>'
|
|
927
1051
|
}
|
|
928
1052
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const artifacts = config.includeArtifacts && test.artifacts ? generateArtifactsHtml(test.artifacts, test.state === 'failed') : ''
|
|
937
|
-
const metadata = config.showMetadata && (test.meta || test.opts) ? generateMetadataHtml(test.meta, test.opts) : ''
|
|
938
|
-
const tags = config.showTags && test.tags && test.tags.length > 0 ? generateTagsHtml(test.tags) : ''
|
|
939
|
-
const retries = config.showRetries && test.retryAttempts > 0 ? generateTestRetryHtml(test.retryAttempts) : ''
|
|
940
|
-
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
|
+
})
|
|
941
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()
|
|
942
1065
|
return `
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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('')}
|
|
955
1117
|
</div>
|
|
956
|
-
</
|
|
957
|
-
|
|
958
|
-
${test.err ? `<div class="error-message"><pre>${escapeHtml(getErrorMessage(test))}</pre></div>` : ''}
|
|
959
|
-
${featureDetails}
|
|
960
|
-
${tags}
|
|
961
|
-
${metadata}
|
|
962
|
-
${retries}
|
|
963
|
-
${notes}
|
|
964
|
-
${hooks}
|
|
965
|
-
${steps}
|
|
966
|
-
${artifacts}
|
|
967
|
-
</div>
|
|
968
|
-
</div>
|
|
969
|
-
`
|
|
1118
|
+
</section>
|
|
1119
|
+
`
|
|
970
1120
|
})
|
|
971
1121
|
.join('')
|
|
972
1122
|
}
|
|
@@ -1056,13 +1206,19 @@ module.exports = function (config) {
|
|
|
1056
1206
|
const statusClass = hook.status || 'unknown'
|
|
1057
1207
|
const hookType = hook.type || 'hook'
|
|
1058
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>` : ''
|
|
1059
1211
|
|
|
1060
1212
|
return `
|
|
1061
1213
|
<div class="hook-item ${statusClass}">
|
|
1062
1214
|
<span class="hook-status ${statusClass}">●</span>
|
|
1063
|
-
<
|
|
1064
|
-
|
|
1065
|
-
|
|
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>
|
|
1066
1222
|
</div>
|
|
1067
1223
|
`
|
|
1068
1224
|
})
|
|
@@ -1122,12 +1278,21 @@ module.exports = function (config) {
|
|
|
1122
1278
|
`
|
|
1123
1279
|
}
|
|
1124
1280
|
|
|
1125
|
-
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
|
+
|
|
1126
1285
|
return `
|
|
1127
1286
|
<div class="retry-section">
|
|
1128
|
-
<h4>Retry
|
|
1287
|
+
<h4>Retry History:</h4>
|
|
1129
1288
|
<div class="retry-info">
|
|
1130
|
-
<
|
|
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>
|
|
1131
1296
|
</div>
|
|
1132
1297
|
</div>
|
|
1133
1298
|
`
|
|
@@ -1135,19 +1300,19 @@ module.exports = function (config) {
|
|
|
1135
1300
|
|
|
1136
1301
|
function generateArtifactsHtml(artifacts, isFailedTest = false) {
|
|
1137
1302
|
if (!artifacts || artifacts.length === 0) {
|
|
1138
|
-
output.
|
|
1303
|
+
output.debug(`HTML Reporter: No artifacts found for test`)
|
|
1139
1304
|
return ''
|
|
1140
1305
|
}
|
|
1141
1306
|
|
|
1142
|
-
output.
|
|
1143
|
-
output.
|
|
1307
|
+
output.debug(`HTML Reporter: Processing ${artifacts.length} artifacts, isFailedTest: ${isFailedTest}`)
|
|
1308
|
+
output.debug(`HTML Reporter: Artifacts: ${JSON.stringify(artifacts)}`)
|
|
1144
1309
|
|
|
1145
1310
|
// Separate screenshots from other artifacts
|
|
1146
1311
|
const screenshots = []
|
|
1147
1312
|
const otherArtifacts = []
|
|
1148
1313
|
|
|
1149
1314
|
artifacts.forEach(artifact => {
|
|
1150
|
-
output.
|
|
1315
|
+
output.debug(`HTML Reporter: Processing artifact: ${artifact} (type: ${typeof artifact})`)
|
|
1151
1316
|
|
|
1152
1317
|
// Handle different artifact formats
|
|
1153
1318
|
let artifactPath = artifact
|
|
@@ -1162,14 +1327,14 @@ module.exports = function (config) {
|
|
|
1162
1327
|
// Check if it's a screenshot file
|
|
1163
1328
|
if (typeof artifactPath === 'string' && artifactPath.match(/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i)) {
|
|
1164
1329
|
screenshots.push(artifactPath)
|
|
1165
|
-
output.
|
|
1330
|
+
output.debug(`HTML Reporter: Found screenshot: ${artifactPath}`)
|
|
1166
1331
|
} else {
|
|
1167
1332
|
otherArtifacts.push(artifact)
|
|
1168
|
-
output.
|
|
1333
|
+
output.debug(`HTML Reporter: Found other artifact: ${artifact}`)
|
|
1169
1334
|
}
|
|
1170
1335
|
})
|
|
1171
1336
|
|
|
1172
|
-
output.
|
|
1337
|
+
output.debug(`HTML Reporter: Found ${screenshots.length} screenshots and ${otherArtifacts.length} other artifacts`)
|
|
1173
1338
|
|
|
1174
1339
|
let artifactsHtml = ''
|
|
1175
1340
|
|
|
@@ -1198,7 +1363,7 @@ module.exports = function (config) {
|
|
|
1198
1363
|
}
|
|
1199
1364
|
}
|
|
1200
1365
|
|
|
1201
|
-
output.
|
|
1366
|
+
output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
|
|
1202
1367
|
|
|
1203
1368
|
return `
|
|
1204
1369
|
<div class="screenshot-container">
|
|
@@ -1242,7 +1407,7 @@ module.exports = function (config) {
|
|
|
1242
1407
|
}
|
|
1243
1408
|
}
|
|
1244
1409
|
|
|
1245
|
-
output.
|
|
1410
|
+
output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
|
|
1246
1411
|
return `<img src="${relativePath}" alt="Screenshot" class="artifact-image" onclick="openImageModal(this.src)"/>`
|
|
1247
1412
|
})
|
|
1248
1413
|
.join('')
|
|
@@ -1373,6 +1538,30 @@ module.exports = function (config) {
|
|
|
1373
1538
|
|
|
1374
1539
|
function escapeHtml(text) {
|
|
1375
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
|
+
}
|
|
1376
1565
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
1377
1566
|
}
|
|
1378
1567
|
|
|
@@ -1488,8 +1677,12 @@ module.exports = function (config) {
|
|
|
1488
1677
|
if (!systemInfo) return ''
|
|
1489
1678
|
|
|
1490
1679
|
const formatInfo = (key, value) => {
|
|
1680
|
+
// Handle array values (e.g., ['Node', '22.14.0', 'path'])
|
|
1491
1681
|
if (Array.isArray(value) && value.length > 1) {
|
|
1492
|
-
|
|
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>`
|
|
1493
1686
|
} else if (typeof value === 'string' && value !== 'N/A' && value !== 'undefined') {
|
|
1494
1687
|
return `<div class="info-item"><span class="info-key">${key}:</span> <span class="info-value">${escapeHtml(value)}</span></div>`
|
|
1495
1688
|
}
|
|
@@ -1531,32 +1724,48 @@ module.exports = function (config) {
|
|
|
1531
1724
|
<!DOCTYPE html>
|
|
1532
1725
|
<html lang="en">
|
|
1533
1726
|
<head>
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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>
|
|
1538
1731
|
</head>
|
|
1539
1732
|
<body>
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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>
|
|
1547
1740
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1741
|
+
<main class="report-content">
|
|
1742
|
+
{{systemInfoHtml}}
|
|
1550
1743
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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>
|
|
1554
1761
|
</section>
|
|
1555
1762
|
|
|
1556
1763
|
<section class="history-section" style="display: {{showHistory}};">
|
|
1557
|
-
<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>
|
|
1558
1767
|
<div class="history-chart-container">
|
|
1559
|
-
<canvas id="historyChart" width="
|
|
1768
|
+
<canvas id="historyChart" width="1600" height="600"></canvas>
|
|
1560
1769
|
</div>
|
|
1561
1770
|
</section>
|
|
1562
1771
|
|
|
@@ -1607,10 +1816,10 @@ module.exports = function (config) {
|
|
|
1607
1816
|
</div>
|
|
1608
1817
|
</section>
|
|
1609
1818
|
|
|
1610
|
-
<section class="retries-section" style="display:
|
|
1611
|
-
<h2>Test Retries</h2>
|
|
1819
|
+
<section class="retries-section" style="display: none;">
|
|
1820
|
+
<h2>Test Retries (Moved to Test Details)</h2>
|
|
1612
1821
|
<div class="retries-container">
|
|
1613
|
-
|
|
1822
|
+
<p>Retry information is now shown in each test's details section.</p>
|
|
1614
1823
|
</div>
|
|
1615
1824
|
</section>
|
|
1616
1825
|
|
|
@@ -1675,7 +1884,7 @@ body {
|
|
|
1675
1884
|
padding: 0 1rem;
|
|
1676
1885
|
}
|
|
1677
1886
|
|
|
1678
|
-
.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 {
|
|
1679
1888
|
background: white;
|
|
1680
1889
|
margin-bottom: 2rem;
|
|
1681
1890
|
border-radius: 8px;
|
|
@@ -1683,7 +1892,7 @@ body {
|
|
|
1683
1892
|
overflow: hidden;
|
|
1684
1893
|
}
|
|
1685
1894
|
|
|
1686
|
-
.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 {
|
|
1687
1896
|
background: #34495e;
|
|
1688
1897
|
color: white;
|
|
1689
1898
|
padding: 1rem;
|
|
@@ -1710,6 +1919,23 @@ body {
|
|
|
1710
1919
|
.stat-card.passed { background: #27ae60; }
|
|
1711
1920
|
.stat-card.failed { background: #e74c3c; }
|
|
1712
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
|
+
}
|
|
1713
1939
|
|
|
1714
1940
|
.stat-card h3 {
|
|
1715
1941
|
font-size: 0.9rem;
|
|
@@ -1737,6 +1963,54 @@ body {
|
|
|
1737
1963
|
height: auto;
|
|
1738
1964
|
}
|
|
1739
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
|
+
|
|
1740
2014
|
.test-item {
|
|
1741
2015
|
border-bottom: 1px solid #eee;
|
|
1742
2016
|
margin: 0;
|
|
@@ -1814,9 +2088,64 @@ body {
|
|
|
1814
2088
|
font-weight: bold;
|
|
1815
2089
|
}
|
|
1816
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
|
+
|
|
1817
2141
|
.test-duration {
|
|
1818
|
-
font-size: 0.
|
|
1819
|
-
|
|
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;
|
|
1820
2149
|
}
|
|
1821
2150
|
|
|
1822
2151
|
.test-details {
|
|
@@ -1854,27 +2183,39 @@ body {
|
|
|
1854
2183
|
|
|
1855
2184
|
.hook-item {
|
|
1856
2185
|
display: flex;
|
|
1857
|
-
align-items:
|
|
1858
|
-
padding: 0.
|
|
1859
|
-
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;
|
|
1860
2192
|
}
|
|
1861
2193
|
|
|
1862
2194
|
.hook-item:last-child {
|
|
1863
|
-
|
|
2195
|
+
margin-bottom: 0;
|
|
1864
2196
|
}
|
|
1865
2197
|
|
|
1866
2198
|
.hook-status {
|
|
1867
|
-
margin-right: 0.
|
|
2199
|
+
margin-right: 0.75rem;
|
|
2200
|
+
flex-shrink: 0;
|
|
2201
|
+
margin-top: 0.2rem;
|
|
1868
2202
|
}
|
|
1869
2203
|
|
|
1870
2204
|
.hook-status.passed { color: #27ae60; }
|
|
1871
2205
|
.hook-status.failed { color: #e74c3c; }
|
|
1872
2206
|
|
|
1873
|
-
.hook-
|
|
2207
|
+
.hook-content {
|
|
1874
2208
|
flex: 1;
|
|
2209
|
+
display: flex;
|
|
2210
|
+
flex-direction: column;
|
|
2211
|
+
gap: 0.25rem;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
.hook-title {
|
|
1875
2215
|
font-family: 'Courier New', monospace;
|
|
1876
2216
|
font-size: 0.9rem;
|
|
1877
2217
|
font-weight: bold;
|
|
2218
|
+
color: #2c3e50;
|
|
1878
2219
|
}
|
|
1879
2220
|
|
|
1880
2221
|
.hook-duration {
|
|
@@ -1882,8 +2223,13 @@ body {
|
|
|
1882
2223
|
color: #7f8c8d;
|
|
1883
2224
|
}
|
|
1884
2225
|
|
|
2226
|
+
.hook-location, .hook-context {
|
|
2227
|
+
font-size: 0.8rem;
|
|
2228
|
+
color: #6c757d;
|
|
2229
|
+
font-style: italic;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
1885
2232
|
.hook-error {
|
|
1886
|
-
width: 100%;
|
|
1887
2233
|
margin-top: 0.5rem;
|
|
1888
2234
|
padding: 0.5rem;
|
|
1889
2235
|
background: #fee;
|
|
@@ -2172,11 +2518,22 @@ body {
|
|
|
2172
2518
|
}
|
|
2173
2519
|
|
|
2174
2520
|
/* Retry Information */
|
|
2521
|
+
.retry-section {
|
|
2522
|
+
margin-top: 1rem;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2175
2525
|
.retry-info {
|
|
2176
|
-
padding:
|
|
2177
|
-
background: #
|
|
2526
|
+
padding: 1rem;
|
|
2527
|
+
background: #fff9e6;
|
|
2178
2528
|
border-radius: 4px;
|
|
2179
|
-
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;
|
|
2180
2537
|
}
|
|
2181
2538
|
|
|
2182
2539
|
.retry-count {
|
|
@@ -2184,6 +2541,29 @@ body {
|
|
|
2184
2541
|
font-weight: 500;
|
|
2185
2542
|
}
|
|
2186
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
|
+
|
|
2187
2567
|
/* Retries Section */
|
|
2188
2568
|
.retry-item {
|
|
2189
2569
|
padding: 1rem;
|
|
@@ -2229,6 +2609,92 @@ body {
|
|
|
2229
2609
|
}
|
|
2230
2610
|
|
|
2231
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
|
+
|
|
2232
2698
|
.history-chart-container {
|
|
2233
2699
|
padding: 2rem 1rem;
|
|
2234
2700
|
display: flex;
|
|
@@ -2240,6 +2706,87 @@ body {
|
|
|
2240
2706
|
height: auto;
|
|
2241
2707
|
}
|
|
2242
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
|
+
|
|
2243
2790
|
/* Hidden items for filtering */
|
|
2244
2791
|
.test-item.filtered-out {
|
|
2245
2792
|
display: none !important;
|
|
@@ -2457,6 +3004,25 @@ body {
|
|
|
2457
3004
|
|
|
2458
3005
|
function getJsScripts() {
|
|
2459
3006
|
return `
|
|
3007
|
+
// Go to Top button
|
|
3008
|
+
function scrollToTop() {
|
|
3009
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
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
|
+
|
|
2460
3026
|
function toggleTestDetails(testId) {
|
|
2461
3027
|
const details = document.getElementById('details-' + testId);
|
|
2462
3028
|
if (details.style.display === 'none' || details.style.display === '') {
|
|
@@ -2939,9 +3505,29 @@ function drawHistoryChart() {
|
|
|
2939
3505
|
// Initialize charts and filters
|
|
2940
3506
|
document.addEventListener('DOMContentLoaded', function() {
|
|
2941
3507
|
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
3508
|
+
// Draw charts
|
|
3509
|
+
drawPieChart();
|
|
3510
|
+
drawHistoryChart();
|
|
3511
|
+
renderTestPerformance();
|
|
3512
|
+
renderHistoryTimeline();
|
|
3513
|
+
|
|
3514
|
+
// Add Go to Top button
|
|
3515
|
+
const goTopBtn = document.createElement('button');
|
|
3516
|
+
goTopBtn.innerText = '↑ Top';
|
|
3517
|
+
goTopBtn.id = 'goTopBtn';
|
|
3518
|
+
goTopBtn.style.position = 'fixed';
|
|
3519
|
+
goTopBtn.style.bottom = '30px';
|
|
3520
|
+
goTopBtn.style.right = '30px';
|
|
3521
|
+
goTopBtn.style.zIndex = '9999';
|
|
3522
|
+
goTopBtn.style.padding = '12px 18px';
|
|
3523
|
+
goTopBtn.style.borderRadius = '50%';
|
|
3524
|
+
goTopBtn.style.background = '#27ae60';
|
|
3525
|
+
goTopBtn.style.color = '#fff';
|
|
3526
|
+
goTopBtn.style.fontSize = '20px';
|
|
3527
|
+
goTopBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
|
3528
|
+
goTopBtn.style.cursor = 'pointer';
|
|
3529
|
+
goTopBtn.onclick = scrollToTop;
|
|
3530
|
+
document.body.appendChild(goTopBtn);
|
|
2945
3531
|
|
|
2946
3532
|
// Set up filter event listeners
|
|
2947
3533
|
document.getElementById('statusFilter').addEventListener('change', applyFilters);
|
|
@@ -2950,6 +3536,141 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
2950
3536
|
document.getElementById('retryFilter').addEventListener('change', applyFilters);
|
|
2951
3537
|
document.getElementById('typeFilter').addEventListener('change', applyFilters);
|
|
2952
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
|
+
}
|
|
2953
3674
|
`
|
|
2954
3675
|
}
|
|
2955
3676
|
}
|