Skip to content

Commit d9bb1f7

Browse files
committed
test_runner: add skip, todo, only, and expectFailure to subtest context
The top-level test() function exposes test.skip(), test.todo(), test.only(), and test.expectFailure() variants, but these were missing from TestContext's test() method used in subtests, causing TypeError. Move test() from the class prototype to an arrow function in the constructor, allowing variants to be attached as properties. Extract a shared runSubtest() helper to avoid duplicating the plan counting and createSubtest logic. This trades one shared prototype method for 5 closures per TestContext instance (one base + four variants), which is acceptable given V8's closure optimization for same-shape functions. Includes tests for: variant existence, skip preventing callback execution, todo with and without callback, plan counting with variants, and nested subtest variant availability. Fixes: #50665
1 parent 5b5f069 commit d9bb1f7

File tree

2 files changed

+78
-19
lines changed

2 files changed

+78
-19
lines changed

lib/internal/test_runner/test.js

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
const {
33
ArrayPrototypeEvery,
4+
ArrayPrototypeForEach,
45
ArrayPrototypePush,
56
ArrayPrototypePushApply,
67
ArrayPrototypeShift,
@@ -259,6 +260,35 @@ class TestContext {
259260

260261
constructor(test) {
261262
this.#test = test;
263+
264+
const runSubtest = (name, options, fn, extraOverrides) => {
265+
const overrides = {
266+
__proto__: null,
267+
...extraOverrides,
268+
loc: getCallerLocation(),
269+
};
270+
271+
const { plan } = this.#test;
272+
if (plan !== null) {
273+
plan.count();
274+
}
275+
276+
const subtest = this.#test.createSubtest(
277+
// eslint-disable-next-line no-use-before-define
278+
Test, name, options, fn, overrides,
279+
);
280+
281+
return subtest.start();
282+
};
283+
284+
this.test = (name, options, fn) => runSubtest(name, options, fn);
285+
ArrayPrototypeForEach(
286+
['expectFailure', 'skip', 'todo', 'only'],
287+
(keyword) => {
288+
this.test[keyword] = (name, options, fn) =>
289+
runSubtest(name, options, fn, { [keyword]: true });
290+
},
291+
);
262292
}
263293

264294
get signal() {
@@ -358,25 +388,6 @@ class TestContext {
358388
this.#test.todo(message);
359389
}
360390

361-
test(name, options, fn) {
362-
const overrides = {
363-
__proto__: null,
364-
loc: getCallerLocation(),
365-
};
366-
367-
const { plan } = this.#test;
368-
if (plan !== null) {
369-
plan.count();
370-
}
371-
372-
const subtest = this.#test.createSubtest(
373-
// eslint-disable-next-line no-use-before-define
374-
Test, name, options, fn, overrides,
375-
);
376-
377-
return subtest.start();
378-
}
379-
380391
before(fn, options) {
381392
this.#test.createHook('before', fn, {
382393
__proto__: null,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('node:assert');
4+
const { test } = require('node:test');
5+
6+
// Verify that all subtest variants exist on TestContext.
7+
test('subtest variants exist on TestContext', common.mustCall(async (t) => {
8+
assert.strictEqual(typeof t.test, 'function');
9+
assert.strictEqual(typeof t.test.skip, 'function');
10+
assert.strictEqual(typeof t.test.todo, 'function');
11+
assert.strictEqual(typeof t.test.only, 'function');
12+
assert.strictEqual(typeof t.test.expectFailure, 'function');
13+
}));
14+
15+
// t.test.skip: callback must NOT be called.
16+
test('t.test.skip prevents callback execution', common.mustCall(async (t) => {
17+
await t.test.skip('skipped subtest', common.mustNotCall());
18+
}));
19+
20+
// t.test.todo without callback: subtest is marked as todo and skipped.
21+
test('t.test.todo without callback', common.mustCall(async (t) => {
22+
await t.test.todo('todo subtest without callback');
23+
}));
24+
25+
// t.test.todo with callback: callback runs but subtest is marked as todo.
26+
test('t.test.todo with callback runs the callback', common.mustCall(async (t) => {
27+
await t.test.todo('todo subtest with callback', common.mustCall());
28+
}));
29+
30+
// Plan counting works with subtest variants.
31+
test('plan counts subtest variants', common.mustCall(async (t) => {
32+
t.plan(3);
33+
await t.test('normal subtest', common.mustCall());
34+
await t.test.skip('skipped subtest');
35+
await t.test.todo('todo subtest');
36+
}));
37+
38+
// Nested subtests also expose the variants.
39+
test('nested subtests have variants', common.mustCall(async (t) => {
40+
await t.test('level 1', common.mustCall(async (t2) => {
41+
assert.strictEqual(typeof t2.test.skip, 'function');
42+
assert.strictEqual(typeof t2.test.todo, 'function');
43+
assert.strictEqual(typeof t2.test.only, 'function');
44+
assert.strictEqual(typeof t2.test.expectFailure, 'function');
45+
46+
await t2.test.skip('nested skipped', common.mustNotCall());
47+
}));
48+
}));

0 commit comments

Comments
 (0)