codeceptjs 3.7.6-beta.2 → 3.7.6-beta.4
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/event.js +10 -1
- package/lib/helper/Appium.js +145 -25
- package/lib/helper/JSONResponse.js +4 -4
- package/lib/helper/Playwright.js +2 -3
- package/lib/helper/Puppeteer.js +6 -6
- package/lib/helper/WebDriver.js +3 -3
- package/lib/listener/steps.js +2 -2
- package/lib/output.js +51 -5
- package/lib/plugin/htmlReporter.js +147 -79
- package/lib/result.js +100 -23
- package/package.json +5 -2
- package/typings/promiseBasedTypes.d.ts +37 -25
- package/typings/types.d.ts +127 -30
|
@@ -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
|
|
@@ -103,12 +110,12 @@ module.exports = function (config) {
|
|
|
103
110
|
// Method 1: Check retryNum property (most reliable)
|
|
104
111
|
if (test.retryNum && test.retryNum > 0) {
|
|
105
112
|
testRetryAttempts.set(testId, test.retryNum)
|
|
106
|
-
output.
|
|
113
|
+
output.debug(`HTML Reporter: Retry count detected (retryNum) for ${test.title}, attempts: ${test.retryNum}`)
|
|
107
114
|
}
|
|
108
115
|
// Method 2: Check currentRetry property
|
|
109
116
|
else if (test.currentRetry && test.currentRetry > 0) {
|
|
110
117
|
testRetryAttempts.set(testId, test.currentRetry)
|
|
111
|
-
output.
|
|
118
|
+
output.debug(`HTML Reporter: Retry count detected (currentRetry) for ${test.title}, attempts: ${test.currentRetry}`)
|
|
112
119
|
}
|
|
113
120
|
// Method 3: Check if this is a retried test
|
|
114
121
|
else if (test.retriedTest && test.retriedTest()) {
|
|
@@ -119,12 +126,12 @@ module.exports = function (config) {
|
|
|
119
126
|
} else {
|
|
120
127
|
testRetryAttempts.set(originalTestId, testRetryAttempts.get(originalTestId) + 1)
|
|
121
128
|
}
|
|
122
|
-
output.
|
|
129
|
+
output.debug(`HTML Reporter: Retry detected (retriedTest) for ${originalTest.title}, attempts: ${testRetryAttempts.get(originalTestId)}`)
|
|
123
130
|
}
|
|
124
131
|
// Method 4: Check if test has been seen before (indicating a retry)
|
|
125
132
|
else if (reportData.tests.some(t => t.id === testId)) {
|
|
126
133
|
testRetryAttempts.set(testId, 1) // First retry detected
|
|
127
|
-
output.
|
|
134
|
+
output.debug(`HTML Reporter: Retry detected (duplicate test) for ${test.title}, attempts: 1`)
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
})
|
|
@@ -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
|
})
|
|
@@ -196,33 +223,39 @@ module.exports = function (config) {
|
|
|
196
223
|
if (test.retryNum && test.retryNum > 0) {
|
|
197
224
|
retryAttempts = test.retryNum
|
|
198
225
|
testRetryAttempts.set(testId, retryAttempts)
|
|
199
|
-
output.
|
|
226
|
+
output.debug(`HTML Reporter: Late retry detection (retryNum) for ${test.title}, attempts: ${retryAttempts}`)
|
|
200
227
|
} else if (test.currentRetry && test.currentRetry > 0) {
|
|
201
228
|
retryAttempts = test.currentRetry
|
|
202
229
|
testRetryAttempts.set(testId, retryAttempts)
|
|
203
|
-
output.
|
|
230
|
+
output.debug(`HTML Reporter: Late retry detection (currentRetry) for ${test.title}, attempts: ${retryAttempts}`)
|
|
204
231
|
} else if (test._retries && test._retries > 0) {
|
|
205
232
|
retryAttempts = test._retries
|
|
206
233
|
testRetryAttempts.set(testId, retryAttempts)
|
|
207
|
-
output.
|
|
234
|
+
output.debug(`HTML Reporter: Late retry detection (_retries) for ${test.title}, attempts: ${retryAttempts}`)
|
|
208
235
|
}
|
|
209
236
|
}
|
|
210
237
|
|
|
211
238
|
// Debug logging
|
|
212
|
-
output.
|
|
239
|
+
output.debug(`HTML Reporter: Test finished - ${test.title}, State: ${test.state}, Retries: ${retryAttempts}`)
|
|
213
240
|
|
|
214
|
-
// Detect if this is a BDD/Gherkin test
|
|
215
|
-
const
|
|
241
|
+
// Detect if this is a BDD/Gherkin test - use test.parent directly instead of currentSuite
|
|
242
|
+
const suite = test.parent || test.suite || currentSuite
|
|
243
|
+
const isBddTest = isBddGherkinTest(test, suite)
|
|
216
244
|
const steps = isBddTest ? currentBddSteps : currentTestSteps
|
|
217
|
-
const featureInfo = isBddTest ? getBddFeatureInfo(test,
|
|
245
|
+
const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null
|
|
218
246
|
|
|
219
247
|
// Check if this test already exists in reportData.tests (from a previous retry)
|
|
220
248
|
const existingTestIndex = reportData.tests.findIndex(t => t.id === testId)
|
|
221
|
-
const hasFailedBefore = existingTestIndex >= 0 && reportData.tests[existingTestIndex].state === 'failed'
|
|
249
|
+
const hasFailedBefore = existingTestIndex >= 0 && reportData.tests[existingTestIndex] && reportData.tests[existingTestIndex].state === 'failed'
|
|
222
250
|
const currentlyFailed = test.state === 'failed'
|
|
223
251
|
|
|
224
252
|
// Debug artifacts collection (but don't process them yet - screenshots may not be ready)
|
|
225
|
-
output.
|
|
253
|
+
output.debug(`HTML Reporter: Test ${test.title} artifacts at test.finished: ${JSON.stringify(test.artifacts)}`)
|
|
254
|
+
|
|
255
|
+
// Extract parent/suite title before serialization (for worker mode)
|
|
256
|
+
// This ensures the feature name is preserved when test data is JSON stringified
|
|
257
|
+
const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null
|
|
258
|
+
const suiteTitle = test.suite?.title || (suite && suite.title) || null
|
|
226
259
|
|
|
227
260
|
const testData = {
|
|
228
261
|
...test,
|
|
@@ -239,16 +272,19 @@ module.exports = function (config) {
|
|
|
239
272
|
uid: test.uid,
|
|
240
273
|
isBdd: isBddTest,
|
|
241
274
|
feature: featureInfo,
|
|
275
|
+
// Store parent/suite titles as simple strings for worker mode serialization
|
|
276
|
+
parentTitle: parentTitle,
|
|
277
|
+
suiteTitle: suiteTitle,
|
|
242
278
|
}
|
|
243
279
|
|
|
244
280
|
if (existingTestIndex >= 0) {
|
|
245
281
|
// Update existing test with final result (including failed state)
|
|
246
|
-
reportData.tests[existingTestIndex] = testData
|
|
247
|
-
output.
|
|
282
|
+
if (existingTestIndex >= 0) reportData.tests[existingTestIndex] = testData
|
|
283
|
+
output.debug(`HTML Reporter: Updated existing test - ${test.title}, Final state: ${test.state}`)
|
|
248
284
|
} else {
|
|
249
285
|
// Add new test
|
|
250
286
|
reportData.tests.push(testData)
|
|
251
|
-
output.
|
|
287
|
+
output.debug(`HTML Reporter: Added new test - ${test.title}, State: ${test.state}`)
|
|
252
288
|
}
|
|
253
289
|
|
|
254
290
|
// Track retry information - only add if there were actual retries AND the test failed at some point
|
|
@@ -262,7 +298,7 @@ module.exports = function (config) {
|
|
|
262
298
|
if (retryAttempts === 0 && existingRetryIndex >= 0) {
|
|
263
299
|
retryAttempts = reportData.retries[existingRetryIndex].attempts + 1
|
|
264
300
|
testRetryAttempts.set(testId, retryAttempts)
|
|
265
|
-
output.
|
|
301
|
+
output.debug(`HTML Reporter: Incremented retry count for duplicate test ${test.title}, attempts: ${retryAttempts}`)
|
|
266
302
|
}
|
|
267
303
|
|
|
268
304
|
// Remove existing retry info for this test and add updated one
|
|
@@ -274,7 +310,7 @@ module.exports = function (config) {
|
|
|
274
310
|
finalState: test.state,
|
|
275
311
|
duration: test.duration || 0,
|
|
276
312
|
})
|
|
277
|
-
output.
|
|
313
|
+
output.debug(`HTML Reporter: Added retry info for ${test.title}, attempts: ${retryAttempts}, state: ${test.state}`)
|
|
278
314
|
}
|
|
279
315
|
|
|
280
316
|
// Fallback: If this test already exists and either failed before or is failing now, it's a retry
|
|
@@ -288,50 +324,50 @@ module.exports = function (config) {
|
|
|
288
324
|
finalState: test.state,
|
|
289
325
|
duration: test.duration || 0,
|
|
290
326
|
})
|
|
291
|
-
output.
|
|
327
|
+
output.debug(`HTML Reporter: Fallback retry detection for failed test ${test.title}, attempts: ${fallbackAttempts}`)
|
|
292
328
|
}
|
|
293
329
|
})
|
|
294
330
|
|
|
295
331
|
// Generate final report
|
|
296
|
-
event.dispatcher.on(event.all.result, result => {
|
|
297
|
-
reportData.endTime = new Date()
|
|
298
|
-
reportData.duration = reportData.endTime - reportData.startTime
|
|
332
|
+
event.dispatcher.on(event.all.result, async result => {
|
|
333
|
+
reportData.endTime = new Date().toISOString()
|
|
334
|
+
reportData.duration = new Date(reportData.endTime).getTime() - new Date(reportData.startTime).getTime()
|
|
299
335
|
|
|
300
336
|
// Process artifacts now that all async tasks (including screenshots) are complete
|
|
301
|
-
output.
|
|
337
|
+
output.debug(`HTML Reporter: Processing artifacts for ${reportData.tests.length} tests after all async tasks complete`)
|
|
302
338
|
|
|
303
339
|
reportData.tests.forEach(test => {
|
|
304
340
|
const originalArtifacts = test.artifacts
|
|
305
341
|
let collectedArtifacts = []
|
|
306
342
|
|
|
307
|
-
output.
|
|
308
|
-
output.
|
|
343
|
+
output.debug(`HTML Reporter: Processing test "${test.title}" (ID: ${test.id})`)
|
|
344
|
+
output.debug(`HTML Reporter: Test ${test.title} final artifacts: ${JSON.stringify(originalArtifacts)}`)
|
|
309
345
|
|
|
310
346
|
if (originalArtifacts) {
|
|
311
347
|
if (Array.isArray(originalArtifacts)) {
|
|
312
348
|
collectedArtifacts = originalArtifacts
|
|
313
|
-
output.
|
|
349
|
+
output.debug(`HTML Reporter: Using array artifacts: ${collectedArtifacts.length} items`)
|
|
314
350
|
} else if (typeof originalArtifacts === 'object') {
|
|
315
351
|
// Convert object properties to array (screenshotOnFail plugin format)
|
|
316
352
|
collectedArtifacts = Object.values(originalArtifacts).filter(artifact => artifact)
|
|
317
|
-
output.
|
|
318
|
-
output.
|
|
353
|
+
output.debug(`HTML Reporter: Converted artifacts object to array: ${collectedArtifacts.length} items`)
|
|
354
|
+
output.debug(`HTML Reporter: Converted artifacts: ${JSON.stringify(collectedArtifacts)}`)
|
|
319
355
|
}
|
|
320
356
|
}
|
|
321
357
|
|
|
322
358
|
// Only use filesystem fallback if no artifacts found from screenshotOnFail plugin
|
|
323
359
|
if (collectedArtifacts.length === 0 && test.state === 'failed') {
|
|
324
|
-
output.
|
|
360
|
+
output.debug(`HTML Reporter: No artifacts from plugin, trying filesystem for test "${test.title}"`)
|
|
325
361
|
collectedArtifacts = collectScreenshotsFromFilesystem(test, test.id)
|
|
326
|
-
output.
|
|
362
|
+
output.debug(`HTML Reporter: Collected ${collectedArtifacts.length} screenshots from filesystem for failed test "${test.title}"`)
|
|
327
363
|
if (collectedArtifacts.length > 0) {
|
|
328
|
-
output.
|
|
364
|
+
output.debug(`HTML Reporter: Filesystem screenshots for "${test.title}": ${JSON.stringify(collectedArtifacts)}`)
|
|
329
365
|
}
|
|
330
366
|
}
|
|
331
367
|
|
|
332
368
|
// Update test with processed artifacts
|
|
333
369
|
test.artifacts = collectedArtifacts
|
|
334
|
-
output.
|
|
370
|
+
output.debug(`HTML Reporter: Final artifacts for "${test.title}": ${JSON.stringify(test.artifacts)}`)
|
|
335
371
|
})
|
|
336
372
|
|
|
337
373
|
// Calculate stats from our collected test data instead of using result.stats
|
|
@@ -345,7 +381,11 @@ module.exports = function (config) {
|
|
|
345
381
|
.filter(t => t.state === 'failed')
|
|
346
382
|
.map(t => {
|
|
347
383
|
const testName = t.title || 'Unknown Test'
|
|
348
|
-
|
|
384
|
+
// Try to get feature name from BDD, preserved titles (worker mode), or direct access
|
|
385
|
+
let featureName = t.feature?.name || t.parentTitle || t.suiteTitle || t.parent?.title || t.suite?.title || 'Unknown Feature'
|
|
386
|
+
if (featureName === 'Unknown Feature' && t.suite && t.suite.feature && t.suite.feature.name) {
|
|
387
|
+
featureName = t.suite.feature.name
|
|
388
|
+
}
|
|
349
389
|
|
|
350
390
|
if (t.err) {
|
|
351
391
|
const errorMessage = t.err.message || t.err.toString() || 'Test failed'
|
|
@@ -384,19 +424,19 @@ module.exports = function (config) {
|
|
|
384
424
|
}
|
|
385
425
|
|
|
386
426
|
// Debug logging for final stats
|
|
387
|
-
output.
|
|
388
|
-
output.
|
|
389
|
-
output.
|
|
390
|
-
output.
|
|
391
|
-
output.
|
|
427
|
+
output.debug(`HTML Reporter: Calculated stats - Tests: ${reportData.stats.tests}, Passes: ${reportData.stats.passes}, Failures: ${reportData.stats.failures}`)
|
|
428
|
+
output.debug(`HTML Reporter: Collected ${reportData.tests.length} tests in reportData`)
|
|
429
|
+
output.debug(`HTML Reporter: Failures array has ${reportData.failures.length} items`)
|
|
430
|
+
output.debug(`HTML Reporter: Retries array has ${reportData.retries.length} items`)
|
|
431
|
+
output.debug(`HTML Reporter: testRetryAttempts Map size: ${testRetryAttempts.size}`)
|
|
392
432
|
|
|
393
433
|
// Log retry attempts map contents
|
|
394
434
|
for (const [testId, attempts] of testRetryAttempts.entries()) {
|
|
395
|
-
output.
|
|
435
|
+
output.debug(`HTML Reporter: testRetryAttempts - ${testId}: ${attempts} attempts`)
|
|
396
436
|
}
|
|
397
437
|
|
|
398
438
|
reportData.tests.forEach(test => {
|
|
399
|
-
output.
|
|
439
|
+
output.debug(`HTML Reporter: Test in reportData - ${test.title}, State: ${test.state}, Retries: ${test.retryAttempts}`)
|
|
400
440
|
})
|
|
401
441
|
|
|
402
442
|
// Check if running with workers
|
|
@@ -410,15 +450,20 @@ module.exports = function (config) {
|
|
|
410
450
|
// Always overwrite the file with the latest complete data from this worker
|
|
411
451
|
// This prevents double-counting when the event is triggered multiple times
|
|
412
452
|
fs.writeFileSync(jsonPath, safeJsonStringify(reportData))
|
|
413
|
-
output.
|
|
453
|
+
output.debug(`HTML Reporter: Generated worker JSON results: ${jsonFileName}`)
|
|
414
454
|
} catch (error) {
|
|
415
|
-
output.
|
|
455
|
+
output.debug(`HTML Reporter: Failed to write worker JSON: ${error.message}`)
|
|
416
456
|
}
|
|
417
457
|
return
|
|
418
458
|
}
|
|
419
459
|
|
|
420
460
|
// Single process mode - generate report normally
|
|
421
|
-
|
|
461
|
+
try {
|
|
462
|
+
await generateHtmlReport(reportData, options)
|
|
463
|
+
} catch (error) {
|
|
464
|
+
output.print(`Failed to generate HTML report: ${error.message}`)
|
|
465
|
+
output.debug(`HTML Reporter error stack: ${error.stack}`)
|
|
466
|
+
}
|
|
422
467
|
|
|
423
468
|
// Export stats if configured
|
|
424
469
|
if (options.exportStats) {
|
|
@@ -542,8 +587,8 @@ module.exports = function (config) {
|
|
|
542
587
|
const testName = replicateTestToFileName(originalTestName)
|
|
543
588
|
const featureName = replicateTestToFileName(originalFeatureName)
|
|
544
589
|
|
|
545
|
-
output.
|
|
546
|
-
output.
|
|
590
|
+
output.debug(`HTML Reporter: Original test title: "${originalTestName}"`)
|
|
591
|
+
output.debug(`HTML Reporter: CodeceptJS filename: "${testName}"`)
|
|
547
592
|
|
|
548
593
|
// Generate possible screenshot names based on CodeceptJS patterns
|
|
549
594
|
const possibleNames = [
|
|
@@ -567,19 +612,19 @@ module.exports = function (config) {
|
|
|
567
612
|
'failure.jpg',
|
|
568
613
|
]
|
|
569
614
|
|
|
570
|
-
output.
|
|
615
|
+
output.debug(`HTML Reporter: Checking ${possibleNames.length} possible screenshot names for "${testName}"`)
|
|
571
616
|
|
|
572
617
|
// Search for screenshots in possible directories
|
|
573
618
|
for (const dir of possibleDirs) {
|
|
574
|
-
output.
|
|
619
|
+
output.debug(`HTML Reporter: Checking directory: ${dir}`)
|
|
575
620
|
if (!fs.existsSync(dir)) {
|
|
576
|
-
output.
|
|
621
|
+
output.debug(`HTML Reporter: Directory does not exist: ${dir}`)
|
|
577
622
|
continue
|
|
578
623
|
}
|
|
579
624
|
|
|
580
625
|
try {
|
|
581
626
|
const files = fs.readdirSync(dir)
|
|
582
|
-
output.
|
|
627
|
+
output.debug(`HTML Reporter: Found ${files.length} files in ${dir}`)
|
|
583
628
|
|
|
584
629
|
// Look for exact matches first
|
|
585
630
|
for (const name of possibleNames) {
|
|
@@ -587,7 +632,7 @@ module.exports = function (config) {
|
|
|
587
632
|
const fullPath = path.join(dir, name)
|
|
588
633
|
if (!screenshots.includes(fullPath)) {
|
|
589
634
|
screenshots.push(fullPath)
|
|
590
|
-
output.
|
|
635
|
+
output.debug(`HTML Reporter: Found screenshot: ${fullPath}`)
|
|
591
636
|
}
|
|
592
637
|
}
|
|
593
638
|
}
|
|
@@ -618,16 +663,16 @@ module.exports = function (config) {
|
|
|
618
663
|
const fullPath = path.join(dir, file)
|
|
619
664
|
if (!screenshots.includes(fullPath)) {
|
|
620
665
|
screenshots.push(fullPath)
|
|
621
|
-
output.
|
|
666
|
+
output.debug(`HTML Reporter: Found related screenshot: ${fullPath}`)
|
|
622
667
|
}
|
|
623
668
|
}
|
|
624
669
|
} catch (error) {
|
|
625
670
|
// Ignore directory read errors
|
|
626
|
-
output.
|
|
671
|
+
output.debug(`HTML Reporter: Could not read directory ${dir}: ${error.message}`)
|
|
627
672
|
}
|
|
628
673
|
}
|
|
629
674
|
} catch (error) {
|
|
630
|
-
output.
|
|
675
|
+
output.debug(`HTML Reporter: Error collecting screenshots: ${error.message}`)
|
|
631
676
|
}
|
|
632
677
|
|
|
633
678
|
return screenshots
|
|
@@ -654,7 +699,7 @@ module.exports = function (config) {
|
|
|
654
699
|
const statsPath = path.resolve(reportDir, config.exportStatsPath)
|
|
655
700
|
|
|
656
701
|
const exportData = {
|
|
657
|
-
timestamp: data.endTime
|
|
702
|
+
timestamp: data.endTime, // Already an ISO string
|
|
658
703
|
duration: data.duration,
|
|
659
704
|
stats: data.stats,
|
|
660
705
|
retries: data.retries,
|
|
@@ -698,7 +743,7 @@ module.exports = function (config) {
|
|
|
698
743
|
|
|
699
744
|
// Add current run to history
|
|
700
745
|
history.unshift({
|
|
701
|
-
timestamp: data.endTime
|
|
746
|
+
timestamp: data.endTime, // Already an ISO string
|
|
702
747
|
duration: data.duration,
|
|
703
748
|
stats: data.stats,
|
|
704
749
|
retries: data.retries.length,
|
|
@@ -725,11 +770,11 @@ module.exports = function (config) {
|
|
|
725
770
|
const jsonFiles = fs.readdirSync(reportDir).filter(file => file.startsWith('worker-') && file.endsWith('-results.json'))
|
|
726
771
|
|
|
727
772
|
if (jsonFiles.length === 0) {
|
|
728
|
-
output.
|
|
773
|
+
output.debug('HTML Reporter: No worker JSON results found to consolidate')
|
|
729
774
|
return
|
|
730
775
|
}
|
|
731
776
|
|
|
732
|
-
output.
|
|
777
|
+
output.debug(`HTML Reporter: Found ${jsonFiles.length} worker JSON files to consolidate`)
|
|
733
778
|
|
|
734
779
|
// Initialize consolidated data structure
|
|
735
780
|
const consolidatedData = {
|
|
@@ -821,9 +866,9 @@ module.exports = function (config) {
|
|
|
821
866
|
saveTestHistory(consolidatedData, config)
|
|
822
867
|
}
|
|
823
868
|
|
|
824
|
-
output.
|
|
869
|
+
output.debug(`HTML Reporter: Successfully consolidated ${jsonFiles.length} worker reports`)
|
|
825
870
|
} catch (error) {
|
|
826
|
-
output.
|
|
871
|
+
output.debug(`HTML Reporter: Failed to consolidate worker reports: ${error.message}`)
|
|
827
872
|
}
|
|
828
873
|
}
|
|
829
874
|
|
|
@@ -844,7 +889,7 @@ module.exports = function (config) {
|
|
|
844
889
|
|
|
845
890
|
// Add current run to history for chart display (before saving to file)
|
|
846
891
|
const currentRun = {
|
|
847
|
-
timestamp: data.endTime
|
|
892
|
+
timestamp: data.endTime, // Already an ISO string
|
|
848
893
|
duration: data.duration,
|
|
849
894
|
stats: data.stats,
|
|
850
895
|
retries: data.retries.length,
|
|
@@ -863,7 +908,7 @@ module.exports = function (config) {
|
|
|
863
908
|
|
|
864
909
|
const html = template(getHtmlTemplate(), {
|
|
865
910
|
title: `CodeceptJS Test Report v${Codecept.version()}`,
|
|
866
|
-
timestamp: data.endTime
|
|
911
|
+
timestamp: data.endTime, // Already an ISO string
|
|
867
912
|
duration: formatDuration(data.duration),
|
|
868
913
|
stats: JSON.stringify(data.stats),
|
|
869
914
|
history: JSON.stringify(history),
|
|
@@ -929,7 +974,9 @@ module.exports = function (config) {
|
|
|
929
974
|
return tests
|
|
930
975
|
.map(test => {
|
|
931
976
|
const statusClass = test.state || 'unknown'
|
|
932
|
-
|
|
977
|
+
// Use preserved parent/suite titles (for worker mode) or fallback to direct access
|
|
978
|
+
const feature = test.isBdd && test.feature ? test.feature.name : test.parentTitle || test.suiteTitle || test.parent?.title || test.suite?.title || 'Unknown Feature'
|
|
979
|
+
// Always try to show steps if available, even for unknown feature
|
|
933
980
|
const steps = config.showSteps && test.steps ? (test.isBdd ? generateBddStepsHtml(test.steps) : generateStepsHtml(test.steps)) : ''
|
|
934
981
|
const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : ''
|
|
935
982
|
const hooks = test.hooks && test.hooks.length > 0 ? generateHooksHtml(test.hooks) : ''
|
|
@@ -1135,19 +1182,19 @@ module.exports = function (config) {
|
|
|
1135
1182
|
|
|
1136
1183
|
function generateArtifactsHtml(artifacts, isFailedTest = false) {
|
|
1137
1184
|
if (!artifacts || artifacts.length === 0) {
|
|
1138
|
-
output.
|
|
1185
|
+
output.debug(`HTML Reporter: No artifacts found for test`)
|
|
1139
1186
|
return ''
|
|
1140
1187
|
}
|
|
1141
1188
|
|
|
1142
|
-
output.
|
|
1143
|
-
output.
|
|
1189
|
+
output.debug(`HTML Reporter: Processing ${artifacts.length} artifacts, isFailedTest: ${isFailedTest}`)
|
|
1190
|
+
output.debug(`HTML Reporter: Artifacts: ${JSON.stringify(artifacts)}`)
|
|
1144
1191
|
|
|
1145
1192
|
// Separate screenshots from other artifacts
|
|
1146
1193
|
const screenshots = []
|
|
1147
1194
|
const otherArtifacts = []
|
|
1148
1195
|
|
|
1149
1196
|
artifacts.forEach(artifact => {
|
|
1150
|
-
output.
|
|
1197
|
+
output.debug(`HTML Reporter: Processing artifact: ${artifact} (type: ${typeof artifact})`)
|
|
1151
1198
|
|
|
1152
1199
|
// Handle different artifact formats
|
|
1153
1200
|
let artifactPath = artifact
|
|
@@ -1162,14 +1209,14 @@ module.exports = function (config) {
|
|
|
1162
1209
|
// Check if it's a screenshot file
|
|
1163
1210
|
if (typeof artifactPath === 'string' && artifactPath.match(/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i)) {
|
|
1164
1211
|
screenshots.push(artifactPath)
|
|
1165
|
-
output.
|
|
1212
|
+
output.debug(`HTML Reporter: Found screenshot: ${artifactPath}`)
|
|
1166
1213
|
} else {
|
|
1167
1214
|
otherArtifacts.push(artifact)
|
|
1168
|
-
output.
|
|
1215
|
+
output.debug(`HTML Reporter: Found other artifact: ${artifact}`)
|
|
1169
1216
|
}
|
|
1170
1217
|
})
|
|
1171
1218
|
|
|
1172
|
-
output.
|
|
1219
|
+
output.debug(`HTML Reporter: Found ${screenshots.length} screenshots and ${otherArtifacts.length} other artifacts`)
|
|
1173
1220
|
|
|
1174
1221
|
let artifactsHtml = ''
|
|
1175
1222
|
|
|
@@ -1198,7 +1245,7 @@ module.exports = function (config) {
|
|
|
1198
1245
|
}
|
|
1199
1246
|
}
|
|
1200
1247
|
|
|
1201
|
-
output.
|
|
1248
|
+
output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
|
|
1202
1249
|
|
|
1203
1250
|
return `
|
|
1204
1251
|
<div class="screenshot-container">
|
|
@@ -1242,7 +1289,7 @@ module.exports = function (config) {
|
|
|
1242
1289
|
}
|
|
1243
1290
|
}
|
|
1244
1291
|
|
|
1245
|
-
output.
|
|
1292
|
+
output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
|
|
1246
1293
|
return `<img src="${relativePath}" alt="Screenshot" class="artifact-image" onclick="openImageModal(this.src)"/>`
|
|
1247
1294
|
})
|
|
1248
1295
|
.join('')
|
|
@@ -1556,7 +1603,7 @@ module.exports = function (config) {
|
|
|
1556
1603
|
<section class="history-section" style="display: {{showHistory}};">
|
|
1557
1604
|
<h2>Test History</h2>
|
|
1558
1605
|
<div class="history-chart-container">
|
|
1559
|
-
<canvas id="historyChart" width="
|
|
1606
|
+
<canvas id="historyChart" width="1600" height="600"></canvas>
|
|
1560
1607
|
</div>
|
|
1561
1608
|
</section>
|
|
1562
1609
|
|
|
@@ -2457,6 +2504,10 @@ body {
|
|
|
2457
2504
|
|
|
2458
2505
|
function getJsScripts() {
|
|
2459
2506
|
return `
|
|
2507
|
+
// Go to Top button
|
|
2508
|
+
function scrollToTop() {
|
|
2509
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
2510
|
+
}
|
|
2460
2511
|
function toggleTestDetails(testId) {
|
|
2461
2512
|
const details = document.getElementById('details-' + testId);
|
|
2462
2513
|
if (details.style.display === 'none' || details.style.display === '') {
|
|
@@ -2939,9 +2990,26 @@ function drawHistoryChart() {
|
|
|
2939
2990
|
// Initialize charts and filters
|
|
2940
2991
|
document.addEventListener('DOMContentLoaded', function() {
|
|
2941
2992
|
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2993
|
+
// Draw charts
|
|
2994
|
+
drawPieChart();
|
|
2995
|
+
drawHistoryChart();
|
|
2996
|
+
// Add Go to Top button
|
|
2997
|
+
const goTopBtn = document.createElement('button');
|
|
2998
|
+
goTopBtn.innerText = '↑ Top';
|
|
2999
|
+
goTopBtn.id = 'goTopBtn';
|
|
3000
|
+
goTopBtn.style.position = 'fixed';
|
|
3001
|
+
goTopBtn.style.bottom = '30px';
|
|
3002
|
+
goTopBtn.style.right = '30px';
|
|
3003
|
+
goTopBtn.style.zIndex = '9999';
|
|
3004
|
+
goTopBtn.style.padding = '12px 18px';
|
|
3005
|
+
goTopBtn.style.borderRadius = '50%';
|
|
3006
|
+
goTopBtn.style.background = '#27ae60';
|
|
3007
|
+
goTopBtn.style.color = '#fff';
|
|
3008
|
+
goTopBtn.style.fontSize = '20px';
|
|
3009
|
+
goTopBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
|
3010
|
+
goTopBtn.style.cursor = 'pointer';
|
|
3011
|
+
goTopBtn.onclick = scrollToTop;
|
|
3012
|
+
document.body.appendChild(goTopBtn);
|
|
2945
3013
|
|
|
2946
3014
|
// Set up filter event listeners
|
|
2947
3015
|
document.getElementById('statusFilter').addEventListener('change', applyFilters);
|