Skip to content

Commit d2f7a2a

Browse files
authored
fix(hooks): emit hook.failed when a helper suite hook throws (#5684)
A custom helper's `_beforeSuite()` / `_afterSuite()` is queued on the recorder from the `event.suite.before` / `event.suite.after` listeners in lib/listener/helpers.js. That path runs inside suiteSetup/suiteTeardown, not inside the `injected()` wrapper, and only `injected()` calls `fireHook()`. So a failing helper lifecycle method rejected the mocha hook without ever emitting `event.hook.failed`, and reporters that listen for it, junitReporter among them, recorded nothing. Both error handlers now emit the matching hook object before calling done, so `hookName` reads BeforeSuite or AfterSuite exactly as it does for the test-file-defined hooks. Closes #5660
1 parent 7db4946 commit d2f7a2a

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

lib/mocha/asyncWrapper.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import recorder from '../recorder.js'
44
import assertThrown from '../assert/throws.js'
55
import { ucfirst, isAsyncFunction } from '../utils.js'
66
import { getInjectedArguments } from './inject.js'
7-
import { fireHook } from './hooks.js'
7+
import { fireHook, BeforeSuiteHook, AfterSuiteHook } from './hooks.js'
88

99
const injectHook = function (inject, suite) {
1010
try {
@@ -232,6 +232,10 @@ export function suiteSetup(suite) {
232232

233233
// Set up error handler for suite setup
234234
recorder.errHandler(err => {
235+
// A helper's `_beforeSuite()` runs through this hook, not through the
236+
// `injected()` wrapper, so nothing here used to emit `hook.failed` and
237+
// reporters listening for it never saw the failure. (#5660)
238+
event.emit(event.hook.failed, new BeforeSuiteHook(suite, err))
235239
doneFn(err)
236240
})
237241

@@ -254,6 +258,8 @@ export function suiteTeardown(suite) {
254258

255259
// Set up error handler for suite teardown
256260
recorder.errHandler(err => {
261+
// Same for a helper's `_afterSuite()`. (#5660)
262+
event.emit(event.hook.failed, new AfterSuiteHook(suite, err))
257263
doneFn(err)
258264
})
259265

test/unit/mocha/asyncWrapper_test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,48 @@ describe('AsyncWrapper', () => {
269269
expect(arg, 'done called with no error').to.be.undefined
270270
})
271271
})
272+
describe('helper lifecycle hook failures (#5660)', () => {
273+
beforeEach(() => recorder.start())
274+
275+
// A helper's _beforeSuite()/_afterSuite() is queued on the recorder from an
276+
// event.suite.before/after listener (lib/listener/helpers.js), not through
277+
// the injected() wrapper, so it lands in suiteSetup/suiteTeardown's
278+
// errHandler rather than in the path that fires hook.failed.
279+
function queueFailingHelperHook(evt, message) {
280+
event.dispatcher.on(evt, () => {
281+
recorder.add(`hook MyHelper.${message}()`, () => {
282+
throw new Error(message)
283+
})
284+
recorder.catch()
285+
})
286+
}
287+
288+
it('suiteSetup(): a failing helper _beforeSuite emits hook.failed', async () => {
289+
const failed = sinon.spy()
290+
event.dispatcher.on(event.hook.failed, failed)
291+
queueFailingHelperHook(event.suite.before, '_beforeSuite')
292+
293+
const suite = { title: 'Login', ctx: { test: { title: 'codeceptjs.beforeSuite' } } }
294+
const { arg } = await runHook(suiteSetup(suite), 2000)
295+
296+
expect(arg).to.be.instanceof(Error)
297+
expect(arg.message).to.equal('_beforeSuite')
298+
expect(failed.called, 'hook.failed was emitted').to.be.true
299+
expect(failed.firstCall.args[0].hookName).to.equal('BeforeSuite')
300+
expect(failed.firstCall.args[0].err).to.equal(arg)
301+
})
302+
303+
it('suiteTeardown(): a failing helper _afterSuite emits hook.failed', async () => {
304+
const failed = sinon.spy()
305+
event.dispatcher.on(event.hook.failed, failed)
306+
queueFailingHelperHook(event.suite.after, '_afterSuite')
307+
308+
const suite = { title: 'Login', ctx: { test: { title: 'codeceptjs.afterSuite' } } }
309+
const { arg } = await runHook(suiteTeardown(suite), 2000)
310+
311+
expect(arg).to.be.instanceof(Error)
312+
expect(failed.called, 'hook.failed was emitted').to.be.true
313+
expect(failed.firstCall.args[0].hookName).to.equal('AfterSuite')
314+
})
315+
})
272316
})

0 commit comments

Comments
 (0)