From 65d4ba819a4ca95d7a76a83f41643cbf12c5b3c9 Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Wed, 21 Oct 2026 15:10:22 +0200 Subject: [PATCH] fix(junitReporter): group run-workers tests into one testsuite per suite Under run-workers each test arrives in the main process with its own deserialized parent, so grouping by object identity put every test into its own . Group by suite title and file instead, and keep the suite file when serializing tests so worker suite hook failures land in the same as their tests. Fixes #5760 Co-Authored-By: Claude Opus 5.5 --- lib/mocha/test.js | 1 + lib/plugin/junitReporter.js | 12 ++-------- test/unit/junitReporter_test.js | 39 +++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/lib/mocha/test.js b/lib/mocha/test.js index f85c070ef..cf012fb0b 100644 --- a/lib/mocha/test.js +++ b/lib/mocha/test.js @@ -106,6 +106,7 @@ function serializeTest(test, error = null) { const parent = {} if (test.parent) { parent.title = test.parent.title + parent.file = test.parent.file } if (test.opts) { diff --git a/lib/plugin/junitReporter.js b/lib/plugin/junitReporter.js index 2e84d2076..f6fcbf57d 100644 --- a/lib/plugin/junitReporter.js +++ b/lib/plugin/junitReporter.js @@ -67,10 +67,6 @@ export default function (config = {}) { let written = false const hookFailures = [] - // groupBySuite() (below) groups by object identity, not by value — reused - // across BeforeSuite/AfterSuite failures from the same worker-forwarded - // suite so they land in one , not one per failure. - const workerSuiteByKey = new Map() event.dispatcher.on(event.hook.failed, hook => { if (!hook || !['BeforeSuite', 'AfterSuite'].includes(hook.hookName)) return @@ -84,11 +80,7 @@ export default function (config = {}) { // groups under its real suite instead of the "Tests" fallback. let suite = runnable && runnable.parent if (!suite && hook.suiteTitle) { - const key = `${hook.suiteTitle} ${hook.suiteFile || ''}` - if (!workerSuiteByKey.has(key)) { - workerSuiteByKey.set(key, { title: hook.suiteTitle, file: hook.suiteFile, tags: hook.suiteTags }) - } - suite = workerSuiteByKey.get(key) + suite = { title: hook.suiteTitle, file: hook.suiteFile, tags: hook.suiteTags } } hookFailures.push({ title: hook.title || `${hook.hookName} hook failed`, @@ -296,7 +288,7 @@ function groupBySuite(tests) { const groups = [] const byKey = new Map() for (const test of tests) { - const key = test.parent || test + const key = test.parent ? `${test.parent.title}\0${test.parent.file || test.file || ''}` : test if (!byKey.has(key)) { const list = [] byKey.set(key, list) diff --git a/test/unit/junitReporter_test.js b/test/unit/junitReporter_test.js index feef9efbc..b4a56625a 100644 --- a/test/unit/junitReporter_test.js +++ b/test/unit/junitReporter_test.js @@ -231,6 +231,45 @@ describe('JUnit Reporter Plugin', () => { expect(tagsProp.$.value).to.equal('@smoke') }) + it('groups worker tests with separately deserialized parents into one testsuite per suite', async () => { + junitReporter({}) + + const workerTest = (suiteTitle, title) => ({ title, state: 'passed', duration: 10, parent: { title: suiteTitle }, meta: {}, steps: [] }) + + event.dispatcher.emit(event.workers.result, { + tests: [workerTest('SuiteB', 'scenario 1'), workerTest('SuiteB', 'scenario 2'), workerTest('SuiteA', 'scenario 1'), workerTest('SuiteA', 'scenario 2')], + stats: { start: new Date(), passes: 4, failures: 0, pending: 0, tests: 4 }, + }) + + const parsed = await parseReport(tmpDir) + const suites = parsed.testsuites.testsuite + expect(suites.map(s => s.$.name)).to.deep.equal(['SuiteB', 'SuiteA']) + expect(suites.map(s => s.$.tests)).to.deep.equal(['2', '2']) + }) + + it('groups a worker suite hook failure with the tests of the same suite', async () => { + junitReporter({}) + + event.dispatcher.emit(event.hook.failed, { + hookName: 'AfterSuite', + title: '"after all" hook: AfterSuite for "scenario 1"', + suiteTitle: 'SuiteC', + suiteFile: '/tests/suite_c_test.js', + error: { message: 'AfterSuite failed' }, + }) + + event.dispatcher.emit(event.workers.result, { + tests: [{ title: 'scenario 1', state: 'passed', duration: 10, parent: { title: 'SuiteC', file: '/tests/suite_c_test.js' }, meta: {}, steps: [] }], + stats: { start: new Date(), passes: 1, failures: 0, pending: 0, tests: 1 }, + }) + + const parsed = await parseReport(tmpDir) + const suites = parsed.testsuites.testsuite + expect(suites).to.have.length(1) + expect(suites[0].$.name).to.equal('SuiteC') + expect(suites[0].$.tests).to.equal('2') + }) + it('adds suite hook failures forwarded from workers as failed testcases', async () => { junitReporter({})