test_runner: add global setup and teardown functionality
Some checks are pending
Coverage Linux (without intl) / coverage-linux-without-intl (push) Waiting to run
Coverage Linux / coverage-linux (push) Waiting to run
Coverage Windows / coverage-windows (push) Waiting to run
Test and upload documentation to artifacts / build-docs (push) Waiting to run
Linters / lint-addon-docs (push) Waiting to run
Linters / lint-cpp (push) Waiting to run
Linters / format-cpp (push) Waiting to run
Linters / lint-js-and-md (push) Waiting to run
Linters / lint-py (push) Waiting to run
Linters / lint-yaml (push) Waiting to run
Linters / lint-sh (push) Waiting to run
Linters / lint-codeowners (push) Waiting to run
Linters / lint-pr-url (push) Waiting to run
Linters / lint-readme (push) Waiting to run
Notify on Push / Notify on Force Push on `main` (push) Waiting to run
Notify on Push / Notify on Push on `main` that lacks metadata (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run

PR-URL: https://github.com/nodejs/node/pull/57438
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
This commit is contained in:
Pietro Marchini 2025-04-16 19:51:06 +02:00 committed by GitHub
parent e800f009f6
commit cb5f671a34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1270 additions and 10 deletions

View File

@ -2452,6 +2452,19 @@ added:
Configures the test runner to exit the process once all known tests have
finished executing even if the event loop would otherwise remain active.
### `--test-global-setup=module`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development
Specify a module that will be evaluated before all tests are executed and
can be used to setup global state or fixtures for tests.
See the documentation on [global setup and teardown][] for more details.
### `--test-isolation=mode`
<!-- YAML
@ -3347,6 +3360,7 @@ one is included in the list below.
* `--test-coverage-functions`
* `--test-coverage-include`
* `--test-coverage-lines`
* `--test-global-setup`
* `--test-isolation`
* `--test-name-pattern`
* `--test-only`
@ -3898,6 +3912,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[emit_warning]: process.md#processemitwarningwarning-options
[environment_variables]: #environment-variables
[filtering tests by name]: test.md#filtering-tests-by-name
[global setup and teardown]: test.md#global-setup-and-teardown
[jitless]: https://v8.dev/blog/jitless
[libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html
[module compile cache]: module.md#module-compile-cache

View File

@ -397,6 +397,60 @@ their dependencies. When a change is detected, the test runner will
rerun the tests affected by the change.
The test runner will continue to run until the process is terminated.
## Global setup and teardown
<!-- YAML
added: REPLACEME
-->
> Stability: 1.0 - Early development
The test runner supports specifying a module that will be evaluated before all tests are executed and
can be used to setup global state or fixtures for tests. This is useful for preparing resources or setting up
shared state that is required by multiple tests.
This module can export any of the following:
* A `globalSetup` function which runs once before all tests start
* A `globalTeardown` function which runs once after all tests complete
The module is specified using the `--test-global-setup` flag when running tests from the command line.
```cjs
// setup-module.js
async function globalSetup() {
// Setup shared resources, state, or environment
console.log('Global setup executed');
// Run servers, create files, prepare databases, etc.
}
async function globalTeardown() {
// Clean up resources, state, or environment
console.log('Global teardown executed');
// Close servers, remove files, disconnect from databases, etc.
}
module.exports = { globalSetup, globalTeardown };
```
```mjs
// setup-module.mjs
export async function globalSetup() {
// Setup shared resources, state, or environment
console.log('Global setup executed');
// Run servers, create files, prepare databases, etc.
}
export async function globalTeardown() {
// Clean up resources, state, or environment
console.log('Global teardown executed');
// Close servers, remove files, disconnect from databases, etc.
}
```
If the global setup function throws an error, no tests will be run and the process will exit with a non-zero exit code.
The global teardown function will not be called in this case.
## Running tests from the command line
The Node.js test runner can be invoked from the command line by passing the

View File

@ -392,6 +392,9 @@
"test-coverage-lines": {
"type": "number"
},
"test-global-setup": {
"type": "string"
},
"test-isolation": {
"type": "string"
},

View File

@ -464,6 +464,9 @@ Require a minimum threshold for line coverage (0 - 100).
Configures the test runner to exit the process once all known tests have
finished executing even if the event loop would otherwise remain active.
.
.It Fl -test-global-setup
Specifies a module containing global setup and teardown functions for the test runner.
.
.It Fl -test-isolation Ns = Ns Ar mode
Configures the type of test isolation used in the test runner.
.

View File

@ -25,14 +25,15 @@ const {
parseCommandLine,
reporterScope,
shouldColorizeTestFiles,
setupGlobalSetupTeardownFunctions,
} = require('internal/test_runner/utils');
const { queueMicrotask } = require('internal/process/task_queues');
const { TIMEOUT_MAX } = require('internal/timers');
const { clearInterval, setInterval } = require('timers');
const { bigint: hrtime } = process.hrtime;
const resolvedPromise = PromiseResolve();
const testResources = new SafeMap();
let globalRoot;
let globalSetupExecuted = false;
testResources.set(reporterScope.asyncId(), reporterScope);
@ -47,7 +48,6 @@ function createTestTree(rootTestOptions, globalOptions) {
buildPromise: buildPhaseDeferred.promise,
buildSuites: [],
isWaitingForBuildPhase: false,
bootstrapPromise: resolvedPromise,
watching: false,
config: globalOptions,
coverage: null,
@ -71,6 +71,21 @@ function createTestTree(rootTestOptions, globalOptions) {
snapshotManager: null,
isFilteringByName,
isFilteringByOnly,
async runBootstrap() {
if (globalSetupExecuted) {
return PromiseResolve();
}
globalSetupExecuted = true;
const globalSetupFunctions = await setupGlobalSetupTeardownFunctions(
globalOptions.globalSetupPath,
globalOptions.cwd,
);
harness.globalTeardownFunction = globalSetupFunctions.globalTeardownFunction;
if (typeof globalSetupFunctions.globalSetupFunction === 'function') {
return globalSetupFunctions.globalSetupFunction();
}
return PromiseResolve();
},
async waitForBuildPhase() {
if (harness.buildSuites.length > 0) {
await SafePromiseAllReturnVoid(harness.buildSuites);
@ -81,6 +96,7 @@ function createTestTree(rootTestOptions, globalOptions) {
};
harness.resetCounters();
harness.bootstrapPromise = harness.runBootstrap();
globalRoot = new Test({
__proto__: null,
...rootTestOptions,
@ -232,6 +248,11 @@ function setupProcessState(root, globalOptions) {
'Promise resolution is still pending but the event loop has already resolved',
kCancelledByParent));
if (root.harness.globalTeardownFunction) {
await root.harness.globalTeardownFunction();
root.harness.globalTeardownFunction = null;
}
hook.disable();
process.removeListener('uncaughtException', exceptionHandler);
process.removeListener('unhandledRejection', rejectionHandler);
@ -278,7 +299,10 @@ function lazyBootstrapRoot() {
process.exitCode = kGenericUserError;
}
});
globalRoot.harness.bootstrapPromise = globalOptions.setup(globalRoot.reporter);
globalRoot.harness.bootstrapPromise = SafePromiseAllReturnVoid([
globalRoot.harness.bootstrapPromise,
globalOptions.setup(globalRoot.reporter),
]);
}
return globalRoot;
}

View File

@ -87,6 +87,7 @@ const {
} = require('internal/test_runner/utils');
const { Glob } = require('internal/fs/glob');
const { once } = require('events');
const { validatePath } = require('internal/fs/utils');
const {
triggerUncaughtException,
exitCodes: { kGenericUserError },
@ -559,6 +560,7 @@ function run(options = kEmptyObject) {
isolation = 'process',
watch,
setup,
globalSetupPath,
only,
globPatterns,
coverage = false,
@ -668,6 +670,10 @@ function run(options = kEmptyObject) {
validateStringArray(argv, 'options.argv');
validateStringArray(execArgv, 'options.execArgv');
if (globalSetupPath != null) {
validatePath(globalSetupPath, 'options.globalSetupPath');
}
const rootTestOptions = { __proto__: null, concurrency, timeout, signal };
const globalOptions = {
__proto__: null,
@ -682,6 +688,7 @@ function run(options = kEmptyObject) {
branchCoverage: branchCoverage,
functionCoverage: functionCoverage,
cwd,
globalSetupPath,
};
const root = createTestTree(rootTestOptions, globalOptions);
let testFiles = files ?? createTestFileList(globPatterns, cwd);
@ -754,7 +761,9 @@ function run(options = kEmptyObject) {
const cascadedLoader = esmLoader.getOrInitializeCascadedLoader();
let topLevelTestCount = 0;
root.harness.bootstrapPromise = promise;
root.harness.bootstrapPromise = root.harness.bootstrapPromise ?
SafePromiseAllReturnVoid([root.harness.bootstrapPromise, promise]) :
promise;
const userImports = getOptionValue('--import');
for (let i = 0; i < userImports.length; i++) {
@ -799,12 +808,15 @@ function run(options = kEmptyObject) {
debug('beginning test execution');
root.entryFile = null;
finishBootstrap();
root.processPendingSubtests();
return root.processPendingSubtests();
};
}
}
const runChain = async () => {
if (root.harness?.bootstrapPromise) {
await root.harness.bootstrapPromise;
}
if (typeof setup === 'function') {
await setup(root.reporter);
}

View File

@ -27,7 +27,7 @@ const {
} = primordials;
const { AsyncResource } = require('async_hooks');
const { relative, sep } = require('path');
const { relative, sep, resolve } = require('path');
const { createWriteStream } = require('fs');
const { pathToFileURL } = require('internal/url');
const { getOptionValue } = require('internal/options');
@ -41,7 +41,12 @@ const {
kIsNodeError,
} = require('internal/errors');
const { compose } = require('stream');
const { validateInteger } = require('internal/validators');
const {
validateInteger,
validateFunction,
} = require('internal/validators');
const { validatePath } = require('internal/fs/utils');
const { kEmptyObject } = require('internal/util');
const coverageColors = {
__proto__: null,
@ -199,6 +204,7 @@ function parseCommandLine() {
const timeout = getOptionValue('--test-timeout') || Infinity;
const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child';
const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8';
let globalSetupPath;
let concurrency;
let coverageExcludeGlobs;
let coverageIncludeGlobs;
@ -223,6 +229,7 @@ function parseCommandLine() {
} else {
destinations = getOptionValue('--test-reporter-destination');
reporters = getOptionValue('--test-reporter');
globalSetupPath = getOptionValue('--test-global-setup');
if (reporters.length === 0 && destinations.length === 0) {
ArrayPrototypePush(reporters, kDefaultReporter);
}
@ -328,6 +335,7 @@ function parseCommandLine() {
only,
reporters,
setup,
globalSetupPath,
shard,
sourceMaps,
testNamePatterns,
@ -597,6 +605,27 @@ function getCoverageReport(pad, summary, symbol, color, table) {
return report;
}
async function setupGlobalSetupTeardownFunctions(globalSetupPath, cwd) {
let globalSetupFunction;
let globalTeardownFunction;
if (globalSetupPath) {
validatePath(globalSetupPath, 'options.globalSetupPath');
const fileURL = pathToFileURL(resolve(cwd, globalSetupPath));
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const globalSetupModule = await cascadedLoader
.import(fileURL, pathToFileURL(cwd + sep).href, kEmptyObject);
if (globalSetupModule.globalSetup) {
validateFunction(globalSetupModule.globalSetup, 'globalSetupModule.globalSetup');
globalSetupFunction = globalSetupModule.globalSetup;
}
if (globalSetupModule.globalTeardown) {
validateFunction(globalSetupModule.globalTeardown, 'globalSetupModule.globalTeardown');
globalTeardownFunction = globalSetupModule.globalTeardown;
}
}
return { __proto__: null, globalSetupFunction, globalTeardownFunction };
}
module.exports = {
convertStringToRegExp,
countCompletedTest,
@ -607,4 +636,5 @@ module.exports = {
reporterScope,
shouldColorizeTestFiles,
getCoverageReport,
setupGlobalSetupTeardownFunctions,
};

View File

@ -761,6 +761,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"exclude files from coverage report that match this glob pattern",
&EnvironmentOptions::coverage_exclude_pattern,
kAllowedInEnvvar);
AddOption("--test-global-setup",
"specifies the path to the global setup file",
&EnvironmentOptions::test_global_setup_path,
kAllowedInEnvvar);
AddOption("--test-udp-no-try-send", "", // For testing only.
&EnvironmentOptions::test_udp_no_try_send);
AddOption("--throw-deprecation",

View File

@ -197,6 +197,7 @@ class EnvironmentOptions : public Options {
std::vector<std::string> test_name_pattern;
std::vector<std::string> test_reporter;
std::vector<std::string> test_reporter_destination;
std::string test_global_setup_path;
bool test_only = false;
bool test_udp_no_try_send = false;
std::string test_isolation = "process";

View File

@ -0,0 +1,13 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
test('Another test that verifies setup flag existance', (t) => {
const setupFlagPath = process.env.SETUP_FLAG_PATH;
assert.ok(fs.existsSync(setupFlagPath), 'Setup flag file should exist');
const content = fs.readFileSync(setupFlagPath, 'utf8');
assert.strictEqual(content, 'Setup was executed');
});

View File

@ -0,0 +1,26 @@
'use strict';
const fs = require('node:fs');
const { setTimeout } = require('node:timers/promises');
const asyncFlagPath = process.env.ASYNC_FLAG_PATH;
async function globalSetup() {
console.log('Async setup starting');
await setTimeout(500);
fs.writeFileSync(asyncFlagPath, 'Setup part');
console.log('Async setup completed');
}
async function globalTeardown() {
console.log('Async teardown starting');
await setTimeout(100);
fs.appendFileSync(asyncFlagPath, ', Teardown part');
console.log('Async teardown completed');
}
module.exports = { globalSetup, globalTeardown };

View File

@ -0,0 +1,20 @@
'use strict';
const fs = require('node:fs');
// Path for temporary file to track execution
const setupFlagPath = process.env.SETUP_FLAG_PATH;
const teardownFlagPath = process.env.TEARDOWN_FLAG_PATH;
async function globalSetup() {
console.log('Global setup executed');
fs.writeFileSync(setupFlagPath, 'Setup was executed');
}
async function globalTeardown() {
console.log('Global teardown executed');
fs.writeFileSync(teardownFlagPath, 'Teardown was executed');
fs.rmSync(setupFlagPath, { force: true });
}
module.exports = { globalSetup, globalTeardown };

View File

@ -0,0 +1,18 @@
import * as fs from 'node:fs';
// Path for temporary file to track execution
const setupFlagPath = process.env.SETUP_FLAG_PATH;
const teardownFlagPath = process.env.TEARDOWN_FLAG_PATH;
async function globalSetup() {
console.log('Global setup executed');
fs.writeFileSync(setupFlagPath, 'Setup was executed');
}
async function globalTeardown() {
console.log('Global teardown executed');
fs.writeFileSync(teardownFlagPath, 'Teardown was executed');
fs.rmSync(setupFlagPath, { force: true });
}
export { globalSetup, globalTeardown };

View File

@ -0,0 +1,24 @@
import * as fs from 'node:fs';
// Path for temporary file to track execution
const setupFlagPath = process.env.SETUP_FLAG_PATH;
const teardownFlagPath = process.env.TEARDOWN_FLAG_PATH;
async function globalSetup(): Promise<void> {
console.log('Global setup executed');
if (setupFlagPath) {
fs.writeFileSync(setupFlagPath, 'Setup was executed');
}
}
async function globalTeardown(): Promise<void> {
console.log('Global teardown executed');
if (teardownFlagPath) {
fs.writeFileSync(teardownFlagPath, 'Teardown was executed');
}
if (setupFlagPath) {
fs.rmSync(setupFlagPath, { force: true });
}
}
export { globalSetup, globalTeardown };

View File

@ -0,0 +1,11 @@
'use strict';
async function globalSetup() {
throw new Error('Deliberate error in global setup');
}
async function globalTeardown() {
console.log('Teardown should not run if setup fails');
}
module.exports = { globalSetup, globalTeardown };

View File

@ -0,0 +1,4 @@
import { test } from 'node:test';
test('Imported module Ok', () => {});
test('Imported module Fail', () => { throw new Error('fail'); });

View File

@ -0,0 +1 @@
console.log('Imported module executed');

View File

@ -0,0 +1 @@
console.log('Required module executed');

View File

@ -0,0 +1,12 @@
'use strict';
const fs = require('node:fs');
const setupFlagPath = process.env.SETUP_ONLY_FLAG_PATH;
async function globalSetup() {
console.log('Setup-only module executed');
fs.writeFileSync(setupFlagPath, 'Setup-only was executed');
}
module.exports = { globalSetup };

View File

@ -0,0 +1,12 @@
'use strict';
const fs = require('node:fs');
const teardownFlagPath = process.env.TEARDOWN_ONLY_FLAG_PATH;
async function globalTeardown() {
console.log('Teardown-only module executed');
fs.writeFileSync(teardownFlagPath, 'Teardown-only was executed');
}
module.exports = { globalTeardown };

View File

@ -0,0 +1,17 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
test('verify setup was executed', (t) => {
const setupFlagPath = process.env.SETUP_FLAG_PATH;
assert.ok(fs.existsSync(setupFlagPath), 'Setup flag file should exist');
const content = fs.readFileSync(setupFlagPath, 'utf8');
assert.strictEqual(content, 'Setup was executed');
});
test('another simple test', (t) => {
assert.ok(true);
});

View File

@ -0,0 +1,35 @@
import { run } from 'node:test';
import { spec } from 'node:test/reporters';
import { parseArgs } from 'node:util';
const options = {
file: {
type: 'string',
},
globalSetup: {
type: 'string',
},
isolation: {
type: 'string',
},
};
const {
values,
} = parseArgs({ args: process.argv.slice(2), options });
let files;
let globalSetupPath;
if (values.file) {
files = [values.file];
}
if (values.globalSetup) {
globalSetupPath = values.globalSetup;
}
run({
files,
globalSetupPath,
}).compose(spec).pipe(process.stdout);

View File

@ -0,0 +1,568 @@
import '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import { spawn } from 'node:child_process';
import tmpdir from '../common/tmpdir.js';
import { once } from 'node:events';
import { join } from 'node:path';
const testFixtures = fixtures.path('test-runner');
async function runTest(
{
isolation,
globalSetupFile,
testFiles = ['test-file.js'],
env = {},
additionalFlags = [],
runnerEnabled = true,
}
) {
const globalSetupPath = join(testFixtures, 'global-setup-teardown', globalSetupFile);
const args = [];
if (runnerEnabled) {
args.push('--test');
}
if (isolation) {
args.push(`--test-isolation=${isolation}`);
}
args.push(
'--test-reporter=spec',
`--test-global-setup=${globalSetupPath}`
);
if (additionalFlags.length > 0) {
args.push(...additionalFlags);
}
const testFilePaths = testFiles.map((file) => join(testFixtures, 'global-setup-teardown', file));
args.push(...testFilePaths);
const child = spawn(
process.execPath,
args,
{
encoding: 'utf8',
stdio: 'pipe',
env: {
...process.env,
...env
}
}
);
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
await once(child, 'exit');
return { stdout, stderr };
}
[
{
isolation: 'none',
runnerEnabled: true
},
{
isolation: 'process',
runnerEnabled: true
},
{
isolation: undefined,
runnerEnabled: false
},
].forEach((testCase) => {
const { isolation, runnerEnabled } = testCase;
describe(`test runner global hooks with isolation=${isolation} and --test: ${runnerEnabled}`, { concurrency: false }, () => {
beforeEach(() => {
tmpdir.refresh();
});
it('should run globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed.tmp');
const { stdout, stderr } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
assert.strictEqual(stderr.length, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should run setup-only module', async () => {
const setupOnlyFlagPath = tmpdir.resolve('setup-only-executed.tmp');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'setup-only.js',
env: {
SETUP_ONLY_FLAG_PATH: setupOnlyFlagPath,
SETUP_FLAG_PATH: setupOnlyFlagPath
},
runnerEnabled
});
assert.match(stdout, /Setup-only module executed/);
assert.ok(fs.existsSync(setupOnlyFlagPath), 'Setup-only flag file should exist');
const content = fs.readFileSync(setupOnlyFlagPath, 'utf8');
assert.strictEqual(content, 'Setup-only was executed');
});
it('should run teardown-only module', async () => {
const teardownOnlyFlagPath = tmpdir.resolve('teardown-only-executed.tmp');
const setupFlagPath = tmpdir.resolve('setup-for-teardown-only.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'Setup was executed');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'teardown-only.js',
env: {
TEARDOWN_ONLY_FLAG_PATH: teardownOnlyFlagPath,
SETUP_FLAG_PATH: setupFlagPath
},
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Teardown-only module executed/);
assert.ok(fs.existsSync(teardownOnlyFlagPath), 'Teardown-only flag file should exist');
const content = fs.readFileSync(teardownOnlyFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown-only was executed');
});
// Create a file in globalSetup and delete it in globalTeardown
// two test files should both verify that the file exists
// This works as the globalTeardown removes the setupFlag
it('should run globalTeardown only after all tests are done in case of more than one test file', async () => {
const teardownOnlyFlagPath = tmpdir.resolve('teardown-only-executed.tmp');
const setupFlagPath = tmpdir.resolve('setup-for-teardown-only.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'Setup was executed');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'teardown-only.js',
env: {
TEARDOWN_ONLY_FLAG_PATH: teardownOnlyFlagPath,
SETUP_FLAG_PATH: setupFlagPath
},
runnerEnabled,
testFiles: ['test-file.js', 'another-test-file.js']
});
if (runnerEnabled) {
assert.match(stdout, /pass 3/);
} else {
assert.match(stdout, /pass 2/);
}
assert.match(stdout, /fail 0/);
assert.match(stdout, /Teardown-only module executed/);
assert.ok(fs.existsSync(teardownOnlyFlagPath), 'Teardown-only flag file should exist');
const content = fs.readFileSync(teardownOnlyFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown-only was executed');
});
// TODO(pmarchini): We should be able to share context between setup and teardown
it.todo('should share context between setup and teardown');
it('should handle async setup and teardown', async () => {
const asyncFlagPath = tmpdir.resolve('async-executed.tmp');
const setupFlagPath = tmpdir.resolve('setup-for-async.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'Setup was executed');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'async-setup-teardown.js',
env: {
ASYNC_FLAG_PATH: asyncFlagPath,
SETUP_FLAG_PATH: setupFlagPath
},
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Async setup starting/);
assert.match(stdout, /Async setup completed/);
assert.match(stdout, /Async teardown starting/);
assert.match(stdout, /Async teardown completed/);
assert.ok(fs.existsSync(asyncFlagPath), 'Async flag file should exist');
const content = fs.readFileSync(asyncFlagPath, 'utf8');
assert.strictEqual(content, 'Setup part, Teardown part');
});
it('should handle error in setup', async () => {
const setupFlagPath = tmpdir.resolve('setup-for-error.tmp');
const { stdout, stderr } = await runTest({
isolation,
globalSetupFile: 'error-in-setup.js',
env: {
SETUP_FLAG_PATH: setupFlagPath
},
runnerEnabled
});
// Verify that the error is reported properly
const errorReported = stderr.includes('Deliberate error in global setup');
assert.ok(errorReported, 'Should report the error from global setup');
// Verify the teardown wasn't executed
assert.ok(!stdout.includes('Teardown should not run if setup fails'),
'Teardown should not run after setup fails');
});
it('should run TypeScript globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-ts.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-ts.tmp');
const { stdout, stderr } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.ts',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: ['--no-warnings'],
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
assert.strictEqual(stderr.length, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should run ESM globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-esm.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-esm.tmp');
const { stdout, stderr } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.mjs',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
assert.strictEqual(stderr.length, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should run globalSetup only once for run', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled
});
const GlobalSetupOccurrences = (stdout.match(/Global setup executed/g) || []).length;
// Global setup should run only once
assert.strictEqual(GlobalSetupOccurrences, 1);
});
it('should run globalSetup and globalTeardown only once for run', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled
});
const GlobalTeardownOccurrences = (stdout.match(/Global teardown executed/g) || []).length;
// Global teardown should run only once
assert.strictEqual(GlobalTeardownOccurrences, 1);
});
it('should run globalSetup and globalTeardown only once for run with multiple test files',
{
skip: !runnerEnabled ? 'Skipping test as --test is not enabled' : false
},
async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-once.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-once.tmp');
const testFiles = ['test-file.js', 'another-test-file.js'];
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
runnerEnabled,
testFiles
});
const GlobalSetupOccurrences = (stdout.match(/Global setup executed/g) || []).length;
const GlobalTeardownOccurrences = (stdout.match(/Global teardown executed/g) || []).length;
assert.strictEqual(GlobalSetupOccurrences, 1);
assert.strictEqual(GlobalTeardownOccurrences, 1);
assert.match(stdout, /pass 3/);
assert.match(stdout, /fail 0/);
}
);
describe('interop with --require and --import', () => {
const cjsPath = join(testFixtures, 'global-setup-teardown', 'required-module.cjs');
const esmpFile = fixtures.fileURL('test-runner', 'global-setup-teardown', 'imported-module.mjs');
it('should run required module before globalSetup', async () => {
const setupFlagPath = tmpdir.resolve('setup-for-required.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-for-required.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, '');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
requirePath: './required-module.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: [
`--require=${cjsPath}`,
],
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Required module executed/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
// Verify that the required module was executed before the global setup
const requiredExecutedPosition = stdout.indexOf('Required module executed');
const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
assert.ok(requiredExecutedPosition < globalSetupExecutedPosition,
'Required module should have been executed before global setup');
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
// This difference in behavior is due to the way --import is being handled by
// run_main entry point or test_runner entry point
if (runnerEnabled) {
it('should run imported module after globalSetup', async () => {
const setupFlagPath = tmpdir.resolve('setup-for-imported.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-for-imported.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'non-empty');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.mjs',
importPath: './imported-module.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: [
`--import=${esmpFile}`,
],
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Imported module executed/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
// Verify that the imported module was executed after the global setup
const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
const importedExecutedPosition = stdout.indexOf('Imported module executed');
assert.ok(globalSetupExecutedPosition < importedExecutedPosition,
'Imported module should be executed after global setup');
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
} else {
it('should run imported module before globalSetup', async () => {
const setupFlagPath = tmpdir.resolve('setup-for-imported.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-for-imported.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'non-empty');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.mjs',
importPath: './imported-module.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: [
`--import=${esmpFile}`,
],
runnerEnabled
});
assert.match(stdout, /pass 2/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /Imported module executed/);
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Global teardown executed/);
// Verify that the imported module was executed before the global setup
const importedExecutedPosition = stdout.indexOf('Imported module executed');
const globalSetupExecutedPosition = stdout.indexOf('Global setup executed');
assert.ok(importedExecutedPosition < globalSetupExecutedPosition,
'Imported module should be executed before global setup');
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
}
it('should execute globalSetup and globalTeardown correctly with imported module containing tests', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed.tmp');
const importedModuleWithTestFile = fixtures.fileURL(
'test-runner',
'global-setup-teardown',
'imported-module-with-test.mjs'
);
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'non-empty');
const { stdout } = await runTest({
isolation,
globalSetupFile: 'basic-setup-teardown.js',
env: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
additionalFlags: [
`--import=${importedModuleWithTestFile}`,
],
runnerEnabled
});
assert.match(stdout, /Global setup executed/);
assert.match(stdout, /Imported module Ok/);
assert.match(stdout, /Imported module Fail/);
assert.match(stdout, /verify setup was executed/);
assert.match(stdout, /another simple test/);
assert.match(stdout, /Global teardown executed/);
assert.match(stdout, /tests 4/);
assert.match(stdout, /suites 0/);
assert.match(stdout, /pass 3/);
assert.match(stdout, /fail 1/);
assert.match(stdout, /cancelled 0/);
assert.match(stdout, /skipped 0/);
assert.match(stdout, /todo 0/);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
});
});
});

View File

@ -0,0 +1,125 @@
import * as common from '../common/index.mjs';
import { beforeEach, describe, it } from 'node:test';
import { once } from 'node:events';
import assert from 'node:assert';
import { spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import tmpdir from '../common/tmpdir.js';
if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
if (common.isAIX)
common.skip('folder watch capability is limited in AIX.');
let fixturePaths;
// This test updates these files repeatedly,
// Reading them from disk is unreliable due to race conditions.
const fixtureContent = {
'test.js': `
const test = require('node:test');
test('test with global hooks', (t) => {
t.assert.ok('test passed');
});
`,
'global-setup-teardown.js': `
async function globalSetup() {
console.log('Global setup executed');
process.on('message', (message) => {
if (message === 'exit') {
process.kill(process.pid, 'SIGINT');
}
});
}
async function globalTeardown() {
console.log('Global teardown executed');
}
module.exports = { globalSetup, globalTeardown };
`
};
function refresh() {
tmpdir.refresh();
fixturePaths = Object.keys(fixtureContent)
.reduce((acc, file) => ({ ...acc, [file]: tmpdir.resolve(file) }), {});
Object.entries(fixtureContent)
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content));
}
describe('test runner watch mode with global setup hooks', () => {
beforeEach(refresh);
for (const isolation of ['none', 'process']) {
describe(`isolation: ${isolation}`, () => {
it(`should run global setup/teardown hooks with each test run in watch mode`,
// TODO(pmarchini): Skip test on Windows as the VS2022 build
// has issues handling SIGTERM and SIGINT signals correctly.
// See: https://github.com/nodejs/node/issues/46097
{ todo: common.isWindows },
async () => {
const globalSetupFileFixture = fixturePaths['global-setup-teardown.js'];
const ran1 = Promise.withResolvers();
const ran2 = Promise.withResolvers();
const child = spawn(process.execPath,
[
'--watch',
'--test',
'--test-reporter=spec',
`--test-isolation=${isolation}`,
'--test-global-setup=' + globalSetupFileFixture,
fixturePaths['test.js'],
],
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
cwd: tmpdir.path,
});
let stdout = '';
let currentRun = '';
const runs = [];
child.stdout.on('data', (data) => {
stdout += data.toString();
currentRun += data.toString();
const testRuns = stdout.match(/duration_ms\s\d+/g);
if (testRuns?.length >= 1) ran1.resolve();
if (testRuns?.length >= 2) ran2.resolve();
});
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const content = fixtureContent['test.js'];
const path = fixturePaths['test.js'];
writeFileSync(path, content);
await ran2.promise;
runs.push(currentRun);
currentRun = '';
child.send('exit');
await once(child, 'exit');
assert.match(runs[0], /Global setup executed/);
assert.match(runs[0], /tests 1/);
assert.match(runs[0], /pass 1/);
assert.match(runs[0], /fail 0/);
assert.doesNotMatch(runs[1], /Global setup executed/);
assert.doesNotMatch(runs[1], /Global teardown executed/);
assert.match(runs[1], /tests 1/);
assert.match(runs[1], /pass 1/);
assert.match(runs[1], /fail 0/);
// Verify stdout after killing the child
assert.doesNotMatch(currentRun, /Global setup executed/);
assert.match(currentRun, /Global teardown executed/);
});
});
}
});

View File

@ -0,0 +1,227 @@
import '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it, beforeEach, run } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import tmpdir from '../common/tmpdir.js';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
const testFixtures = fixtures.path('test-runner', 'global-setup-teardown');
const runnerFixture = fixtures.path('test-runner', 'test-runner-global-hooks.mjs');
describe('require(\'node:test\').run with global hooks', { concurrency: false }, () => {
beforeEach(() => {
tmpdir.refresh();
});
async function runTestWithGlobalHooks({
globalSetupFile,
testFile = 'test-file.js',
runnerEnv = {},
isolation = 'process'
}) {
const testFilePath = path.join(testFixtures, testFile);
const globalSetupPath = path.join(testFixtures, globalSetupFile);
const child = spawn(
process.execPath,
[
runnerFixture,
'--file', testFilePath,
'--globalSetup', globalSetupPath,
'--isolation', isolation,
],
{
encoding: 'utf8',
stdio: 'pipe',
env: {
...runnerEnv,
...process.env,
AVOID_PRINT_LOGS: 'true',
NODE_OPTIONS: '--no-warnings',
}
}
);
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
await once(child, 'exit');
// Assert in order to print a detailed error message if the test fails
assert.partialDeepStrictEqual(stderr, '');
assert.match(stdout, /pass (\d+)/);
assert.match(stdout, /fail (\d+)/);
const results = {
passed: parseInt((stdout.match(/pass (\d+)/) || [])[1] || '0', 10),
failed: parseInt((stdout.match(/fail (\d+)/) || [])[1] || '0', 10)
};
return { results };
}
for (const isolation of ['none', 'process']) {
describe(`with isolation : ${isolation}`, () => {
it('should run globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed.tmp');
const { results } = await runTestWithGlobalHooks({
globalSetupFile: 'basic-setup-teardown.js',
runnerEnv: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
isolation
});
assert.strictEqual(results.passed, 2);
assert.strictEqual(results.failed, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should run setup-only module', async () => {
const setupOnlyFlagPath = tmpdir.resolve('setup-only-executed.tmp');
const { results } = await runTestWithGlobalHooks({
globalSetupFile: 'setup-only.js',
runnerEnv: {
SETUP_ONLY_FLAG_PATH: setupOnlyFlagPath,
SETUP_FLAG_PATH: setupOnlyFlagPath
},
isolation
});
assert.strictEqual(results.passed, 1);
assert.strictEqual(results.failed, 1);
assert.ok(fs.existsSync(setupOnlyFlagPath), 'Setup-only flag file should exist');
const content = fs.readFileSync(setupOnlyFlagPath, 'utf8');
assert.strictEqual(content, 'Setup-only was executed');
});
it('should run teardown-only module', async () => {
const teardownOnlyFlagPath = tmpdir.resolve('teardown-only-executed.tmp');
const setupFlagPath = tmpdir.resolve('setup-for-teardown-only.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'Setup was executed');
const { results } = await runTestWithGlobalHooks({
globalSetupFile: 'teardown-only.js',
runnerEnv: {
TEARDOWN_ONLY_FLAG_PATH: teardownOnlyFlagPath,
SETUP_FLAG_PATH: setupFlagPath
},
isolation
});
assert.strictEqual(results.passed, 2);
assert.strictEqual(results.failed, 0);
assert.ok(fs.existsSync(teardownOnlyFlagPath), 'Teardown-only flag file should exist');
const content = fs.readFileSync(teardownOnlyFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown-only was executed');
});
// TODO(pmarchini): We should be able to share context between setup and teardown
it.todo('should share context between setup and teardown');
it('should handle async setup and teardown', async () => {
const asyncFlagPath = tmpdir.resolve('async-executed.tmp');
const setupFlagPath = tmpdir.resolve('setup-for-async.tmp');
// Create a setup file for test-file.js to find
fs.writeFileSync(setupFlagPath, 'Setup was executed');
const { results } = await runTestWithGlobalHooks({
globalSetupFile: 'async-setup-teardown.js',
runnerEnv: {
ASYNC_FLAG_PATH: asyncFlagPath,
SETUP_FLAG_PATH: setupFlagPath
},
isolation
});
assert.strictEqual(results.passed, 2);
assert.strictEqual(results.failed, 0);
assert.ok(fs.existsSync(asyncFlagPath), 'Async flag file should exist');
const content = fs.readFileSync(asyncFlagPath, 'utf8');
assert.strictEqual(content, 'Setup part, Teardown part');
});
it('should run TypeScript globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-ts.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-ts.tmp');
const { results } = await runTestWithGlobalHooks({
globalSetupFile: 'basic-setup-teardown.ts',
runnerEnv: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
isolation
});
assert.strictEqual(results.passed, 2);
assert.strictEqual(results.failed, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
it('should run ESM globalSetup and globalTeardown functions', async () => {
const setupFlagPath = tmpdir.resolve('setup-executed-esm.tmp');
const teardownFlagPath = tmpdir.resolve('teardown-executed-esm.tmp');
const { results } = await runTestWithGlobalHooks({
globalSetupFile: 'basic-setup-teardown.mjs',
runnerEnv: {
SETUP_FLAG_PATH: setupFlagPath,
TEARDOWN_FLAG_PATH: teardownFlagPath
},
isolation
});
assert.strictEqual(results.passed, 2);
assert.strictEqual(results.failed, 0);
// After all tests complete, the teardown should have run
assert.ok(fs.existsSync(teardownFlagPath), 'Teardown flag file should exist');
const content = fs.readFileSync(teardownFlagPath, 'utf8');
assert.strictEqual(content, 'Teardown was executed');
// Setup flag should have been removed by teardown
assert.ok(!fs.existsSync(setupFlagPath), 'Setup flag file should have been removed');
});
});
}
it('should validate that globalSetupPath is a string', () => {
[123, {}, [], true, false].forEach((invalidValue) => {
assert.throws(() => {
run({
files: [path.join(testFixtures, 'test-file.js')],
globalSetupPath: invalidValue
});
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "options\.globalSetupPath" property must be of type string/
});
});
});
});

View File

@ -206,7 +206,7 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
if (data.name === 'this is a test') {
t.assert.strictEqual(data.type, 'test');
}
}, 2));
}, 3));
stream.on('test:dequeue', common.mustCall((data) => {
if (data.name === 'this is a suite') {
t.assert.strictEqual(data.type, 'suite');
@ -214,7 +214,7 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
if (data.name === 'this is a test') {
t.assert.strictEqual(data.type, 'test');
}
}, 2));
}, 3));
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);