diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index 3393e3369..ded7f7b42 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -33,12 +33,22 @@ class Hook { } simplify() { + // this.runnable (context.ctx.test) is the hook's own runnable; its + // .parent is the real Mocha Suite that owns this hook. Included here so + // run-workers can forward suite identity to the main process, where + // listeners (e.g. junitReporter) have no other way to recover it — the + // worker's live Suite/ctx objects aren't serializable across the thread + // boundary, only these plain fields are. + const suite = this.runnable?.parent return { hookName: this.hookName, title: this.title, // test: this.test ? serializeTest(this.test) : null, // suite: this.suite ? serializeSuite(this.suite) : null, error: this.err ? serializeError(this.err) : null, + suiteTitle: suite?.title || null, + suiteFile: suite?.file || null, + suiteTags: suite?.tags || [], } } diff --git a/lib/plugin/junitReporter.js b/lib/plugin/junitReporter.js index 0fac0a8a8..a3f104e5d 100644 --- a/lib/plugin/junitReporter.js +++ b/lib/plugin/junitReporter.js @@ -67,13 +67,29 @@ 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 const err = hook.err || hook.error if (!err) return const runnable = hook.ctx && hook.ctx.test - const suite = runnable && runnable.parent + // Under run-workers, hook.ctx is absent — the failure arrives as the + // plain object from Hook.simplify() in the main process instead of a + // live Hook instance. Fall back to the suiteTitle/suiteFile/suiteTags + // simplify() carries across the worker boundary so the failure still + // 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) + } hookFailures.push({ title: hook.title || `${hook.hookName} hook failed`, state: 'failed',