diff --git a/doc/api/errors.md b/doc/api/errors.md
index 9139194719ade5..c94c1401df3620 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -697,6 +697,14 @@ message thrown by `block` because that usage suggests that the user believes
`message` is the expected message rather than the message the `AssertionError`
will display if `block` does not throw.
+
+
+### `ERR_ARG_NOT_ALLOWED`
+
+A function argument that can't be accepted depending of some logic restriction.
+e.g. The function `run(...)` from module `node:test` can't accept `testNamePatterns`
+param from programatic function and from CLI at same time.
+
### `ERR_ARG_NOT_ITERABLE`
diff --git a/doc/api/test.md b/doc/api/test.md
index 59b1647b12632a..750fad667a4a05 100644
--- a/doc/api/test.md
+++ b/doc/api/test.md
@@ -737,6 +737,10 @@ added:
number. If a nullish value is provided, each process gets its own port,
incremented from the primary's `process.debugPort`.
**Default:** `undefined`.
+ * `testNamePatterns` {array|regex} A regex used to filter the tests
+ by name before run it.
+ This can be a regex, or a regex string.
+ **Default:** `undefined`.
* Returns: {TestsStream}
```js
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 742d723155e7fe..ce036e3be7d066 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -960,6 +960,7 @@ module.exports = {
// Note: Node.js specific errors must begin with the prefix ERR_
E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError);
+E('ERR_ARG_NOT_ALLOWED', '%s is already being assigned', Error);
E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError);
E('ERR_ASSERTION', '%s', Error);
E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError);
diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js
index 5b772251be981e..59914b09796b1a 100644
--- a/lib/internal/test_runner/runner.js
+++ b/lib/internal/test_runner/runner.js
@@ -256,8 +256,9 @@ class FileTest extends Test {
const runningProcesses = new SafeMap();
const runningSubtests = new SafeMap();
-function runTestFile(path, root, inspectPort, filesWatcher) {
- const subtest = root.createSubtest(FileTest, path, async (t) => {
+function runTestFile(options) {
+ const { path, root, inspectPort, filesWatcher, testNamePatterns } = options;
+ const subtest = root.createSubtest(FileTest, path, { testNamePatterns }, async (t) => {
const args = getRunArgs({ path, inspectPort });
const stdio = ['pipe', 'pipe', 'pipe'];
const env = { ...process.env };
@@ -340,7 +341,7 @@ function runTestFile(path, root, inspectPort, filesWatcher) {
return promise;
}
-function watchFiles(testFiles, root, inspectPort) {
+function watchFiles(testFiles, root, inspectPort, testNamePatterns) {
const filesWatcher = new FilesWatcher({ throttle: 500, mode: 'filter' });
filesWatcher.on('changed', ({ owners }) => {
filesWatcher.unfilterFilesOwnedBy(owners);
@@ -354,7 +355,7 @@ function watchFiles(testFiles, root, inspectPort) {
await once(runningProcess, 'exit');
}
await runningSubtests.get(file);
- runningSubtests.set(file, runTestFile(file, root, inspectPort, filesWatcher));
+ runningSubtests.set(file, runTestFile({ file, root, inspectPort, filesWatcher, testNamePatterns }));
}, undefined, (error) => {
triggerUncaughtException(error, true /* fromPromise */);
}));
@@ -366,7 +367,7 @@ function run(options) {
if (options === null || typeof options !== 'object') {
options = kEmptyObject;
}
- const { concurrency, timeout, signal, files, inspectPort, watch, setup } = options;
+ const { concurrency, timeout, signal, files, inspectPort, watch, setup, testNamePatterns } = options;
if (files != null) {
validateArray(files, 'options.files');
@@ -384,13 +385,14 @@ function run(options) {
let postRun = () => root.postRun();
let filesWatcher;
if (watch) {
- filesWatcher = watchFiles(testFiles, root, inspectPort);
+ filesWatcher = watchFiles(testFiles, root, inspectPort, testNamePatterns);
postRun = undefined;
}
- const runFiles = () => {
+
+ const runFiles = async () => {
root.harness.bootstrapComplete = true;
return SafePromiseAllSettledReturnVoid(testFiles, (path) => {
- const subtest = runTestFile(path, root, inspectPort, filesWatcher);
+ const subtest = runTestFile({ path, root, inspectPort, filesWatcher, testNamePatterns });
runningSubtests.set(path, subtest);
return subtest;
});
diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js
index ffbf2d257aed62..29815b49e8003d 100644
--- a/lib/internal/test_runner/test.js
+++ b/lib/internal/test_runner/test.js
@@ -26,6 +26,7 @@ const { AbortController } = require('internal/abort_controller');
const {
codes: {
ERR_INVALID_ARG_TYPE,
+ ERR_ARG_NOT_ALLOWED,
ERR_TEST_FAILURE,
},
AbortError,
@@ -42,6 +43,7 @@ const {
kEmptyObject,
once: runOnce,
} = require('internal/util');
+const { ObjectHasOwn } = primordials;
const { isPromise } = require('internal/util/types');
const {
validateAbortSignal,
@@ -163,9 +165,16 @@ class Test extends AsyncResource {
constructor(options) {
super('Test');
- let { fn, name, parent, skip } = options;
+ let { fn, name, parent, skip, testNamePatterns: _testNamePatterns } = options;
const { concurrency, only, timeout, todo, signal } = options;
+ if (ObjectHasOwn(options, '_testNamePatterns') && testNamePatterns) {
+ throw new ERR_ARG_NOT_ALLOWED('testNamePatterns');
+ }
+ if (!_testNamePatterns) {
+ _testNamePatterns = testNamePatterns;
+ }
+
if (typeof fn !== 'function') {
fn = noop;
}
@@ -224,10 +233,10 @@ class Test extends AsyncResource {
this.timeout = timeout;
}
- if (testNamePatterns !== null) {
+ if (_testNamePatterns !== null) {
// eslint-disable-next-line no-use-before-define
const match = this instanceof TestHook || ArrayPrototypeSome(
- testNamePatterns,
+ _testNamePatterns,
(re) => RegExpPrototypeExec(re, name) !== null,
);
diff --git a/test/fixtures/test-runner/test/random.cjs b/test/fixtures/test-runner/test/random.cjs
index 2a722c504b9fa5..277af025abee84 100644
--- a/test/fixtures/test-runner/test/random.cjs
+++ b/test/fixtures/test-runner/test/random.cjs
@@ -2,3 +2,5 @@
const test = require('node:test');
test('this should pass');
+
+test('this should pass too');
diff --git a/test/parallel/test-runner-cli.js b/test/parallel/test-runner-cli.js
index 8cfceedfe6a53a..d92e18a07b4195 100644
--- a/test/parallel/test-runner-cli.js
+++ b/test/parallel/test-runner-cli.js
@@ -18,6 +18,18 @@ const testFixtures = fixtures.path('test-runner');
assert.match(child.stderr.toString(), /^Could not find/);
}
+
+{
+ // Return only test that maches with the filter
+ const args = ['--test', '--test-name-pattern="too"', join(testFixtures, 'test/random.cjs')];
+ const child = spawnSync('./node', args);
+
+ assert.strictEqual(child.stderr.toString(), '');
+ const stdout = child.stdout.toString();
+ assert.match(stdout, /ok 1 - this should pass # SKIP test name does not match pattern/);
+ assert.match(stdout, /ok 2 - this should pass too/);
+}
+
{
// Default behavior. node_modules is ignored. Files that don't match the
// pattern are ignored except in test/ directories.
diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs
index 6ac007bfb5dd6c..8c5d2f21e04f15 100644
--- a/test/parallel/test-runner-run.mjs
+++ b/test/parallel/test-runner-run.mjs
@@ -8,6 +8,17 @@ const testFixtures = fixtures.path('test-runner');
describe('require(\'node:test\').run', { concurrency: true }, () => {
+ it('should not throw error', async () => {
+ const argv = process.argv;
+ process.argv = [null, `${process.cwd()}/test/fixtures/test-runner/test/random.cjs`];
+ const stream = run();
+ stream.on('test:pass', common.mustCall(2));
+ stream.on('test:fail', common.mustNotCall());
+ // eslint-disable-next-line no-unused-vars
+ for await (const _ of stream);
+ process.argv = argv;
+ });
+
it('should run with no tests', async () => {
const stream = run({ files: [] });
stream.on('test:fail', common.mustNotCall());
@@ -27,6 +38,14 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
it('should succeed with a file', async () => {
const stream = run({ files: [join(testFixtures, 'test/random.cjs')] });
stream.on('test:fail', common.mustNotCall());
+ stream.on('test:pass', common.mustCall(2));
+ // eslint-disable-next-line no-unused-vars
+ for await (const _ of stream);
+ });
+
+ it('should succeed with a file and filter', async () => {
+ const stream = run({ files: [join(testFixtures, 'test/random.cjs')], testNamePatterns: [/too/] });
+ stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustCall(1));
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
@@ -35,7 +54,7 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
it('should run same file twice', async () => {
const stream = run({ files: [join(testFixtures, 'test/random.cjs'), join(testFixtures, 'test/random.cjs')] });
stream.on('test:fail', common.mustNotCall());
- stream.on('test:pass', common.mustCall(2));
+ stream.on('test:pass', common.mustCall(4));
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
});