@@ -102,7 +102,7 @@ const simplifyObject = object => {
102102 } , { } )
103103}
104104
105- const createWorkerObjects = ( testGroups , config , testRoot , options , selectedRuns ) => {
105+ const createWorkerObjects = ( testGroups , config , testRoot , options , selectedRuns , workerOutputGroups ) => {
106106 selectedRuns = options && options . all && config . multiple ? Object . keys ( config . multiple ) : selectedRuns
107107 if ( selectedRuns === undefined || ! selectedRuns . length || config . multiple === undefined ) {
108108 return testGroups . map ( ( tests , index ) => {
@@ -111,36 +111,23 @@ const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns
111111 workerObj . addTests ( tests )
112112 workerObj . setTestRoot ( testRoot )
113113 workerObj . addOptions ( options )
114+ if ( workerOutputGroups ) {
115+ workerOutputGroups . set ( index , { outputDir : config . output } )
116+ }
114117 return workerObj
115118 } )
116119 }
117120 const workersToExecute = [ ]
121+ const workerOutputDirs = [ ]
118122
119123 const currentOutputFolder = config . output
120- let currentMochawesomeReportDir
121- let currentMochaJunitReporterFile
122-
123- if ( config . mocha && config . mocha . reporterOptions ) {
124- currentMochawesomeReportDir = config . mocha . reporterOptions ?. mochawesome . options . reportDir
125- currentMochaJunitReporterFile = config . mocha . reporterOptions [ 'mocha-junit-reporter' ] . options . mochaFile
126- }
127124
128125 createRuns ( selectedRuns , config ) . forEach ( worker => {
129126 const separator = path . sep
130127 const _config = { ...config }
131128 let workerName = worker . name . replace ( ':' , '_' )
132129 _config . output = `${ currentOutputFolder } ${ separator } ${ workerName } `
133- if ( config . mocha && config . mocha . reporterOptions ) {
134- _config . mocha . reporterOptions . mochawesome . options . reportDir = `${ currentMochawesomeReportDir } ${ separator } ${ workerName } `
135-
136- const _tempArray = currentMochaJunitReporterFile . split ( separator )
137- _tempArray . splice (
138- _tempArray . findIndex ( item => item . includes ( '.xml' ) ) ,
139- 0 ,
140- workerName ,
141- )
142- _config . mocha . reporterOptions [ 'mocha-junit-reporter' ] . options . mochaFile = _tempArray . join ( separator )
143- }
130+ workerOutputDirs . push ( _config . output )
144131 workerName = worker . getOriginalName ( ) || worker . getName ( )
145132 const workerConfig = worker . getConfig ( )
146133 workersToExecute . push ( getOverridenConfig ( workerName , workerConfig , _config ) )
@@ -149,12 +136,16 @@ const createWorkerObjects = (testGroups, config, testRoot, options, selectedRuns
149136 let index = 0
150137 testGroups . forEach ( tests => {
151138 const testWorkerArray = [ ]
152- workersToExecute . forEach ( finalConfig => {
153- const workerObj = new WorkerObject ( index ++ )
139+ workersToExecute . forEach ( ( finalConfig , runIndex ) => {
140+ const workerObj = new WorkerObject ( index )
154141 workerObj . addConfig ( finalConfig )
155142 workerObj . addTests ( tests )
156143 workerObj . setTestRoot ( testRoot )
157144 workerObj . addOptions ( options )
145+ if ( workerOutputGroups ) {
146+ workerOutputGroups . set ( index , { outputDir : workerOutputDirs [ runIndex ] } )
147+ }
148+ index ++
158149 testWorkerArray . push ( workerObj )
159150 } )
160151 workers . push ( ...testWorkerArray )
@@ -293,6 +284,9 @@ class Workers extends EventEmitter {
293284 this . isPoolMode = config . by === 'pool'
294285 this . activeWorkers = new Map ( )
295286 this . maxWorkers = numberOfWorkers // Track original worker count for pool mode
287+ this . _savedReporterConfig = null
288+ this . _workerOutputGroups = new Map ( )
289+ this . _reporterTests = [ ]
296290
297291 createOutputDir ( config . testConfig )
298292 // Defer worker initialization until codecept is ready
@@ -316,7 +310,36 @@ class Workers extends EventEmitter {
316310 this . splitTestsByGroups ( numberOfWorkers , config )
317311 // For function-based grouping, use the actual number of test groups created
318312 const actualNumberOfWorkers = isFunction ( config . by ) ? this . testGroups . length : numberOfWorkers
319- this . workers = createWorkerObjects ( this . testGroups , this . codecept . config , getTestRoot ( config . testConfig ) , config . options , config . selectedRuns )
313+
314+ let codeceptConfig = this . codecept . config
315+ if ( codeceptConfig . mocha && codeceptConfig . mocha . reporter ) {
316+ const testRoot = getTestRoot ( config . testConfig )
317+ const resolvedOutput = path . isAbsolute ( codeceptConfig . output ) ? codeceptConfig . output : path . join ( testRoot , codeceptConfig . output )
318+ const reporterOpts = codeceptConfig . mocha . reporterOptions ? deepClone ( codeceptConfig . mocha . reporterOptions ) : { }
319+ const pathKeys = [ 'reportDir' , 'output' , 'mochaFile' , 'stdout' ]
320+ const resolvePathsDeep = ( obj ) => {
321+ if ( ! obj || typeof obj !== 'object' ) return
322+ for ( const key of Object . keys ( obj ) ) {
323+ if ( pathKeys . includes ( key ) && typeof obj [ key ] === 'string' && ! path . isAbsolute ( obj [ key ] ) ) {
324+ obj [ key ] = path . join ( testRoot , obj [ key ] )
325+ } else if ( typeof obj [ key ] === 'object' && obj [ key ] !== null ) {
326+ resolvePathsDeep ( obj [ key ] )
327+ }
328+ }
329+ }
330+ resolvePathsDeep ( reporterOpts )
331+ this . _savedReporterConfig = {
332+ reporter : codeceptConfig . mocha . reporter ,
333+ reporterOptions : reporterOpts ,
334+ outputDir : resolvedOutput ,
335+ }
336+ codeceptConfig = deepClone ( codeceptConfig )
337+ codeceptConfig . mocha . reporter = null
338+ codeceptConfig . mocha . reporterOptions = { }
339+ codeceptConfig . output = resolvedOutput
340+ }
341+
342+ this . workers = createWorkerObjects ( this . testGroups , codeceptConfig , getTestRoot ( config . testConfig ) , config . options , config . selectedRuns , this . _workerOutputGroups )
320343 this . numberOfWorkers = this . workers . length
321344 }
322345
@@ -618,8 +641,16 @@ class Workers extends EventEmitter {
618641 }
619642
620643 if ( message . data . tests ) {
644+ const outputGroup = this . _workerOutputGroups . get ( message . workerIndex - 1 )
621645 message . data . tests . forEach ( test => {
622- Container . result ( ) . addTest ( deserializeTest ( test ) )
646+ const deserialized = deserializeTest ( test )
647+ if ( outputGroup ) {
648+ deserialized . _outputGroup = outputGroup
649+ }
650+ Container . result ( ) . addTest ( deserialized )
651+ if ( this . _savedReporterConfig ) {
652+ this . _reporterTests . push ( deserialized )
653+ }
623654 } )
624655 }
625656
@@ -777,7 +808,142 @@ class Workers extends EventEmitter {
777808
778809 this . emit ( event . all . result , Container . result ( ) )
779810 event . dispatcher . emit ( event . workers . result , Container . result ( ) )
780- this . emit ( 'end' ) // internal event
811+
812+ this . _runReporters ( ) . then ( ( ) => {
813+ this . emit ( 'end' )
814+ } ) . catch ( err => {
815+ output . error ( `Reporter error: ${ err . message } ` )
816+ this . emit ( 'end' )
817+ } )
818+ }
819+
820+ async _runReporters ( ) {
821+ if ( ! this . _savedReporterConfig ) return
822+
823+ const { reporter : reporterName , reporterOptions : savedReporterOptions , outputDir : defaultOutputDir } = this . _savedReporterConfig
824+
825+ let ReporterClass
826+ try {
827+ if ( typeof reporterName === 'function' ) {
828+ ReporterClass = reporterName
829+ } else {
830+ try {
831+ const mod = await import ( reporterName )
832+ ReporterClass = mod . default || mod
833+ } catch {
834+ const { createRequire } = await import ( 'module' )
835+ const require = createRequire ( import . meta. url )
836+ ReporterClass = require ( reporterName )
837+ }
838+ }
839+ } catch ( err ) {
840+ output . error ( `Could not load reporter "${ reporterName } ": ${ err . message } ` )
841+ return
842+ }
843+
844+ const createStatsCollector = ( await import ( 'mocha/lib/stats-collector.js' ) ) . default
845+ const MochaSuite = ( await import ( 'mocha/lib/suite.js' ) ) . default
846+ const MochaTest = ( await import ( 'mocha/lib/test.js' ) ) . default
847+
848+ const tests = this . _reporterTests
849+ const groups = new Map ( )
850+
851+ for ( const test of tests ) {
852+ const groupKey = test . _outputGroup ?. outputDir || defaultOutputDir
853+ if ( ! groups . has ( groupKey ) ) {
854+ groups . set ( groupKey , { tests : [ ] , outputDir : groupKey } )
855+ }
856+ groups . get ( groupKey ) . tests . push ( test )
857+ }
858+
859+ for ( const [ , group ] of groups ) {
860+ try {
861+ const rootSuite = new MochaSuite ( '' , null , true )
862+ rootSuite . root = true
863+
864+ const suiteMap = new Map ( )
865+ for ( const test of group . tests ) {
866+ const suiteTitle = test . parent ?. title || 'Suite'
867+ if ( ! suiteMap . has ( suiteTitle ) ) {
868+ suiteMap . set ( suiteTitle , [ ] )
869+ }
870+ suiteMap . get ( suiteTitle ) . push ( test )
871+ }
872+
873+ for ( const [ suiteTitle , suiteTests ] of suiteMap ) {
874+ const childSuite = MochaSuite . create ( rootSuite , suiteTitle )
875+ childSuite . root = false
876+ for ( const test of suiteTests ) {
877+ const mochaTest = new MochaTest ( test . title , ( ) => { } )
878+ mochaTest . state = test . state
879+ mochaTest . duration = test . duration || 0
880+ mochaTest . speed = test . duration > 75 ? 'slow' : ( test . duration > 37 ? 'medium' : 'fast' )
881+ mochaTest . file = test . file
882+ mochaTest . pending = test . state === 'pending' || test . state === 'skipped'
883+ if ( test . err ) {
884+ const err = new Error ( test . err . message || '' )
885+ err . stack = test . err . stack || ''
886+ err . name = test . err . name || 'Error'
887+ err . actual = test . err . actual
888+ err . expected = test . err . expected
889+ mochaTest . err = err
890+ }
891+ childSuite . addTest ( mochaTest )
892+ }
893+ }
894+
895+ let reporterOptions = savedReporterOptions ? deepClone ( savedReporterOptions ) : { }
896+ if ( group . outputDir && group . outputDir !== defaultOutputDir ) {
897+ mkdirp . sync ( group . outputDir )
898+ reporterOptions = replaceValueDeep ( reporterOptions , 'reportDir' , group . outputDir )
899+ reporterOptions = replaceValueDeep ( reporterOptions , 'output' , group . outputDir )
900+ reporterOptions = replaceValueDeep ( reporterOptions , 'mochaFile' , path . join ( group . outputDir , 'report.xml' ) )
901+ reporterOptions = replaceValueDeep ( reporterOptions , 'stdout' , path . join ( group . outputDir , 'console.log' ) )
902+ }
903+
904+ const runner = new EventEmitter ( )
905+ runner . suite = rootSuite
906+ runner . total = group . tests . length
907+ runner . failures = 0
908+ createStatsCollector ( runner )
909+
910+ const cliKey = 'codeceptjs-cli-reporter'
911+ const internalCliKey = 'codeceptjs/lib/mocha/cli'
912+ const filteredOptions = { ...reporterOptions }
913+ delete filteredOptions [ cliKey ]
914+ delete filteredOptions [ internalCliKey ]
915+
916+ const reporterInstance = new ReporterClass ( runner , { reporterOption : filteredOptions , reporterOptions : filteredOptions } )
917+
918+ runner . emit ( 'start' )
919+
920+ for ( const childSuite of rootSuite . suites ) {
921+ runner . emit ( 'suite' , childSuite )
922+ for ( const test of childSuite . tests ) {
923+ runner . emit ( 'test' , test )
924+ if ( test . pending ) {
925+ runner . emit ( 'pending' , test )
926+ } else if ( test . state === 'passed' ) {
927+ runner . emit ( 'pass' , test )
928+ } else if ( test . state === 'failed' ) {
929+ runner . emit ( 'fail' , test , test . err )
930+ }
931+ runner . emit ( 'test end' , test )
932+ }
933+ runner . emit ( 'suite end' , childSuite )
934+ }
935+
936+ runner . emit ( 'end' )
937+
938+ if ( typeof reporterInstance . done === 'function' ) {
939+ await new Promise ( ( resolve ) => {
940+ reporterInstance . done ( runner . failures || 0 , resolve )
941+ } )
942+ }
943+ } catch ( err ) {
944+ output . error ( `Reporter error: ${ err . message } ` )
945+ }
946+ }
781947 }
782948
783949 printResults ( ) {
0 commit comments