Skip to content

🐛 Bug: Suite.timeout() propagation misses hooks, chained describe(...).timeout(N) does not apply to hooks #6033

Description

@satouriko

Bug Report Checklist

Expected

Summary

PR #5612 made Suite.prototype.timeout(ms) retroactively propagate the new timeout to already-registered children, fixing #5422. However, it only iterates over this.tests and this.suites — it misses the hook arrays (_beforeAll, _beforeEach, _afterAll, _afterEach).

As a result, the chained form

describe('foo', () => {
  before(async () => { /* slow setup */ });
  it('bar', () => { /* ... */ });
}).timeout(10000);

does not raise the timeout for before (or any other hook). The hooks were created during the describe callback with the suite's timeout at that moment (default 2000ms), and the subsequent .timeout(10000) call never reaches them.

Actual

Which call shapes work / don't work

Form before / after it
describe('x', function () { this.timeout(N); before(...); it(...); }) ✅ works (hook reads suite timeout at _createHook time) ✅ works
describe('x', () => { before(...); it(...); }).timeout(N) (chained, after callback returns) broken — hooks were created with default 2000ms, setter doesn't propagate to them ✅ works (PR #5612)
describe('x', () => { before(function () { this.timeout(N); ... }); }) (per-hook override) ✅ works n/a

Root cause

lib/suite.mjs, Suite.prototype.timeout setter (master, lines 222–233):

this._timeout = parseInt(ms, 10);

// Allow overriding inner/nested suites
// https://github.com/mochajs/mocha/issues/5422
for (const t of this.tests) {
  t.timeout(this._timeout);
}
for (const s of this.suites) {
  s.timeout(this._timeout);
}
return this;

The four hook arrays — this._beforeAll, this._beforeEach, this._afterAll, this._afterEach — are populated by the time the chained .timeout(N) runs (the describe callback already finished), but the setter doesn't visit them.

Minimal, Complete and Verifiable Example

Reproduction

// test/repro.test.js
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// A — BROKEN: chained .timeout() after describe doesn't reach before()
describe('A: chained .timeout() — does it reach before()?', () => {
  before(async () => {
    await sleep(3000); // > default 2000ms hook timeout, < chained 10000ms
  });
  it('reaches this only if before() was rescued by chained .timeout(10000)', () => {});
}).timeout(10000);

// B — OK: chained .timeout() does reach it() (PR #5612)
describe('B: chained .timeout() — does it reach it()?', () => {
  it('it() sleeps 3000ms', async () => {
    await sleep(3000);
  });
}).timeout(10000);

// C — OK: function() form, this.timeout() inside the describe callback
describe('C: function() form, this.timeout() inside', function () {
  this.timeout(10000);
  before(async () => {
    await sleep(3000);
  });
  it('passes', () => {});
});
$ npx mocha test/repro.test.js

  A: chained .timeout() — does it reach before()?
    1) "before all" hook for "reaches this only if ..."

  B: chained .timeout() — does it reach it()?
    ✔ it() sleeps 3000ms (3000ms)

  C: function() form, this.timeout() inside
    ✔ passes


  2 passing (8s)
  1 failing

  1) A: chained .timeout() — does it reach before()?
       "before all" hook for "...":
     Error: Timeout of 2000ms exceeded.

Suggested fix

Add hook propagation alongside tests / suites:

for (const hooks of [this._beforeAll, this._beforeEach, this._afterAll, this._afterEach]) {
  for (const h of hooks) {
    h.timeout(this._timeout);
  }
}

Hooks are Runnable instances and already have a .timeout(ms) setter, so this mirrors the existing tests/suites loops exactly.

Versions

Affects: mocha master (current lib/suite.mjs), confirmed reproducing on 11.7.6.

Environment

  • mocha 11.7.6 (also reproduced against current master lib/suite.mjs)
  • node v24.11.1
  • macOS 25.5.0 (Darwin arm64)

Additional Info

No response

Metadata

Metadata

Assignees

Labels

status: in triagea maintainer should (re-)triage (review) this issuetype: buga defect, confirmed by a maintainer

Type

No type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions