diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 2203bbd3497659..123d5a80baf2db 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1,6 +1,7 @@ 'use strict'; const { ArrayPrototypeEvery, + ArrayPrototypeForEach, ArrayPrototypePush, ArrayPrototypePushApply, ArrayPrototypeShift, @@ -259,6 +260,41 @@ class TestContext { constructor(test) { this.#test = test; + + // Attach .skip, .todo, .only, .expectFailure variants to this.test, + // matching the same set exposed on the top-level test() in harness.js. + this.test = (name, options, fn) => + this.#runTest(name, options, fn, getCallerLocation()); + ArrayPrototypeForEach( + ['expectFailure', 'only', 'skip', 'todo'], + (keyword) => { + this.test[keyword] = (name, options, fn) => + this.#runTest(name, options, fn, getCallerLocation(), { + __proto__: null, + [keyword]: true, + }); + }, + ); + } + + #runTest(name, options, fn, loc, extraOverrides) { + const overrides = { + __proto__: null, + ...extraOverrides, + loc, + }; + + const { plan } = this.#test; + if (plan !== null) { + plan.count(); + } + + const subtest = this.#test.createSubtest( + // eslint-disable-next-line no-use-before-define + Test, name, options, fn, overrides, + ); + + return subtest.start(); } get signal() { @@ -358,25 +394,6 @@ class TestContext { this.#test.todo(message); } - test(name, options, fn) { - const overrides = { - __proto__: null, - loc: getCallerLocation(), - }; - - const { plan } = this.#test; - if (plan !== null) { - plan.count(); - } - - const subtest = this.#test.createSubtest( - // eslint-disable-next-line no-use-before-define - Test, name, options, fn, overrides, - ); - - return subtest.start(); - } - before(fn, options) { this.#test.createHook('before', fn, { __proto__: null, diff --git a/test/parallel/test-runner-subtest-skip-todo-only.js b/test/parallel/test-runner-subtest-skip-todo-only.js new file mode 100644 index 00000000000000..62b02af35704e9 --- /dev/null +++ b/test/parallel/test-runner-subtest-skip-todo-only.js @@ -0,0 +1,50 @@ +'use strict'; +// Regression test for https://github.com/nodejs/node/issues/50665 +// t.test() should expose the same .skip, .todo, .only, .expectFailure +// variants as the top-level test() function. +const common = require('../common'); +const assert = require('node:assert'); +const { test } = require('node:test'); + +test('context test function exposes all variant methods', async (t) => { + for (const variant of ['skip', 'todo', 'only', 'expectFailure']) { + assert.strictEqual( + typeof t.test[variant], 'function', + `t.test.${variant} should be a function`, + ); + } +}); + +test('skip variant does not execute the callback', async (t) => { + await t.test.skip('this is skipped', common.mustNotCall()); +}); + +test('todo variant without callback marks subtest as todo', async (t) => { + await t.test.todo('pending feature'); +}); + +test('todo variant with callback still runs', async (t) => { + let ran = false; + await t.test.todo('work in progress', () => { ran = true; }); + assert.ok(ran, 'todo callback should have been invoked'); +}); + +test('variants increment the plan counter', async (t) => { + t.plan(4); + await t.test('regular', common.mustCall()); + await t.test.skip('skipped'); + await t.test.todo('todo'); + await t.test.todo('todo with cb', common.mustCall()); +}); + +test('variants propagate to nested subtests', async (t) => { + await t.test('outer', async (t2) => { + for (const variant of ['skip', 'todo', 'only', 'expectFailure']) { + assert.strictEqual( + typeof t2.test[variant], 'function', + `nested t.test.${variant} should be a function`, + ); + } + await t2.test.skip('inner skip', common.mustNotCall()); + }); +});