diff --git a/README.md b/README.md index 3867990..cfe3261 100644 --- a/README.md +++ b/README.md @@ -440,12 +440,166 @@ same as [`it([name], { skip: true }[, fn])`][it options]. Shorthand for marking a test as `TODO`, same as [`it([name], { todo: true }[, fn])`][it options]. +### `before([, fn][, options])` + +* `fn` {Function|AsyncFunction} The hook function. + If the hook uses callbacks, + the callback function is passed as the second argument. **Default:** A no-op + function. +* `options` {Object} Configuration options for the hook. The following + properties are supported: + * `signal` {AbortSignal} Allows aborting an in-progress hook + * `timeout` {number} A number of milliseconds the hook will fail after. + If unspecified, subtests inherit this value from their parent. + **Default:** `Infinity`. + +This function is used to create a hook running before running a suite. + +```js +describe('tests', async () => { + before(() => console.log('about to run some test')); + it('is a subtest', () => { + assert.ok('some relevant assertion here'); + }); +}); +``` + +### `after([, fn][, options])` + +* `fn` {Function|AsyncFunction} The hook function. + If the hook uses callbacks, + the callback function is passed as the second argument. **Default:** A no-op + function. +* `options` {Object} Configuration options for the hook. The following + properties are supported: + * `signal` {AbortSignal} Allows aborting an in-progress hook + * `timeout` {number} A number of milliseconds the hook will fail after. + If unspecified, subtests inherit this value from their parent. + **Default:** `Infinity`. + +This function is used to create a hook running after running a suite. + +```js +describe('tests', async () => { + after(() => console.log('finished running tests')); + it('is a subtest', () => { + assert.ok('some relevant assertion here'); + }); +}); +``` + +### `beforeEach([, fn][, options])` + +* `fn` {Function|AsyncFunction} The hook function. + If the hook uses callbacks, + the callback function is passed as the second argument. **Default:** A no-op + function. +* `options` {Object} Configuration options for the hook. The following + properties are supported: + * `signal` {AbortSignal} Allows aborting an in-progress hook + * `timeout` {number} A number of milliseconds the hook will fail after. + If unspecified, subtests inherit this value from their parent. + **Default:** `Infinity`. + +This function is used to create a hook running +before each subtest of the current suite. + +```js +describe('tests', async () => { + beforeEach(() => t.diagnostics('about to run a test')); + it('is a subtest', () => { + assert.ok('some relevant assertion here'); + }); +}); +``` + +### `afterEach([, fn][, options])` + +* `fn` {Function|AsyncFunction} The hook function. + If the hook uses callbacks, + the callback function is passed as the second argument. **Default:** A no-op + function. +* `options` {Object} Configuration options for the hook. The following + properties are supported: + * `signal` {AbortSignal} Allows aborting an in-progress hook + * `timeout` {number} A number of milliseconds the hook will fail after. + If unspecified, subtests inherit this value from their parent. + **Default:** `Infinity`. + +This function is used to create a hook running +after each subtest of the current test. + +```js +describe('tests', async () => { + afterEach(() => t.diagnostics('about to run a test')); + it('is a subtest', () => { + assert.ok('some relevant assertion here'); + }); +}); +``` + ## Class: `TestContext` An instance of `TestContext` is passed to each test function in order to interact with the test runner. However, the `TestContext` constructor is not exposed as part of the API. +### `context.beforeEach([, fn][, options])` + +* `fn` {Function|AsyncFunction} The hook function. The first argument + to this function is a [`TestContext`][] object. If the hook uses callbacks, + the callback function is passed as the second argument. **Default:** A no-op + function. +* `options` {Object} Configuration options for the hook. The following + properties are supported: + * `signal` {AbortSignal} Allows aborting an in-progress hook + * `timeout` {number} A number of milliseconds the hook will fail after. + If unspecified, subtests inherit this value from their parent. + **Default:** `Infinity`. + +This function is used to create a hook running +before each subtest of the current test. + +```js +test('top level test', async (t) => { + t.beforeEach((t) => t.diagnostics(`about to run ${t.name}`)); + await t.test( + 'This is a subtest', + (t) => { + assert.ok('some relevant assertion here'); + } + ); +}); +``` + +### `context.afterEach([, fn][, options])` + +* `fn` {Function|AsyncFunction} The hook function. The first argument + to this function is a [`TestContext`][] object. If the hook uses callbacks, + the callback function is passed as the second argument. **Default:** A no-op + function. +* `options` {Object} Configuration options for the hook. The following + properties are supported: + * `signal` {AbortSignal} Allows aborting an in-progress hook + * `timeout` {number} A number of milliseconds the hook will fail after. + If unspecified, subtests inherit this value from their parent. + **Default:** `Infinity`. + +This function is used to create a hook running +after each subtest of the current test. + +```js +test('top level test', async (t) => { + t.afterEach((t) => t.diagnostics(`finished running ${t.name}`)); + await t.test( + 'This is a subtest', + (t) => { + assert.ok('some relevant assertion here'); + } + ); +}); +``` + ### `context.diagnostic(message)` - `message` {string} Message to be displayed as a TAP diagnostic. @@ -454,6 +608,10 @@ This function is used to write TAP diagnostics to the output. Any diagnostic information is included at the end of the test's results. This function does not return a value. + `context.name` + +The name of the test + ### `context.runOnly(shouldRunOnlyTests)` - `shouldRunOnlyTests` {boolean} Whether or not to run `only` tests. @@ -528,6 +686,10 @@ An instance of `SuiteContext` is passed to each suite function in order to interact with the test runner. However, the `SuiteContext` constructor is not exposed as part of the API. +### `context.name` + +The name of the suite + ### `context.signal` * [`AbortSignal`][] Can be used to abort test subtasks when the test has been aborted. diff --git a/lib/internal/errors.js b/lib/internal/errors.js index a0c399c..8f3a07e 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -16,8 +16,10 @@ const { ReflectApply, SafeMap, SafeWeakMap, + StringPrototypeIncludes, StringPrototypeMatch, StringPrototypeStartsWith, + StringPrototypeSlice, Symbol, SymbolFor } = require('#internal/per_context/primordials') @@ -362,6 +364,19 @@ E('ERR_TEST_FAILURE', function (error, failureType) { E('ERR_INVALID_ARG_TYPE', (name, expected, actual) => `Expected ${name} to be ${expected}, got type ${typeof actual}`, TypeError) +E('ERR_INVALID_ARG_VALUE', (name, value, reason = 'is invalid') => { + let inspected + try { + inspected = String(value) + } catch { + inspected = `type ${typeof value}` + } + if (inspected.length > 128) { + inspected = `${StringPrototypeSlice(inspected, 0, 128)}...` + } + const type = StringPrototypeIncludes(name, '.') ? 'property' : 'argument' + return `The ${type} '${name}' ${reason}. Received ${inspected}` +}, TypeError, RangeError) E('ERR_OUT_OF_RANGE', (name, expected, actual) => `Expected ${name} to be ${expected}, got ${actual}`, RangeError) diff --git a/lib/internal/per_context/primordials.js b/lib/internal/per_context/primordials.js index c5d4e05..e599da9 100644 --- a/lib/internal/per_context/primordials.js +++ b/lib/internal/per_context/primordials.js @@ -7,6 +7,7 @@ exports.ArrayPrototypeFilter = (arr, fn) => arr.filter(fn) exports.ArrayPrototypeForEach = (arr, fn, thisArg) => arr.forEach(fn, thisArg) exports.ArrayPrototypeIncludes = (arr, el, fromIndex) => arr.includes(el, fromIndex) exports.ArrayPrototypeJoin = (arr, str) => arr.join(str) +exports.ArrayPrototypeMap = (arr, mapFn) => arr.map(mapFn) exports.ArrayPrototypePush = (arr, ...el) => arr.push(...el) exports.ArrayPrototypeReduce = (arr, fn, originalVal) => arr.reduce(fn, originalVal) exports.ArrayPrototypeShift = arr => arr.shift() @@ -27,6 +28,7 @@ exports.ObjectFreeze = obj => Object.freeze(obj) exports.ObjectGetOwnPropertyDescriptor = (obj, key) => Object.getOwnPropertyDescriptor(obj, key) exports.ObjectIsExtensible = obj => Object.isExtensible(obj) exports.ObjectPrototypeHasOwnProperty = (obj, property) => Object.prototype.hasOwnProperty.call(obj, property) +exports.ObjectSeal = (obj) => Object.seal(obj) exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args) exports.Promise = Promise exports.PromiseAll = iterator => Promise.all(iterator) @@ -39,11 +41,13 @@ exports.SafePromiseAll = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn) exports.SafePromiseRace = (array, mapFn) => Promise.race(mapFn ? array.map(mapFn) : array) exports.SafeSet = Set exports.SafeWeakMap = WeakMap +exports.StringPrototypeIncludes = (str, needle) => str.includes(needle) exports.StringPrototypeMatch = (str, reg) => str.match(reg) exports.StringPrototypeReplace = (str, search, replacement) => str.replace(search, replacement) exports.StringPrototypeReplaceAll = replaceAll exports.StringPrototypeStartsWith = (haystack, needle, index) => haystack.startsWith(needle, index) +exports.StringPrototypeSlice = (str, ...args) => str.slice(...args) exports.StringPrototypeSplit = (str, search, limit) => str.split(search, limit) exports.Symbol = Symbol exports.SymbolFor = repr => Symbol.for(repr) diff --git a/lib/internal/test_runner/harness.js b/lib/internal/test_runner/harness.js index 1780178..4d6e9c4 100644 --- a/lib/internal/test_runner/harness.js +++ b/lib/internal/test_runner/harness.js @@ -1,4 +1,4 @@ -// https://github.com/nodejs/node/blob/26e27424ad91c60a44d3d4c58b62a39b555ba75d/lib/internal/test_runner/harness.js +// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/lib/internal/test_runner/harness.js 'use strict' const { ArrayPrototypeForEach, @@ -177,8 +177,19 @@ function runInParentContext (Factory) { return cb } +function hook (hook) { + return (fn, options) => { + const parent = testResources.get(executionAsyncId()) || setup(root) + parent.createHook(hook, fn, options) + } +} + module.exports = { test: FunctionPrototypeBind(test, root), describe: runInParentContext(Suite), - it: runInParentContext(ItTest) + it: runInParentContext(ItTest), + before: hook('before'), + after: hook('after'), + beforeEach: hook('beforeEach'), + afterEach: hook('afterEach') } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index e7aaf9c..b504105 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1,14 +1,17 @@ -// https://github.com/nodejs/node/blob/a3e110820ff98702e1761831e7beaf0f5f1f75e7/lib/internal/test_runner/test.js +// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/lib/internal/test_runner/test.js 'use strict' const { ArrayPrototypePush, + ArrayPrototypeReduce, ArrayPrototypeShift, + ArrayPrototypeSlice, ArrayPrototypeUnshift, FunctionPrototype, MathMax, Number, + ObjectSeal, PromisePrototypeThen, PromiseResolve, ReflectApply, @@ -25,12 +28,11 @@ const { ERR_INVALID_ARG_TYPE, ERR_TEST_FAILURE }, - kIsNodeError, AbortError } = require('#internal/errors') const { getOptionValue } = require('#internal/options') const { TapStream } = require('#internal/test_runner/tap_stream') -const { createDeferredCallback } = require('#internal/test_runner/utils') +const { createDeferredCallback, isTestFailureError } = require('#internal/test_runner/utils') const { createDeferredPromise, kEmptyObject @@ -39,6 +41,7 @@ const { isPromise } = require('#internal/util/types') const { validateAbortSignal, validateNumber, + validateOneOf, validateUint32 } = require('#internal/validators') const { setTimeout } = require('#timers/promises') @@ -51,6 +54,7 @@ const kParentAlreadyFinished = 'parentAlreadyFinished' const kSubtestsFailed = 'subtestsFailed' const kTestCodeFailure = 'testCodeFailure' const kTestTimeoutFailure = 'testTimeoutFailure' +const kHookFailure = 'hookFailed' const kDefaultIndent = ' ' const kDefaultTimeout = null const noop = FunctionPrototype @@ -59,6 +63,8 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only') // TODO(cjihrig): Use uv_available_parallelism() once it lands. const rootConcurrency = isTestRunner ? MathMax(cpus().length - 1, 1) : 1 const kShouldAbort = Symbol('kShouldAbort') +const kRunHook = Symbol('kRunHook') +const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']) function stopTest (timeout, signal) { if (timeout === kDefaultTimeout) { @@ -83,6 +89,10 @@ class TestContext { return this.#test.signal } + get name () { + return this.#test.name + } + diagnostic (message) { this.#test.diagnostic(message) } @@ -105,6 +115,14 @@ class TestContext { return subtest.start() } + + beforeEach (fn, options) { + this.#test.createHook('beforeEach', fn, options) + } + + afterEach (fn, options) { + this.#test.createHook('afterEach', fn, options) + } } class Test extends AsyncResource { @@ -209,6 +227,12 @@ class Test extends AsyncResource { this.pendingSubtests = [] this.readySubtests = new SafeMap() this.subtests = [] + this.hooks = { + before: [], + after: [], + beforeEach: [], + afterEach: [] + } this.waitingOn = 0 this.finished = false } @@ -327,10 +351,19 @@ 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 this.#abortController.abort() } + createHook (name, fn, options) { + validateOneOf(name, 'hook name', kHookNames) + // eslint-disable-next-line no-use-before-define + const hook = new TestHook(fn, options) + ArrayPrototypePush(this.hooks[name], hook) + return hook + } + fail (err) { if (this.error !== null) { return @@ -394,8 +427,27 @@ class Test extends AsyncResource { return { ctx, args: [ctx] } } + async [kRunHook] (hook, args) { + validateOneOf(hook, 'hook name', kHookNames) + try { + await ArrayPrototypeReduce(this.hooks[hook], async (prev, hook) => { + await prev + await hook.run(args) + if (hook.error) { + throw hook.error + } + }, PromiseResolve()) + } catch (err) { + const error = new ERR_TEST_FAILURE(`failed running ${hook} hook`, kHookFailure) + error.cause = isTestFailureError(err) ? err.cause : err + throw error + } + } + async run () { - this.parent.activeSubtests++ + if (this.parent !== null) { + this.parent.activeSubtests++ + } this.startTime = hrtime() if (this[kShouldAbort]()) { @@ -404,16 +456,20 @@ class Test extends AsyncResource { } try { - const stopPromise = stopTest(this.timeout, this.signal) const { args, ctx } = this.getRunArgs() - ArrayPrototypeUnshift(args, this.fn, ctx) // Note that if it's not OK to mutate args, we need to first clone it. + if (this.parent?.hooks.beforeEach.length > 0) { + await this.parent[kRunHook]('beforeEach', { args, ctx }) + } + const stopPromise = stopTest(this.timeout, this.signal) + const runArgs = ArrayPrototypeSlice(args) + ArrayPrototypeUnshift(runArgs, this.fn, ctx) - if (this.fn.length === args.length - 1) { + if (this.fn.length === runArgs.length - 1) { // This test is using legacy Node.js error first callbacks. const { promise, cb } = createDeferredCallback() - ArrayPrototypePush(args, cb) - const ret = ReflectApply(this.runInAsyncScope, this, args) + ArrayPrototypePush(runArgs, cb) + const ret = ReflectApply(this.runInAsyncScope, this, runArgs) if (isPromise(ret)) { this.fail(new ERR_TEST_FAILURE( @@ -426,7 +482,7 @@ class Test extends AsyncResource { } } else { // This test is synchronous or using Promises. - const promise = ReflectApply(this.runInAsyncScope, this, args) + const promise = ReflectApply(this.runInAsyncScope, this, runArgs) await SafePromiseRace([PromiseResolve(promise), stopPromise]) } @@ -435,9 +491,13 @@ class Test extends AsyncResource { return } + if (this.parent?.hooks.afterEach.length > 0) { + await this.parent[kRunHook]('afterEach', { args, ctx }) + } + this.pass() } catch (err) { - if (err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err) { + if (isTestFailureError(err)) { if (err.failureType === kTestTimeoutFailure) { this.cancel(err) } else { @@ -551,10 +611,30 @@ class Test extends AsyncResource { } } +class TestHook extends Test { + #args + constructor (fn, options) { + if (options === null || typeof options !== 'object') { + options = kEmptyObject + } + const { timeout, signal } = options + super({ __proto__: null, fn, timeout, signal }) + } + + run (args) { + this.#args = args + return super.run() + } + + getRunArgs () { + return this.#args + } +} + class ItTest extends Test { constructor (opt) { super(opt) } // eslint-disable-line no-useless-constructor getRunArgs () { - return { ctx: { signal: this.signal }, args: [] } + return { ctx: { signal: this.signal, name: this.name }, args: [] } } } class Suite extends Test { @@ -562,9 +642,9 @@ class Suite extends Test { super(options) try { - const context = { signal: this.signal } + const { ctx, args } = this.getRunArgs() this.buildSuite = PromisePrototypeThen( - PromiseResolve(this.runInAsyncScope(this.fn, context, [context])), + PromiseResolve(this.runInAsyncScope(this.fn, ctx, args)), undefined, (err) => { this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)) @@ -576,23 +656,39 @@ class Suite extends Test { this.buildPhaseFinished = true } + getRunArgs () { + return { ctx: { signal: this.signal, name: this.name }, args: [] } + } + async run () { - this.parent.activeSubtests++ - await this.buildSuite - this.startTime = hrtime() + try { + this.parent.activeSubtests++ + await this.buildSuite + this.startTime = hrtime() - if (this[kShouldAbort]()) { - this.subtests = [] - this.postRun() - return - } + if (this[kShouldAbort]()) { + this.subtests = [] + this.postRun() + return + } + + const hookArgs = this.getRunArgs() + await this[kRunHook]('before', hookArgs) + const stopPromise = stopTest(this.timeout, this.signal) + const subtests = this.skipped || this.error ? [] : this.subtests + const promise = SafePromiseAll(subtests, (subtests) => subtests.start()) - const stopPromise = stopTest(this.timeout, this.signal) - const subtests = this.skipped || this.error ? [] : this.subtests - const promise = SafePromiseAll(subtests, (subtests) => subtests.start()) + await SafePromiseRace([promise, stopPromise]) + await this[kRunHook]('after', hookArgs) + this.pass() + } catch (err) { + if (isTestFailureError(err)) { + this.fail(err) + } else { + this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure)) + } + } - await SafePromiseRace([promise, stopPromise]) - this.pass() this.postRun() } } diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 647ec75..595431e 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -1,4 +1,4 @@ -// https://github.com/nodejs/node/blob/e2225ba8e1c00995c0f8bd56e607ea7c5b463ab9/lib/internal/test_runner/utils.js +// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/lib/internal/test_runner/utils.js 'use strict' const { RegExpPrototypeExec } = require('#internal/per_context/primordials') const { basename } = require('path') @@ -6,7 +6,8 @@ const { createDeferredPromise } = require('#internal/util') const { codes: { ERR_TEST_FAILURE - } + }, + kIsNodeError } = require('#internal/errors') const kMultipleCallbackInvocations = 'multipleCallbackInvocations' @@ -50,8 +51,13 @@ function createDeferredCallback () { return { promise, cb } } +function isTestFailureError (err) { + return err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err +} + module.exports = { createDeferredCallback, doesPathMatchFilter, - isSupportedFileType + isSupportedFileType, + isTestFailureError } diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 062e37d..511e61b 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -1,6 +1,12 @@ // https://github.com/nodejs/node/blob/60da0a1b364efdd84870269d23b39faa12fb46d8/lib/internal/validators.js +const { + ArrayPrototypeIncludes, + ArrayPrototypeJoin, + ArrayPrototypeMap +} = require('#internal/per_context/primordials') const { ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE } = require('#internal/errors').codes @@ -47,9 +53,21 @@ const validateUint32 = (value, name, positive) => { } } +const validateOneOf = (value, name, oneOf) => { + if (!ArrayPrototypeIncludes(oneOf, value)) { + const allowed = ArrayPrototypeJoin( + ArrayPrototypeMap(oneOf, (v) => + (typeof v === 'string' ? `'${v}'` : String(v))), + ', ') + const reason = 'must be one of: ' + allowed + throw new ERR_INVALID_ARG_VALUE(name, value, reason) + } +} + module.exports = { isUint32, validateAbortSignal, validateNumber, + validateOneOf, validateUint32 } diff --git a/lib/test.js b/lib/test.js index 964c97d..a65fb3c 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,10 +1,12 @@ -// https://github.com/nodejs/node/blob/e2225ba8e1c00995c0f8bd56e607ea7c5b463ab9/lib/test.js - +// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/lib/test.js 'use strict' - -const { test, describe, it } = require('#internal/test_runner/harness') +const { test, describe, it, before, after, beforeEach, afterEach } = require('#internal/test_runner/harness') module.exports = test module.exports.test = test module.exports.describe = describe module.exports.it = it +module.exports.before = before +module.exports.after = after +module.exports.beforeEach = beforeEach +module.exports.afterEach = afterEach diff --git a/test/message.js b/test/message.js old mode 100644 new mode 100755 index 130ea08..ea35fb1 --- a/test/message.js +++ b/test/message.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node 'use strict' const { createReadStream, promises: fs } = require('node:fs') diff --git a/test/message/test_runner_desctibe_it.js b/test/message/test_runner_desctibe_it.js index 1690310..d8c1bb3 100644 --- a/test/message/test_runner_desctibe_it.js +++ b/test/message/test_runner_desctibe_it.js @@ -1,4 +1,4 @@ -// https://github.com/nodejs/node/blob/a3e110820ff98702e1761831e7beaf0f5f1f75e7/test/message/test_runner_desctibe_it.js +// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/test/message/test_runner_desctibe_it.js // Flags: --no-warnings 'use strict' require('../common') @@ -213,15 +213,15 @@ it('callback fail', (done) => { }) it('sync t is this in test', function () { - assert.deepStrictEqual(this, { signal: this.signal }) + assert.deepStrictEqual(this, { signal: this.signal, name: this.name }) }) it('async t is this in test', async function () { - assert.deepStrictEqual(this, { signal: this.signal }) + assert.deepStrictEqual(this, { signal: this.signal, name: this.name }) }) it('callback t is this in test', function (done) { - assert.deepStrictEqual(this, { signal: this.signal }) + assert.deepStrictEqual(this, { signal: this.signal, name: this.name }) done() }) diff --git a/test/message/test_runner_hooks.js b/test/message/test_runner_hooks.js new file mode 100644 index 0000000..836ef0e --- /dev/null +++ b/test/message/test_runner_hooks.js @@ -0,0 +1,112 @@ +// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/test/message/test_runner_hooks.js +// Flags: --no-warnings +'use strict' +require('../common') +const assert = require('assert') +const { test, describe, it, before, after, beforeEach, afterEach } = require('#node:test') + +describe('describe hooks', () => { + const testArr = [] + before(function () { + testArr.push('before ' + this.name) + }) + after(function () { + testArr.push('after ' + this.name) + assert.deepStrictEqual(testArr, [ + 'before describe hooks', + 'beforeEach 1', '1', 'afterEach 1', + 'beforeEach 2', '2', 'afterEach 2', + 'before nested', + 'beforeEach nested 1', 'nested 1', 'afterEach nested 1', + 'beforeEach nested 2', 'nested 2', 'afterEach nested 2', + 'after nested', + 'after describe hooks' + ]) + }) + beforeEach(function () { + testArr.push('beforeEach ' + this.name) + }) + afterEach(function () { + testArr.push('afterEach ' + this.name) + }) + + it('1', () => testArr.push('1')) + it('2', () => testArr.push('2')) + + describe('nested', () => { + before(function () { + testArr.push('before ' + this.name) + }) + after(function () { + testArr.push('after ' + this.name) + }) + beforeEach(function () { + testArr.push('beforeEach ' + this.name) + }) + afterEach(function () { + testArr.push('afterEach ' + this.name) + }) + it('nested 1', () => testArr.push('nested 1')) + it('nested 2', () => testArr.push('nested 2')) + }) +}) + +describe('before throws', () => { + before(() => { throw new Error('before') }) + it('1', () => {}) + it('2', () => {}) +}) + +describe('after throws', () => { + after(() => { throw new Error('after') }) + it('1', () => {}) + it('2', () => {}) +}) + +describe('beforeEach throws', () => { + beforeEach(() => { throw new Error('beforeEach') }) + it('1', () => {}) + it('2', () => {}) +}) + +describe('afterEach throws', () => { + afterEach(() => { throw new Error('afterEach') }) + it('1', () => {}) + it('2', () => {}) +}) + +test('test hooks', async (t) => { + const testArr = [] + t.beforeEach((t) => testArr.push('beforeEach ' + t.name)) + t.afterEach((t) => testArr.push('afterEach ' + t.name)) + await t.test('1', () => testArr.push('1')) + await t.test('2', () => testArr.push('2')) + + await t.test('nested', async (t) => { + t.beforeEach((t) => testArr.push('nested beforeEach ' + t.name)) + t.afterEach((t) => testArr.push('nested afterEach ' + t.name)) + await t.test('nested 1', () => testArr.push('nested1')) + await t.test('nested 2', () => testArr.push('nested 2')) + }) + + assert.deepStrictEqual(testArr, [ + 'beforeEach 1', '1', 'afterEach 1', + 'beforeEach 2', '2', 'afterEach 2', + 'beforeEach nested', + 'nested beforeEach nested 1', 'nested1', 'nested afterEach nested 1', + 'nested beforeEach nested 2', 'nested 2', 'nested afterEach nested 2', + 'afterEach nested' + ]) +}) + +test('t.beforeEach throws', async (t) => { + t.beforeEach(() => { throw new Error('beforeEach') }) + await t.test('1', () => {}) + await t.test('2', () => {}) +}) + +test('t.afterEach throws', async (t) => { + t.afterEach(() => { throw new Error('afterEach') }) + await t.test('1', () => {}) + await t.test('2', () => {}) +}) diff --git a/test/message/test_runner_hooks.out b/test/message/test_runner_hooks.out new file mode 100644 index 0000000..cd85fe9 --- /dev/null +++ b/test/message/test_runner_hooks.out @@ -0,0 +1,229 @@ +TAP version 13 +# Subtest: describe hooks + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + # Subtest: nested + # Subtest: nested 1 + ok 1 - nested 1 + --- + duration_ms: * + ... + # Subtest: nested 2 + ok 2 - nested 2 + --- + duration_ms: * + ... + 1..2 + ok 3 - nested + --- + duration_ms: * + ... + 1..3 +ok 1 - describe hooks + --- + duration_ms: * + ... +# Subtest: before throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + failureType: 'cancelledByParent' + error: 'test did not finish before its parent and was cancelled' + code: 'ERR_TEST_FAILURE' + ... + 1..2 +not ok 2 - before throws + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running before hook' + code: 'ERR_TEST_FAILURE' + ... +# Subtest: after throws + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + 1..2 +not ok 3 - after throws + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running after hook' + code: 'ERR_TEST_FAILURE' + ... +# Subtest: beforeEach throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running beforeEach hook' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running beforeEach hook' + code: 'ERR_TEST_FAILURE' + ... + 1..2 +not ok 4 - beforeEach throws + --- + duration_ms: * + failureType: 'subtestsFailed' + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + ... +# Subtest: afterEach throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running afterEach hook' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running afterEach hook' + code: 'ERR_TEST_FAILURE' + ... + 1..2 +not ok 5 - afterEach throws + --- + duration_ms: * + failureType: 'subtestsFailed' + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + ... +# Subtest: test hooks + # Subtest: 1 + ok 1 - 1 + --- + duration_ms: * + ... + # Subtest: 2 + ok 2 - 2 + --- + duration_ms: * + ... + # Subtest: nested + # Subtest: nested 1 + ok 1 - nested 1 + --- + duration_ms: * + ... + # Subtest: nested 2 + ok 2 - nested 2 + --- + duration_ms: * + ... + 1..2 + ok 3 - nested + --- + duration_ms: * + ... + 1..3 +ok 6 - test hooks + --- + duration_ms: * + ... +# Subtest: t.beforeEach throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running beforeEach hook' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running beforeEach hook' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... + 1..2 +not ok 7 - t.beforeEach throws + --- + duration_ms: * + failureType: 'subtestsFailed' + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + ... +# Subtest: t.afterEach throws + # Subtest: 1 + not ok 1 - 1 + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running afterEach hook' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... + # Subtest: 2 + not ok 2 - 2 + --- + duration_ms: * + failureType: 'hookFailed' + error: 'failed running afterEach hook' + code: 'ERR_TEST_FAILURE' + stack: |- + * + ... + 1..2 +not ok 8 - t.afterEach throws + --- + duration_ms: * + failureType: 'subtestsFailed' + error: '2 subtests failed' + code: 'ERR_TEST_FAILURE' + ... +1..8 +# tests 8 +# pass 2 +# fail 6 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms *