Skip to content

Commit 5d13419

Browse files
authored
test_runner: run before hook immediately if test started
PR-URL: #52003 Reviewed-By: Raz Luvaton <rluvaton@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent f0e6acd commit 5d13419

File tree

8 files changed

+431
-38
lines changed

8 files changed

+431
-38
lines changed

lib/internal/test_runner/harness.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,11 @@ function setup(root) {
151151
const rejectionHandler =
152152
createProcessEventHandler('unhandledRejection', root);
153153
const coverage = configureCoverage(root, globalOptions);
154-
const exitHandler = () => {
154+
const exitHandler = async () => {
155+
if (root.subtests.length === 0 && (root.hooks.before.length > 0 || root.hooks.after.length > 0)) {
156+
// Run global before/after hooks in case there are no tests
157+
await root.run();
158+
}
155159
root.postRun(new ERR_TEST_FAILURE(
156160
'Promise resolution is still pending but the event loop has already resolved',
157161
kCancelledByParent));

lib/internal/test_runner/test.js

+22-8
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,23 @@ class TestContext {
177177
}
178178

179179
before(fn, options) {
180-
this.#test.createHook('before', fn, options);
180+
this.#test
181+
.createHook('before', fn, { __proto__: null, ...options, hookType: 'before', loc: getCallerLocation() });
181182
}
182183

183184
after(fn, options) {
184-
this.#test.createHook('after', fn, options);
185+
this.#test
186+
.createHook('after', fn, { __proto__: null, ...options, hookType: 'after', loc: getCallerLocation() });
185187
}
186188

187189
beforeEach(fn, options) {
188-
this.#test.createHook('beforeEach', fn, options);
190+
this.#test
191+
.createHook('beforeEach', fn, { __proto__: null, ...options, hookType: 'beforeEach', loc: getCallerLocation() });
189192
}
190193

191194
afterEach(fn, options) {
192-
this.#test.createHook('afterEach', fn, options);
195+
this.#test
196+
.createHook('afterEach', fn, { __proto__: null, ...options, hookType: 'afterEach', loc: getCallerLocation() });
193197
}
194198
}
195199

@@ -518,6 +522,14 @@ class Test extends AsyncResource {
518522
if (name === 'before' || name === 'after') {
519523
hook.run = runOnce(hook.run);
520524
}
525+
if (name === 'before' && this.startTime !== null) {
526+
// Test has already started, run the hook immediately
527+
PromisePrototypeThen(hook.run(this.getRunArgs()), () => {
528+
if (hook.error != null) {
529+
this.fail(hook.error);
530+
}
531+
});
532+
}
521533
ArrayPrototypePush(this.hooks[name], hook);
522534
return hook;
523535
}
@@ -615,26 +627,28 @@ class Test extends AsyncResource {
615627
return;
616628
}
617629

618-
const { args, ctx } = this.getRunArgs();
630+
const hookArgs = this.getRunArgs();
631+
const { args, ctx } = hookArgs;
619632
const after = async () => {
620633
if (this.hooks.after.length > 0) {
621-
await this.runHook('after', { __proto__: null, args, ctx });
634+
await this.runHook('after', hookArgs);
622635
}
623636
};
624637
const afterEach = runOnce(async () => {
625638
if (this.parent?.hooks.afterEach.length > 0) {
626-
await this.parent.runHook('afterEach', { __proto__: null, args, ctx });
639+
await this.parent.runHook('afterEach', hookArgs);
627640
}
628641
});
629642

630643
let stopPromise;
631644

632645
try {
633646
if (this.parent?.hooks.before.length > 0) {
647+
// This hook usually runs immediately, we need to wait for it to finish
634648
await this.parent.runHook('before', this.parent.getRunArgs());
635649
}
636650
if (this.parent?.hooks.beforeEach.length > 0) {
637-
await this.parent.runHook('beforeEach', { __proto__: null, args, ctx });
651+
await this.parent.runHook('beforeEach', hookArgs);
638652
}
639653
stopPromise = stopTest(this.timeout, this.signal);
640654
const runArgs = ArrayPrototypeSlice(args);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict';
2+
const common = require('../../../common');
3+
const { before, after } = require('node:test');
4+
5+
before(common.mustCall(() => console.log('before')));
6+
after(common.mustCall(() => console.log('after')));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
before
2+
TAP version 13
3+
after
4+
1..0
5+
# tests 0
6+
# suites 0
7+
# pass 0
8+
# fail 0
9+
# cancelled 0
10+
# skipped 0
11+
# todo 0
12+
# duration_ms *

test/fixtures/test-runner/output/hooks.js

+62-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('describe hooks', () => {
1111
before(function() {
1212
testArr.push('before ' + this.name);
1313
});
14-
after(function() {
14+
after(common.mustCall(function() {
1515
testArr.push('after ' + this.name);
1616
assert.deepStrictEqual(testArr, [
1717
'before describe hooks',
@@ -23,7 +23,7 @@ describe('describe hooks', () => {
2323
'after nested',
2424
'after describe hooks',
2525
]);
26-
});
26+
}));
2727
beforeEach(function() {
2828
testArr.push('beforeEach ' + this.name);
2929
});
@@ -52,18 +52,43 @@ describe('describe hooks', () => {
5252
});
5353
});
5454

55+
describe('describe hooks - no subtests', () => {
56+
const testArr = [];
57+
before(function() {
58+
testArr.push('before ' + this.name);
59+
});
60+
after(common.mustCall(function() {
61+
testArr.push('after ' + this.name);
62+
assert.deepStrictEqual(testArr, [
63+
'before describe hooks - no subtests',
64+
'after describe hooks - no subtests',
65+
]);
66+
}));
67+
beforeEach(common.mustNotCall());
68+
afterEach(common.mustNotCall());
69+
});
70+
5571
describe('before throws', () => {
5672
before(() => { throw new Error('before'); });
5773
it('1', () => {});
5874
test('2', () => {});
5975
});
6076

77+
describe('before throws - no subtests', () => {
78+
before(() => { throw new Error('before'); });
79+
after(common.mustCall());
80+
});
81+
6182
describe('after throws', () => {
6283
after(() => { throw new Error('after'); });
6384
it('1', () => {});
6485
test('2', () => {});
6586
});
6687

88+
describe('after throws - no subtests', () => {
89+
after(() => { throw new Error('after'); });
90+
});
91+
6792
describe('beforeEach throws', () => {
6893
beforeEach(() => { throw new Error('beforeEach'); });
6994
it('1', () => {});
@@ -123,13 +148,48 @@ test('test hooks', async (t) => {
123148
}));
124149
});
125150

151+
152+
test('test hooks - no subtests', async (t) => {
153+
const testArr = [];
154+
155+
t.before((t) => testArr.push('before ' + t.name));
156+
t.after(common.mustCall((t) => testArr.push('after ' + t.name)));
157+
t.beforeEach(common.mustNotCall());
158+
t.afterEach(common.mustNotCall());
159+
160+
t.after(common.mustCall(() => {
161+
assert.deepStrictEqual(testArr, [
162+
'before test hooks - no subtests',
163+
'after test hooks - no subtests',
164+
]);
165+
}));
166+
});
167+
126168
test('t.before throws', async (t) => {
127169
t.after(common.mustCall());
128170
t.before(() => { throw new Error('before'); });
129171
await t.test('1', () => {});
130172
await t.test('2', () => {});
131173
});
132174

175+
test('t.before throws - no subtests', async (t) => {
176+
t.after(common.mustCall());
177+
t.before(() => { throw new Error('before'); });
178+
});
179+
180+
test('t.after throws', async (t) => {
181+
t.before(common.mustCall());
182+
t.after(() => { throw new Error('after'); });
183+
await t.test('1', () => {});
184+
await t.test('2', () => {});
185+
});
186+
187+
test('t.after throws - no subtests', async (t) => {
188+
t.before(common.mustCall());
189+
t.after(() => { throw new Error('after'); });
190+
});
191+
192+
133193
test('t.beforeEach throws', async (t) => {
134194
t.after(common.mustCall());
135195
t.beforeEach(() => { throw new Error('beforeEach'); });

0 commit comments

Comments
 (0)