diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 1bdd6e99ed1c3b..8f06eb19d76dcc 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -170,8 +170,20 @@ function runInParentContext(Factory) { return cb; } +function afterEach(fn, options) { + const parent = testResources.get(executionAsyncId()) || setup(root); + parent.createBeforeEachHook(fn, options); + +} +function beforeEach(fn, options) { + const parent = testResources.get(executionAsyncId()) || setup(root); + parent.createBeforeEachHook(fn, options); +} + module.exports = { test: FunctionPrototypeBind(test, root), describe: runInParentContext(Suite), it: runInParentContext(ItTest), + afterEach, + beforeEach, }; diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 3d5230b082c3e6..4914e8147c9233 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -47,6 +47,10 @@ class TestContext { this.#test = test; } + get name() { + return this.#test.name; + } + diagnostic(message) { this.#test.diagnostic(message); } @@ -69,6 +73,14 @@ class TestContext { return subtest.start(); } + + beforeEach(fn, options) { + this.#test.createBeforeEachHook(fn, options); + } + + afterEach(fn, options) { + this.#test.createAfterEachHook(fn, options); + } } class Test extends AsyncResource { @@ -140,6 +152,8 @@ class Test extends AsyncResource { this.pendingSubtests = []; this.readySubtests = new SafeMap(); this.subtests = []; + this.beforeEachHooks = []; + this.afterEachHooks = []; this.waitingOn = 0; this.finished = false; } @@ -242,6 +256,18 @@ class Test extends AsyncResource { return test; } + createBeforeEachHook(fn, options) { + const hook = new TestHook(fn, options); + ArrayPrototypePush(this.beforeEachHooks, hook); + return hook; + } + + createAfterEachHook(fn, options) { + const hook = new TestHook(fn, options); + ArrayPrototypePush(this.afterEachHooks, hook); + return hook; + } + cancel() { if (this.endTime !== null) { return; @@ -253,6 +279,7 @@ class Test extends AsyncResource { kCancelledByParent ) ); + this.startTime = this.startTime || this.endTime; // if a test was canceled before it was started, e.g inside a hook this.cancelled = true; } @@ -309,12 +336,24 @@ class Test extends AsyncResource { return { ctx, args: [ctx] }; } - async run() { - this.parent.activeSubtests++; + async #runHooks(hooks) { + await ArrayPrototypeReduce(hooks, async (prev, hook) => { + await prev; + await hook.run(this.getRunArgs()); + }, PromiseResolve()); + } + + async run(...runArgs) { + if (this.parent !== null) { + this.parent.activeSubtests++; + } + if (this.parent?.beforeEachHooks.length > 0) { + await this.#runHooks(this.parent.beforeEachHooks); + } this.startTime = hrtime(); try { - const { args, ctx } = this.getRunArgs(); + const { args, ctx } = ReflectApply(this.getRunArgs, this, runArgs); ArrayPrototypeUnshift(args, this.fn, ctx); // Note that if it's not OK to mutate args, we need to first clone it. if (this.fn.length === args.length - 1) { @@ -347,9 +386,13 @@ class Test extends AsyncResource { } } + if (this.parent?.afterEachHooks.length > 0) { + await this.#runHooks(this.parent.afterEachHooks); + } + // Clean up the test. Then, try to report the results and execute any // tests that were pending due to available concurrency. - this.postRun(); + await this.postRun(); } postRun() { @@ -387,7 +430,7 @@ class Test extends AsyncResource { this.parent.activeSubtests--; this.parent.addReadySubtest(this); this.parent.processReadySubtestRange(false); - this.parent.processPendingSubtests(); + return this.parent.processPendingSubtests(); } } @@ -447,10 +490,23 @@ class Test extends AsyncResource { } } +class TestHook extends Test { + constructor(fn, options) { + if (options === null || typeof options !== 'object') { + options = kEmptyObject; + } + super({ fn, ...options }); + } + getRunArgs(testContext) { + return testContext; + } +} + class ItTest extends Test { constructor(opt) { super(opt); } // eslint-disable-line no-useless-constructor getRunArgs() { - return { ctx: {}, args: [] }; + const ctx = new TestContext(this); + return { ctx, args: [] }; } } class Suite extends Test { diff --git a/lib/test.js b/lib/test.js index 7ebc852092b93b..18eb79b0ffb103 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,5 +1,5 @@ 'use strict'; -const { test, describe, it } = require('internal/test_runner/harness'); +const { test, describe, it, afterEach, beforeEach } = require('internal/test_runner/harness'); const { emitExperimentalWarning } = require('internal/util'); emitExperimentalWarning('The test runner'); @@ -8,3 +8,5 @@ module.exports = test; module.exports.test = test; module.exports.describe = describe; module.exports.it = it; +module.exports.afterEach = afterEach; +module.exports.beforeEach = beforeEach; diff --git a/test/message/test_runner_desctibe_it.js b/test/message/test_runner_desctibe_it.js index 3f82762d91b4b6..bae499b2c1f45e 100644 --- a/test/message/test_runner_desctibe_it.js +++ b/test/message/test_runner_desctibe_it.js @@ -219,19 +219,6 @@ it('callback fail', (done) => { }); }); -it('sync t is this in test', function() { - assert.deepStrictEqual(this, {}); -}); - -it('async t is this in test', async function() { - assert.deepStrictEqual(this, {}); -}); - -it('callback t is this in test', function(done) { - assert.deepStrictEqual(this, {}); - done(); -}); - it('callback also returns a Promise', async (done) => { throw new Error('thrown from callback also returns a Promise'); }); diff --git a/test/message/test_runner_desctibe_it.out b/test/message/test_runner_desctibe_it.out index f4968b37c63eb0..6823b03df5df69 100644 --- a/test/message/test_runner_desctibe_it.out +++ b/test/message/test_runner_desctibe_it.out @@ -74,6 +74,9 @@ not ok 8 - sync throw fail * * * + * + * + * ... # Subtest: async skip pass ok 9 - async skip pass # SKIP @@ -100,6 +103,9 @@ not ok 11 - async throw fail * * * + * + * + * ... # Subtest: async assertion fail not ok 12 - async assertion fail @@ -108,9 +114,9 @@ not ok 12 - async assertion fail failureType: 'testCodeFailure' error: |- Expected values to be strictly equal: - + true !== false - + code: 'ERR_ASSERTION' stack: |- * @@ -120,6 +126,9 @@ not ok 12 - async assertion fail * * * + * + * + * ... # Subtest: resolve pass ok 13 - resolve pass @@ -141,6 +150,9 @@ not ok 14 - reject fail * * * + * + * + * ... # Subtest: unhandled rejection - passes but warns ok 15 - unhandled rejection - passes but warns @@ -277,6 +289,9 @@ not ok 27 - sync skip option is false fail * * * + * + * + * ... # Subtest: ok 28 - @@ -349,23 +364,8 @@ not ok 40 - callback fail * * ... -# Subtest: sync t is this in test -ok 41 - sync t is this in test - --- - duration_ms: * - ... -# Subtest: async t is this in test -ok 42 - async t is this in test - --- - duration_ms: * - ... -# Subtest: callback t is this in test -ok 43 - callback t is this in test - --- - duration_ms: * - ... # Subtest: callback also returns a Promise -not ok 44 - callback also returns a Promise +not ok 41 - callback also returns a Promise --- duration_ms: * failureType: 'callbackAndPromisePresent' @@ -373,7 +373,7 @@ not ok 44 - callback also returns a Promise code: 'ERR_TEST_FAILURE' ... # Subtest: callback throw -not ok 45 - callback throw +not ok 42 - callback throw --- duration_ms: * failureType: 'testCodeFailure' @@ -387,9 +387,12 @@ not ok 45 - callback throw * * * + * + * + * ... # Subtest: callback called twice -not ok 46 - callback called twice +not ok 43 - callback called twice --- duration_ms: * failureType: 'multipleCallbackInvocations' @@ -400,12 +403,12 @@ not ok 46 - callback called twice * ... # Subtest: callback called twice in different ticks -ok 47 - callback called twice in different ticks +ok 44 - callback called twice in different ticks --- duration_ms: * ... # Subtest: callback called twice in future tick -not ok 48 - callback called twice in future tick +not ok 45 - callback called twice in future tick --- duration_ms: * failureType: 'uncaughtException' @@ -415,7 +418,7 @@ not ok 48 - callback called twice in future tick * ... # Subtest: callback async throw -not ok 49 - callback async throw +not ok 46 - callback async throw --- duration_ms: * failureType: 'uncaughtException' @@ -425,12 +428,12 @@ not ok 49 - callback async throw * ... # Subtest: callback async throw after done -ok 50 - callback async throw after done +ok 47 - callback async throw after done --- duration_ms: * ... # Subtest: custom inspect symbol fail -not ok 51 - custom inspect symbol fail +not ok 48 - custom inspect symbol fail --- duration_ms: * failureType: 'testCodeFailure' @@ -438,7 +441,7 @@ not ok 51 - custom inspect symbol fail code: 'ERR_TEST_FAILURE' ... # Subtest: custom inspect symbol that throws fail -not ok 52 - custom inspect symbol that throws fail +not ok 49 - custom inspect symbol that throws fail --- duration_ms: * failureType: 'testCodeFailure' @@ -482,7 +485,7 @@ not ok 52 - custom inspect symbol that throws fail * ... 1..2 -not ok 53 - subtest sync throw fails +not ok 50 - subtest sync throw fails --- duration_ms: * failureType: 'subtestsFailed' @@ -490,7 +493,7 @@ not ok 53 - subtest sync throw fails code: 'ERR_TEST_FAILURE' ... # Subtest: invalid subtest fail -not ok 54 - invalid subtest fail +not ok 51 - invalid subtest fail --- duration_ms: * failureType: 'parentAlreadyFinished' @@ -499,15 +502,15 @@ not ok 54 - invalid subtest fail stack: |- * ... -1..54 +1..51 # Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. # Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. # Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event. # Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event. # Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event. # Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event. -# tests 54 -# pass 23 +# tests 51 +# pass 20 # fail 17 # cancelled 0 # skipped 9 diff --git a/test/message/test_runner_hooks.js b/test/message/test_runner_hooks.js new file mode 100644 index 00000000000000..c237db02c6190e --- /dev/null +++ b/test/message/test_runner_hooks.js @@ -0,0 +1,31 @@ +// Flags: --no-warnings +'use strict'; +require('../common'); +const { describe, it, beforeEach, afterEach, test } = require('node:test'); + + +test('test hooks', async (t) => { + t.beforeEach((t) => { + t.diagnostic('test before ' + t.name); + }); + t.afterEach((t) => { + t.diagnostic('test after ' + t.name); + }); + + await t.test('1') + await t.test('2') + await t.test('3') +}); + +describe('describe hooks', () => { + beforeEach(function() { + this.diagnostic('it before' + this.name); + }); + afterEach(function() { + this.diagnostic('it after' + this.name); + }); + + it('1') + it('2') + it('3') +}); diff --git a/test/message/test_runner_hooks.out b/test/message/test_runner_hooks.out new file mode 100644 index 00000000000000..0340d53f71b015 --- /dev/null +++ b/test/message/test_runner_hooks.out @@ -0,0 +1,62 @@ +TAP version 13 +# Subtest: test hooks + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # test before 1 + # test after 1 + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + # test before 2 + # test after 2 + # Subtest: 3 + ok 3 - 3 + --- + duration_ms: * + ... + # test before 3 + # test after 3 + 1..3 +ok 1 - test hooks + --- + duration_ms: * + ... +# Subtest: describe hooks + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # it before1 + # it after1 + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + # it before2 + # it after2 + # Subtest: 3 + ok 3 - 3 + --- + duration_ms: * + ... + # it before3 + # it after3 + 1..3 +ok 2 - describe hooks + --- + duration_ms: * + ... +1..2 +# tests 2 +# pass 2 +# fail 0 +# skipped 0 +# todo 0 +# duration_ms * diff --git a/test/message/test_runner_output.out b/test/message/test_runner_output.out index bada2fdacae9a9..e1123d436b5c43 100644 --- a/test/message/test_runner_output.out +++ b/test/message/test_runner_output.out @@ -25,6 +25,7 @@ not ok 3 - sync fail todo # TODO * * * + * ... # Subtest: sync fail todo with message not ok 4 - sync fail todo with message # TODO this is a failing todo @@ -77,6 +78,8 @@ not ok 8 - sync throw fail * * * + * + * ... # Subtest: async skip pass ok 9 - async skip pass # SKIP @@ -104,6 +107,8 @@ not ok 11 - async throw fail * * * + * + * ... # Subtest: async skip fail not ok 12 - async skip fail # SKIP @@ -121,6 +126,8 @@ not ok 12 - async skip fail # SKIP * * * + * + * ... # Subtest: async assertion fail not ok 13 - async assertion fail @@ -129,9 +136,9 @@ not ok 13 - async assertion fail failureType: 'testCodeFailure' error: |- Expected values to be strictly equal: - + true !== false - + code: 'ERR_ASSERTION' stack: |- * @@ -142,6 +149,8 @@ not ok 13 - async assertion fail * * * + * + * ... # Subtest: resolve pass ok 14 - resolve pass @@ -164,6 +173,8 @@ not ok 15 - reject fail * * * + * + * ... # Subtest: unhandled rejection - passes but warns ok 16 - unhandled rejection - passes but warns @@ -311,6 +322,9 @@ not ok 28 - sync skip option is false fail * * * + * + * + * ... # Subtest: ok 29 - @@ -427,6 +441,9 @@ not ok 47 - callback throw * * * + * + * + * ... # Subtest: callback called twice not ok 48 - callback called twice