Skip to content

Commit b5d8eea

Browse files
committed
feat: detect illegally nested tests (#4525)
Throws if there is an it() call inside an it() call. Same for test().
1 parent 6ee5b48 commit b5d8eea

File tree

7 files changed

+287
-0
lines changed

7 files changed

+287
-0
lines changed

lib/interfaces/bdd.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
var Test = require('../test');
44
var EVENT_FILE_PRE_REQUIRE =
55
require('../suite').constants.EVENT_FILE_PRE_REQUIRE;
6+
var errors = require('../errors');
7+
var createUnsupportedError = errors.createUnsupportedError;
68

79
/**
810
* BDD-style interface:
@@ -84,6 +86,18 @@ module.exports = function bddInterface(suite) {
8486
if (suite.isPending()) {
8587
fn = null;
8688
}
89+
90+
// Check for nested test registration
91+
if (global._mochaExecutionState && global._mochaExecutionState.currentRunnable) {
92+
var currentRunnable = global._mochaExecutionState.currentRunnable;
93+
if (currentRunnable.type === 'test') {
94+
throw createUnsupportedError(
95+
'Nested test "' + title + '" detected inside test "' + currentRunnable.title + '". ' +
96+
'Nested tests are not allowed.'
97+
);
98+
}
99+
}
100+
87101
var test = new Test(title, fn);
88102
test.file = file;
89103
suite.addTest(test);

lib/interfaces/tdd.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
var Test = require('../test');
44
var EVENT_FILE_PRE_REQUIRE =
55
require('../suite').constants.EVENT_FILE_PRE_REQUIRE;
6+
var errors = require('../errors');
7+
var createUnsupportedError = errors.createUnsupportedError;
68

79
/**
810
* TDD-style interface:
@@ -84,6 +86,18 @@ module.exports = function (suite) {
8486
if (suite.isPending()) {
8587
fn = null;
8688
}
89+
90+
// Check for nested test registration
91+
if (global._mochaExecutionState && global._mochaExecutionState.currentRunnable) {
92+
var currentRunnable = global._mochaExecutionState.currentRunnable;
93+
if (currentRunnable.type === 'test') {
94+
throw createUnsupportedError(
95+
'Nested test "' + title + '" detected inside test "' + currentRunnable.title + '". ' +
96+
'Nested tests are not allowed.'
97+
);
98+
}
99+
}
100+
87101
var test = new Test(title, fn);
88102
test.file = file;
89103
suite.addTest(test);

lib/runner.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ class Runner extends EventEmitter {
190190
this.state = constants.STATE_IDLE;
191191
this.total = suite.total();
192192
this.failures = 0;
193+
194+
// Initialize global execution state tracking for nested test detection
195+
if (!global._mochaExecutionState) {
196+
global._mochaExecutionState = {
197+
currentRunnable: null
198+
};
199+
}
193200
/**
194201
* @type {Map<EventEmitter,Map<string,Set<EventListener>>>}
195202
*/
@@ -201,6 +208,10 @@ class Runner extends EventEmitter {
201208
if (idx > -1) test.parent.tests[idx] = test;
202209
}
203210
self.checkGlobals(test);
211+
// Clear execution state when test ends
212+
if (global._mochaExecutionState) {
213+
global._mochaExecutionState.currentRunnable = null;
214+
}
204215
});
205216
this.on(constants.EVENT_HOOK_END, function (hook) {
206217
self.checkGlobals(hook);
@@ -523,6 +534,7 @@ Runner.prototype.hook = function (name, fn) {
523534
return fn();
524535
}
525536
self.currentRunnable = hook;
537+
global._mochaExecutionState.currentRunnable = hook;
526538

527539
if (name === HOOK_TYPE_BEFORE_ALL) {
528540
hook.ctx.currentTest = hook.parent.tests[0];
@@ -835,6 +847,7 @@ Runner.prototype.runTests = function (suite, fn) {
835847
return hookErr(err, errSuite, false);
836848
}
837849
self.currentRunnable = self.test;
850+
global._mochaExecutionState.currentRunnable = self.test;
838851
self.runTest(function (err) {
839852
test = self.test;
840853
// conditional skip within it
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
// BDD async nested test fixtures - should fail with nested test errors
4+
describe('Async Nested Tests', function() {
5+
it('sync nested test', function() {
6+
it('nested in sync', function() {
7+
// Should fail immediately
8+
});
9+
});
10+
11+
it('callback nested test', function(done) {
12+
setTimeout(function() {
13+
it('nested in callback', function() {
14+
// Should fail with uncaught error
15+
});
16+
done();
17+
}, 10);
18+
});
19+
20+
it('promise nested test', function() {
21+
return new Promise(function(resolve) {
22+
setTimeout(function() {
23+
it('nested in promise', function() {
24+
// Should fail with uncaught error
25+
});
26+
resolve();
27+
}, 10);
28+
});
29+
});
30+
31+
it('async/await nested test', async function() {
32+
await new Promise(resolve => setTimeout(resolve, 10));
33+
it('nested in async', function() {
34+
// Should fail
35+
});
36+
});
37+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
// BDD nested test fixture - should fail with nested test error
4+
describe('Parent Suite', function() {
5+
it('normal test', function() {
6+
// This should pass
7+
});
8+
9+
it('outer test with nested test', function() {
10+
// This should fail due to nested test
11+
it('inner nested test', function() {
12+
// This nested test should cause an error
13+
});
14+
});
15+
16+
it('another normal test', function() {
17+
// This should not run due to previous failure
18+
});
19+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
// TDD nested test fixture - should fail with nested test error
4+
suite('Parent Suite', function() {
5+
test('normal test', function() {
6+
// This should pass
7+
});
8+
9+
test('outer test with nested test', function() {
10+
// This should fail due to nested test
11+
test('inner nested test', function() {
12+
// This nested test should cause an error
13+
});
14+
});
15+
16+
test('another normal test', function() {
17+
// This should not run due to previous failure
18+
});
19+
});
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
'use strict';
2+
3+
var path = require('node:path').posix;
4+
var helpers = require('./helpers');
5+
var runMocha = helpers.runMocha;
6+
var runMochaJSON = helpers.runMochaJSON;
7+
8+
describe('nested test detection', function () {
9+
var nestedTestErrorMessage = 'Nested tests are not allowed';
10+
11+
describe('BDD interface', function () {
12+
it('should fail when nested tests are detected', function (done) {
13+
var fixture = path.join('nested-tests', 'bdd-nested');
14+
var spawnOpts = {stdio: 'pipe'};
15+
runMocha(
16+
fixture,
17+
['--ui', 'bdd'],
18+
function (err, res) {
19+
if (err) {
20+
return done(err);
21+
}
22+
expect(res, 'to have failed with output', new RegExp(nestedTestErrorMessage));
23+
expect(res, 'to have failed with output', /inner nested test.*detected inside test.*outer test/);
24+
done();
25+
},
26+
spawnOpts
27+
);
28+
});
29+
30+
it('should report correct test counts when nested tests fail', function (done) {
31+
var fixture = path.join('nested-tests', 'bdd-nested');
32+
runMochaJSON(fixture, ['--ui', 'bdd'], function (err, res) {
33+
if (err) {
34+
return done(err);
35+
}
36+
expect(res, 'to have failed')
37+
.and('to have passed test count', 2) // 'normal test' and 'another normal test' should pass
38+
.and('to have failed test count', 1); // 'outer test with nested test' should fail
39+
done();
40+
});
41+
});
42+
});
43+
44+
describe('TDD interface', function () {
45+
it('should fail when nested tests are detected', function (done) {
46+
var fixture = path.join('nested-tests', 'tdd-nested');
47+
var spawnOpts = {stdio: 'pipe'};
48+
runMocha(
49+
fixture,
50+
['--ui', 'tdd'],
51+
function (err, res) {
52+
if (err) {
53+
return done(err);
54+
}
55+
expect(res, 'to have failed with output', new RegExp(nestedTestErrorMessage));
56+
expect(res, 'to have failed with output', /inner nested test.*detected inside test.*outer test/);
57+
done();
58+
},
59+
spawnOpts
60+
);
61+
});
62+
63+
it('should report correct test counts when nested tests fail', function (done) {
64+
var fixture = path.join('nested-tests', 'tdd-nested');
65+
runMochaJSON(fixture, ['--ui', 'tdd'], function (err, res) {
66+
if (err) {
67+
return done(err);
68+
}
69+
expect(res, 'to have failed')
70+
.and('to have passed test count', 2) // 'normal test' and 'another normal test' should pass
71+
.and('to have failed test count', 1); // 'outer test with nested test' should fail
72+
done();
73+
});
74+
});
75+
});
76+
77+
describe('async nested tests', function () {
78+
it('should handle synchronous nested tests', function (done) {
79+
var fixture = path.join('nested-tests', 'bdd-async-nested');
80+
var spawnOpts = {stdio: 'pipe'};
81+
runMocha(
82+
fixture,
83+
['--ui', 'bdd', '--grep', 'sync nested test'],
84+
function (err, res) {
85+
if (err) {
86+
return done(err);
87+
}
88+
expect(res, 'to have failed with output', new RegExp(nestedTestErrorMessage));
89+
expect(res, 'to have failed with output', /nested in sync.*detected inside test.*sync nested test/);
90+
done();
91+
},
92+
spawnOpts
93+
);
94+
});
95+
96+
it('should handle async/await nested tests', function (done) {
97+
var fixture = path.join('nested-tests', 'bdd-async-nested');
98+
var spawnOpts = {stdio: 'pipe'};
99+
runMocha(
100+
fixture,
101+
['--ui', 'bdd', '--grep', 'async/await nested test'],
102+
function (err, res) {
103+
if (err) {
104+
return done(err);
105+
}
106+
expect(res, 'to have failed with output', new RegExp(nestedTestErrorMessage));
107+
expect(res, 'to have failed with output', /nested in async.*detected inside test.*async\/await nested test/);
108+
done();
109+
},
110+
spawnOpts
111+
);
112+
});
113+
114+
it('should handle callback-based nested tests as uncaught errors', function (done) {
115+
var fixture = path.join('nested-tests', 'bdd-async-nested');
116+
var spawnOpts = {stdio: 'pipe'};
117+
runMocha(
118+
fixture,
119+
['--ui', 'bdd', '--grep', 'callback nested test'],
120+
function (err, res) {
121+
if (err) {
122+
return done(err);
123+
}
124+
expect(res, 'to have failed with output', new RegExp(nestedTestErrorMessage));
125+
expect(res, 'to have failed with output', /Uncaught.*nested in callback.*detected inside test.*callback nested test/);
126+
done();
127+
},
128+
spawnOpts
129+
);
130+
});
131+
132+
it('should handle promise-based nested tests as uncaught errors', function (done) {
133+
var fixture = path.join('nested-tests', 'bdd-async-nested');
134+
var spawnOpts = {stdio: 'pipe'};
135+
runMocha(
136+
fixture,
137+
['--ui', 'bdd', '--grep', 'promise nested test'],
138+
function (err, res) {
139+
if (err) {
140+
return done(err);
141+
}
142+
expect(res, 'to have failed with output', new RegExp(nestedTestErrorMessage));
143+
expect(res, 'to have failed with output', /Uncaught.*nested in promise.*detected inside test.*promise nested test/);
144+
done();
145+
},
146+
spawnOpts
147+
);
148+
});
149+
});
150+
151+
describe('error details', function () {
152+
it('should provide helpful error messages with test names', function (done) {
153+
var fixture = path.join('nested-tests', 'bdd-nested');
154+
var spawnOpts = {stdio: 'pipe'};
155+
runMocha(
156+
fixture,
157+
['--ui', 'bdd'],
158+
function (err, res) {
159+
if (err) {
160+
return done(err);
161+
}
162+
expect(res, 'to have failed with output', /Error: Nested test "inner nested test"/);
163+
expect(res, 'to have failed with output', /detected inside test "outer test with nested test"/);
164+
expect(res, 'to have failed with output', /createUnsupportedError/);
165+
done();
166+
},
167+
spawnOpts
168+
);
169+
});
170+
});
171+
});

0 commit comments

Comments
 (0)