Skip to content

4.x: junitReporter doesn't record Before/After suite failures #5645

Description

@mirao

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() or AfterSuite() I could see the failure, typically in Jenkins's JUnit test result report plugin ✔️

For example:

Image

In CodeceptJS 4.x, it was replaced by the plugin "junitReporter". It doesn't record any failure when the test fails in BeforeSuite() or AfterSuite() 🐛 . It records only Scenario() failures.

tests/My_test.ts:

Feature('My');

BeforeSuite(async ({ I }) => {
    I.say("Before Suite");
    throw new Error("Before Suite Error");
});

Scenario('test something',  ({ I }) => {
    I.amOnPage("https://example.com");
});

AfterSuite(async ({ I }) => {
    I.say("After Suite");
    throw new Error("After Suite Error");
});

codecept.conf.ts

import { setHeadlessWhen, setCommonPlugins } from "@codeceptjs/configure";
// turn on headless mode when running with HEADLESS=true environment variable
// export HEADLESS=true && npx codeceptjs run
setHeadlessWhen(process.env.HEADLESS);

// enable all common plugins https://github.com/codeceptjs/configure#setcommonplugins
setCommonPlugins();

export const config: CodeceptJS.MainConfig = {
  tests: "./tests/*_test.ts",
  output: "./output",
  helpers: {
    Playwright: {
      browser: "chromium",
      url: "http://localhost",
      show: true,
    },
  },
  include: {
    I: "./steps_file.ts",
  },
  noGlobals: true,
  plugins: {
    junitReporter: {
      enabled: true,
    },
  },
  name: "my",
  require: ["tsx/cjs"],
};

output:

mirao@rog:~/workspace/my$ codeceptjs run --verbose
***************************************
nodeInfo:  24.16.0
osInfo:  Linux 6.17 Ubuntu 24.04.4 LTS 24.04.4 LTS (Noble Numbat)
cpuInfo:  (16) x64 AMD Ryzen 7 9700X 8-Core Processor
chromeInfo:  149.0.7827.155
edgeInfo:  "N/A"
firefoxInfo:  152.0
safariInfo:  N/A
playwrightBrowsers:  "chromium: 149.0.7827.55, firefox: 151.0, webkit: 26.5"
If you need more detailed info, just run this: npx codeceptjs info
***************************************
CodeceptJS v4.0.8 #StandWithUkraine
Using test root "/home/mirao/workspace/my"
Helpers: Playwright
Plugins: screenshot, junitReporter, retryFailedStep

My --
/home/mirao/workspace/my/tests/My_test.ts
    [1]  Starting recording promises
    Timeouts: 
 › [Session] Starting singleton browser session
  BeforeSuite()
    I say "Before Suite"
   Before Suite
    [1] Error | Error: Before Suite Error e => { const err = recorder.getAsyncErr() === null...
    [1] <teardown>  Stopping recording promises
  ✖ FAILED in 4ms

  AfterSuite()
    [2]  Starting recording promises
    I say "After Suite"
   After Suite
    [2] Error | Error: After Suite Error e => { const err = recorder.getAsyncErr() === null...
    [2] <teardown>  Stopping recording promises
  ✖ FAILED in 1ms


-- FAILURES:

  1) My
       "before all" hook: BeforeSuite for "test something":
     
  Before Suite Error
      at Context.<anonymous> (tests/My_test.ts:5:11)
      at promiseRetry.retries.retries (file:///home/mirao/workspace/my/node_modules/codeceptjs/lib/mocha/asyncWrapper.js:162:20)
  
  
  ◯ File: file:///home/mirao/workspace/my/tests/My_test.ts
  

  2) My
       "after all" hook: AfterSuite for "test something":
     
  After Suite Error
      at Context.<anonymous> (tests/My_test.ts:14:11)
      at promiseRetry.retries.retries (file:///home/mirao/workspace/my/node_modules/codeceptjs/lib/mocha/asyncWrapper.js:162:20)
      at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
  
  
  ◯ File: file:///home/mirao/workspace/my/tests/My_test.ts
  


  FAIL  | 0 passed, 2 failed, 2 failedHooks   // 123ms
 › <junitReporter> JUnit report saved to /home/mirao/workspace/my/output/report.xml

output/report.xml:

<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="CodeceptJS" tests="0" failures="0" skipped="0" errors="0" time="0.000" timestamp="2026-06-19T11:19:00.325Z"/>

Activity

  1. morgan-coded commented on Jul 2, 2026

    @morgan-coded

    junitReporter seems to lose useful JUnit error/failure output when BeforeSuite or AfterSuite hooks fail.

    A reporter regression there should make the formatter change concrete.

  2. mirao commented on Jul 3, 2026

    @mirao
    ContributorAuthor

    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.

  3. mirao commented on Jul 8, 2026

    @mirao
    ContributorAuthor

    @DavertMik Your fix covers only BeforeSuite() and AferSuite(), but not _beforeSuite() and _afterSuite() (i.e. helper methods).
    E.g. when a test fails in _beforeSuite() then output/report.xml remains empty 🐛 .

  4. mirao commented on Jul 8, 2026

    @mirao
    ContributorAuthor

    @DavertMik Your fix covers only BeforeSuite() and AferSuite(), but not _beforeSuite() and _afterSuite() (i.e. helper methods). E.g. when a test fails in _beforeSuite() then output/report.xml remains empty 🐛 .

    I created #5660

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions