Skip to content

Commit a5c9184

Browse files
committed
feat: cli reporter improvements
1 parent 0db01c6 commit a5c9184

File tree

6 files changed

+413
-84
lines changed

6 files changed

+413
-84
lines changed

lib/command/workers/runTests.js

Lines changed: 216 additions & 72 deletions
Large diffs are not rendered by default.

lib/mocha/cli.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,16 @@ class Cli extends Base {
203203
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} file://${test.file}\n`
204204
}
205205

206+
// Add retry information if this test had retries
207+
if (test.retryHistory && test.retryHistory.length > 0) {
208+
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Retry History:')}`
209+
test.retryHistory.forEach((attempt, idx) => {
210+
const status = attempt.passed ? output.styles.success('✓ passed') : output.styles.error('✗ failed')
211+
log += `\n Attempt ${idx + 1}: ${status} (${attempt.duration}ms)`
212+
})
213+
log += '\n'
214+
}
215+
206216
const steps = test.steps || (test.ctx && test.ctx.test.steps)
207217

208218
if (steps && steps.length) {

lib/mocha/hooks.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,19 @@ class Hook {
3636
return {
3737
hookName: this.hookName,
3838
title: this.title,
39+
suiteName: this.suite?.title || 'unknown',
40+
testName: this.test?.title || this.ctx?.test?.title || 'unknown',
41+
file: this.suite?.file || this.test?.file,
3942
// test: this.test ? serializeTest(this.test) : null,
4043
// suite: this.suite ? serializeSuite(this.suite) : null,
4144
error: this.err ? serializeError(this.err) : null,
4245
}
4346
}
4447

4548
toString() {
46-
return this.hookName
49+
const suiteName = this.suite?.title || ''
50+
const testName = this.test?.title || this.ctx?.test?.title || ''
51+
return `${this.hookName}${suiteName ? ` for "${suiteName}"` : ''}${testName ? ` › "${testName}"` : ''}`
4752
}
4853

4954
toCode() {

lib/mocha/test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,28 @@ function enhanceMochaTest(test) {
3535
test.inject = {}
3636
test.opts = {}
3737
test.meta = {}
38+
test.retryHistory = [] // Track retry attempts
3839

3940
test.notes = []
4041
test.addNote = (type, note) => {
4142
test.notes.push({ type, text: note })
4243
}
4344

45+
/**
46+
* Records a retry attempt
47+
* @param {boolean} passed - Whether the attempt passed
48+
* @param {number} duration - Duration of the attempt in ms
49+
* @param {Error} [error] - Error if the attempt failed
50+
*/
51+
test.recordRetryAttempt = function (passed, duration, error = null) {
52+
test.retryHistory.push({
53+
passed,
54+
duration,
55+
error: error ? error.message : null,
56+
timestamp: new Date().toISOString(),
57+
})
58+
}
59+
4460
// Add new methods
4561
/**
4662
* @param {Mocha.Suite} suite - The Mocha suite to add this test to
@@ -125,6 +141,8 @@ function serializeTest(test, error = null) {
125141
tags: test.tags || [],
126142
uid: test.uid,
127143
retries: test._retries,
144+
retryHistory: test.retryHistory || [],
145+
workerIndex: test.workerIndex,
128146
title: test.title,
129147
state: test.state,
130148
notes: test.notes || [],

lib/output.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,22 @@ module.exports = {
247247
hook: {
248248
started(hook) {
249249
if (outputLevel < 1) return
250-
print(` ${colors.dim.bold(hook.toCode())}`)
250+
// Show more detailed hook information instead of generic "Before" or "After"
251+
const hookType = hook.hookName || 'Hook'
252+
const context = hook.title || (hook.ctx?.test?.title ? `"${hook.ctx.test.title}"` : '')
253+
const displayName = context ? `${hookType} for ${context}` : hookType
254+
print(` ${colors.dim.bold(`${displayName}()`)}`)
251255
},
252256
passed(hook) {
253257
if (outputLevel < 1) return
254258
print()
255259
},
256260
failed(hook) {
257261
if (outputLevel < 1) return
258-
print(` ${colors.red.bold(hook.toCode())}`)
262+
const hookType = hook.hookName || 'Hook'
263+
const context = hook.title || (hook.ctx?.test?.title ? `"${hook.ctx.test.title}"` : '')
264+
const displayName = context ? `${hookType} for ${context}` : hookType
265+
print(` ${colors.red.bold(`${displayName}()`)}`)
259266
},
260267
},
261268

lib/workers.js

Lines changed: 154 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const mkdirp = require('mkdirp')
33
const { Worker } = require('worker_threads')
44
const { EventEmitter } = require('events')
55
const ms = require('ms')
6+
const colors = require('chalk')
67
const Codecept = require('./codecept')
78
const MochaFactory = require('./mocha/factory')
89
const Container = require('./container')
@@ -507,7 +508,10 @@ class Workers extends EventEmitter {
507508

508509
if (message.data.tests) {
509510
message.data.tests.forEach(test => {
510-
Container.result().addTest(deserializeTest(test))
511+
const deserializedTest = deserializeTest(test)
512+
// Add worker index to test for grouping
513+
deserializedTest.workerIndex = message.workerIndex
514+
Container.result().addTest(deserializedTest)
511515
})
512516
}
513517

@@ -516,25 +520,39 @@ class Workers extends EventEmitter {
516520
this.emit(event.suite.before, deserializeSuite(message.data))
517521
break
518522
case event.test.before:
519-
this.emit(event.test.before, deserializeTest(message.data))
523+
const testBefore = deserializeTest(message.data)
524+
testBefore.workerIndex = message.workerIndex
525+
this.emit(event.test.before, testBefore)
520526
break
521527
case event.test.started:
522-
this.emit(event.test.started, deserializeTest(message.data))
528+
const testStarted = deserializeTest(message.data)
529+
testStarted.workerIndex = message.workerIndex
530+
this.emit(event.test.started, testStarted)
523531
break
524532
case event.test.failed:
525-
this.emit(event.test.failed, deserializeTest(message.data))
533+
const testFailed = deserializeTest(message.data)
534+
testFailed.workerIndex = message.workerIndex
535+
this.emit(event.test.failed, testFailed)
526536
break
527537
case event.test.passed:
528-
this.emit(event.test.passed, deserializeTest(message.data))
538+
const testPassed = deserializeTest(message.data)
539+
testPassed.workerIndex = message.workerIndex
540+
this.emit(event.test.passed, testPassed)
529541
break
530542
case event.test.skipped:
531-
this.emit(event.test.skipped, deserializeTest(message.data))
543+
const testSkipped = deserializeTest(message.data)
544+
testSkipped.workerIndex = message.workerIndex
545+
this.emit(event.test.skipped, testSkipped)
532546
break
533547
case event.test.finished:
534-
this.emit(event.test.finished, deserializeTest(message.data))
548+
const testFinished = deserializeTest(message.data)
549+
testFinished.workerIndex = message.workerIndex
550+
this.emit(event.test.finished, testFinished)
535551
break
536552
case event.test.after:
537-
this.emit(event.test.after, deserializeTest(message.data))
553+
const testAfter = deserializeTest(message.data)
554+
testAfter.workerIndex = message.workerIndex
555+
this.emit(event.test.after, testAfter)
538556
break
539557
case event.step.finished:
540558
this.emit(event.step.finished, message.data)
@@ -591,6 +609,10 @@ class Workers extends EventEmitter {
591609
output.process(null)
592610
output.print()
593611

612+
// Group tests by feature for better organization
613+
const testsByFeature = this._groupTestsByFeature(result.tests)
614+
const testsByWorker = this._groupTestsByWorker(result.tests)
615+
594616
this.failuresLog = result.failures
595617
.filter(log => log.length && typeof log[1] === 'number')
596618
// mocha/lib/reporters/base.js
@@ -602,10 +624,133 @@ class Workers extends EventEmitter {
602624
this.failuresLog.forEach(log => output.print(...log))
603625
}
604626

605-
output.result(result.stats.passes, result.stats.failures, result.stats.pending, ms(result.duration), result.stats.failedHooks)
627+
// Print enhanced summary with worker info and feature grouping
628+
this._printEnhancedWorkersSummary(result, testsByFeature, testsByWorker)
606629

607630
process.env.RUNS_WITH_WORKERS = 'false'
608631
}
632+
633+
/**
634+
* Groups tests by their feature/suite name
635+
* @private
636+
*/
637+
_groupTestsByFeature(tests) {
638+
const groups = {}
639+
tests.forEach(test => {
640+
const featureName = test.parent?.title || test.suite || 'Ungrouped Tests'
641+
if (!groups[featureName]) {
642+
groups[featureName] = {
643+
passed: 0,
644+
failed: 0,
645+
skipped: 0,
646+
tests: [],
647+
}
648+
}
649+
groups[featureName].tests.push(test)
650+
if (test.state === 'passed') groups[featureName].passed++
651+
else if (test.state === 'failed') groups[featureName].failed++
652+
else if (test.state === 'skipped' || test.state === 'pending') groups[featureName].skipped++
653+
})
654+
return groups
655+
}
656+
657+
/**
658+
* Groups tests by worker
659+
* @private
660+
*/
661+
_groupTestsByWorker(tests) {
662+
const groups = {}
663+
tests.forEach(test => {
664+
const workerIndex = test.workerIndex || 'unknown'
665+
if (!groups[workerIndex]) {
666+
groups[workerIndex] = {
667+
passed: 0,
668+
failed: 0,
669+
skipped: 0,
670+
tests: [],
671+
}
672+
}
673+
groups[workerIndex].tests.push(test)
674+
if (test.state === 'passed') groups[workerIndex].passed++
675+
else if (test.state === 'failed') groups[workerIndex].failed++
676+
else if (test.state === 'skipped' || test.state === 'pending') groups[workerIndex].skipped++
677+
})
678+
return groups
679+
}
680+
681+
/**
682+
* Prints enhanced summary with worker info, feature grouping and metrics
683+
* @private
684+
*/
685+
_printEnhancedWorkersSummary(result, testsByFeature, testsByWorker) {
686+
// Calculate accurate stats from actual test objects instead of relying on Container stats
687+
// which may not aggregate correctly in all scenarios
688+
let actualPassed = 0
689+
let actualFailed = 0
690+
let actualPending = 0
691+
692+
result.tests.forEach(test => {
693+
if (test.state === 'passed') actualPassed++
694+
else if (test.state === 'failed') actualFailed++
695+
else if (test.state === 'pending' || test.state === 'skipped') actualPending++
696+
})
697+
698+
const actualTotal = result.tests.length
699+
const actualFailedHooks = result.stats?.failedHooks || 0
700+
701+
// Use result.duration (wall-clock time) instead of stats.duration (which gets overwritten)
702+
const duration = result.duration || result.stats?.duration || 0
703+
output.print()
704+
output.print(output.styles.bold('-- TEST SUMMARY:'))
705+
output.print()
706+
707+
// Print tests grouped by feature
708+
if (Object.keys(testsByFeature).length > 0) {
709+
output.print(output.styles.bold('Results by Feature:'))
710+
Object.entries(testsByFeature).forEach(([featureName, data]) => {
711+
const totalTests = data.tests.length
712+
const passRate = totalTests > 0 ? Math.round((data.passed / totalTests) * 100) : 0
713+
const status = data.failed > 0 ? output.styles.error('✗') : output.styles.success('✓')
714+
output.print(` ${status} ${output.styles.bold(featureName)}`)
715+
output.print(` Passed: ${output.styles.success(data.passed)} | Failed: ${output.styles.error(data.failed)} | Skipped: ${data.skipped} | Pass Rate: ${passRate}%`)
716+
})
717+
output.print()
718+
}
719+
720+
// Print worker statistics
721+
if (Object.keys(testsByWorker).length > 1) {
722+
output.print(output.styles.bold('Results by Worker:'))
723+
Object.entries(testsByWorker)
724+
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
725+
.forEach(([workerIndex, data]) => {
726+
const totalTests = data.tests.length
727+
const passRate = totalTests > 0 ? Math.round((data.passed / totalTests) * 100) : 0
728+
const status = data.failed > 0 ? output.styles.error('✗') : output.styles.success('✓')
729+
output.print(` ${status} Worker ${workerIndex}`)
730+
output.print(` Tests: ${totalTests} | Passed: ${output.styles.success(data.passed)} | Failed: ${output.styles.error(data.failed)} | Pass Rate: ${passRate}%`)
731+
})
732+
output.print()
733+
}
734+
735+
// Print overall metrics using accurate counts from test objects
736+
output.print(output.styles.bold('Overall Metrics:'))
737+
const passRate = actualTotal > 0 ? Math.round((actualPassed / actualTotal) * 100) : 0
738+
const failRate = actualTotal > 0 ? Math.round((actualFailed / actualTotal) * 100) : 0
739+
output.print(` Total Tests: ${actualTotal}`)
740+
output.print(` Passed: ${output.styles.success(actualPassed)} (${passRate}%)`)
741+
output.print(` Failed: ${output.styles.error(actualFailed)} (${failRate}%)`)
742+
output.print(` Skipped: ${actualPending}`)
743+
if (actualFailedHooks > 0) {
744+
output.print(` Failed Hooks: ${output.styles.error(actualFailedHooks)}`)
745+
}
746+
output.print(` Duration: ${ms(duration)}`)
747+
output.print(` Workers: ${this.numberOfWorkers}`)
748+
output.print(` Strategy: ${this.isPoolMode ? 'pool' : 'test/suite'}`)
749+
output.print()
750+
751+
// Print the classic result line with accurate counts
752+
output.result(actualPassed, actualFailed, actualPending, ms(duration), actualFailedHooks)
753+
}
609754
}
610755

611756
module.exports = Workers

0 commit comments

Comments
 (0)