Repository navigation
4.x: junitReporter doesn't record Before/After suite failures #5645
Description
Activity
junitReporterseems to lose useful JUnit error/failure output whenBeforeSuiteorAfterSuitehooks fail.A reporter regression there should make the formatter change concrete.
In our project we work around this with a pnpm patch on codeceptjs@4.0.8. It hooks into Mocha's runner fail event (the only signal that fires for all hook failures, including BeforeSuite/AfterSuite) and injects synthetic failed entries for suite-level hook failures before the JUnit XML is built. Per-test before each/after each hooks are intentionally excluded since those already appear in result.tests.
But I don't know if such solution can be used for a PR.
diff --git a/lib/plugin/junitReporter.js b/lib/plugin/junitReporter.js index 179f285605ebddb77a040d058b4af084f1c78cf5..0533121568910f60b11c22b57969e66704107f01 100644 --- a/lib/plugin/junitReporter.js +++ b/lib/plugin/junitReporter.js @@ -7,6 +7,7 @@ import { DOMImplementation, XMLSerializer } from '@xmldom/xmldom' import event from '../event.js' import store from '../store.js' import output from '../output.js' +import container from '../container.js' const defaultConfig = { outputName: 'report.xml', @@ -62,11 +63,67 @@ const INVALID_XML_CHARS = new RegExp('[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u0 * * @param {*} config */ +// Matches Mocha's suite-level hook titles, e.g. `"before all" hook: BeforeSuite for "..."` +// or `"after all" hook: codeceptjs.afterSuite for "..."`. Per-test (`before each` / +// `after each`) hooks are intentionally excluded — those tests already live in result.tests. +const SUITE_HOOK_TITLE = /^"(before all|after all)" hook:/ + export default function (config = {}) { config = Object.assign({}, defaultConfig, config) let written = false + // Capture suite-level hook failures so they surface as a synthetic failed <testcase>. + // The Mocha runner's `fail` event is the only signal that fires for *all* hook failures: + // both test-file BeforeSuite/AfterSuite hooks AND helper-lifecycle hooks + // (_beforeSuite/_afterSuite). The latter report only through Mocha — they emit no + // event.test.failed / event.hook.failed on the CodeceptJS bus and never enter + // result.tests — so without this they are invisible in the JUnit report + // (regression vs. the 3.x mocha-junit-reporter, which reported the hook as a test). + const hookFailures = [] + const seenHookFailures = new Set() + let runnerAttached = false + + const attachRunner = () => { + if (runnerAttached) return + let mocha + try { + mocha = container.mocha() + } catch (err) { + return // container/mocha not ready yet + } + const runner = mocha && (mocha.runner || mocha.Runner) + if (!runner || typeof runner.on !== 'function') return + runnerAttached = true + runner.on('fail', (failed, err) => { + // Only suite-level hooks; real test failures already live in result.tests. + if (!failed || failed.type !== 'hook' || !SUITE_HOOK_TITLE.test(failed.title || '')) return + const suite = failed.parent + const suiteTitle = (suite && suite.title) || '' + const key = `${suiteTitle}::${failed.title}` + if (seenHookFailures.has(key)) return // Mocha fires one fail per hook; guard re-entry + seenHookFailures.add(key) + + hookFailures.push({ + title: failed.title || 'hook failed', + state: 'failed', + err: err || failed.err || {}, + parent: suite, + file: failed.file || (suite && suite.file), + tags: suiteTitle.match(/@[\w-]+/g) || [], + meta: {}, + steps: [], + duration: failed.duration || 0, + }) + }) + } + + // The Mocha runner is created right after event.all.before; attach on the first event + // where it exists — well before any suite hook can fail. + event.dispatcher.on(event.all.before, attachRunner) + event.dispatcher.on(event.suite.before, attachRunner) + event.dispatcher.on(event.test.before, attachRunner) + const writeReport = result => { if (written) return if (!result || !Array.isArray(result.tests)) return @@ -76,7 +133,7 @@ export default function (config = {}) { mkdirp.sync(dir) const file = path.join(dir, config.outputName) - fs.writeFileSync(file, buildXml(result, config)) + fs.writeFileSync(file, buildXml(result, config, hookFailures)) output.plugin('junitReporter', `JUnit report saved to ${file}`) } @@ -84,17 +141,18 @@ export default function (config = {}) { event.dispatcher.on(event.workers.result, writeReport) } -function buildXml(result, config) { +function buildXml(result, config, hookFailures = []) { const doc = new DOMImplementation().createDocument(null, null, null) - const suites = groupBySuite(result.tests) + const allTests = result.tests.concat(hookFailures) + const suites = groupBySuite(allTests) const root = doc.createElement('testsuites') setAttr(root, 'name', config.testGroupName) - setAttr(root, 'tests', result.tests.length) - setAttr(root, 'failures', countState(result.tests, 'failed')) - setAttr(root, 'skipped', countSkipped(result.tests)) + setAttr(root, 'tests', allTests.length) + setAttr(root, 'failures', countState(allTests, 'failed')) + setAttr(root, 'skipped', countSkipped(allTests)) setAttr(root, 'errors', 0) - setAttr(root, 'time', toSeconds(sumDuration(result.tests))) + setAttr(root, 'time', toSeconds(sumDuration(allTests))) setAttr(root, 'timestamp', toIso(result.stats && result.stats.start)) doc.appendChild(root)
Applied via pnpm patch codeceptjs@4.0.8 in our repo and confirmed it produces a failed entry for both BeforeSuite/AfterSuite failures and helper _beforeSuite/_afterSuite failures, without touching per-test hooks that were already reported correctly.
- added a commit that references this issue
on Jul 6, 2026 @DavertMik Your fix covers only
BeforeSuite()andAferSuite(), but not_beforeSuite()and_afterSuite()(i.e. helper methods).
E.g. when a test fails in_beforeSuite()thenoutput/report.xmlremains empty 🐛 .@DavertMik Your fix covers only
BeforeSuite()andAferSuite(), but not_beforeSuite()and_afterSuite()(i.e. helper methods). E.g. when a test fails in_beforeSuite()thenoutput/report.xmlremains empty 🐛 .I created #5660
- added a commit that references this issue
on Sep 8, 2026
In CodeceptJS 3.x there could be the "mocha-junit-reporter" configured in a CodeceptJS config in "mocha" section.
If the test failed in
BeforeSuite(),Scenario()orAfterSuite()I could see the failure, typically in Jenkins's JUnit test result report plugin ✔️For example:
In CodeceptJS 4.x, it was replaced by the plugin "junitReporter". It doesn't record any failure when the test fails in
BeforeSuite()orAfterSuite()🐛 . It records onlyScenario()failures.tests/My_test.ts:
codecept.conf.ts
output:
output/report.xml: