diff --git a/doc/api/test.md b/doc/api/test.md index 5f2d3dfad56cd0..85577ab2c96ae3 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -267,6 +267,23 @@ allows regular expression flags to be used. In the previous example, starting Node.js with `--test-name-pattern="/test [4-5]/i"` would match `Test 4` and `Test 5` because the pattern is case-insensitive. +To match a single test with a pattern, you can prefix it with all its ancestor +test names separated by space, to ensure it is unique. +For example, given the following test file: + +```js +describe('test 1', (t) => { + it('some test'); +}); + +describe('test 2', (t) => { + it('some test'); +}); +``` + +Starting Node.js with `--test-name-pattern="test 1 some test"` would match +only `some test` in `test 1`. + Test name patterns do not change the set of files that the test runner executes. ## Extraneous asynchronous activity diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index ee2c345e7a0403..d9154606b7dc05 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -362,8 +362,41 @@ class Test extends AsyncResource { } matchesTestNamePatterns() { - return ArrayPrototypeSome(testNamePatterns, (re) => RegExpPrototypeExec(re, this.name) !== null) || - this.parent?.matchesTestNamePatterns(); + const matchesByNameOrParent = ArrayPrototypeSome(testNamePatterns, (re) => + RegExpPrototypeExec(re, this.name) !== null || + this.parent?.matchesTestNamePatterns(), + ); + + if (matchesByNameOrParent) { + return true; + } + + const testNameWithAncestors = this.getTestNameWithAncestors(); + if (!testNameWithAncestors) { + return false; + } + + return ArrayPrototypeSome(testNamePatterns, (re) => RegExpPrototypeExec(re, testNameWithAncestors) !== null); + } + + /** + * Returns a name of the test prefixed by name of all its ancestors in ascending order, separated by a space + * Ex."grandparent parent test" + * + * It's needed to match a single test with non-unique name by pattern + */ + getTestNameWithAncestors() { + const ancestorNames = []; + let parent = this.parent; + + for (let i = 0; i < this.nesting; i++) { + ArrayPrototypePush(ancestorNames, parent.name); + parent = parent.parent; + } + + const formattedTestNameWithAncestors = `${ancestorNames.reverse().join(' ')} ${this.name}`; + + return ancestorNames.length ? formattedTestNameWithAncestors : undefined; } hasConcurrency() { diff --git a/test/fixtures/test-runner/output/name_pattern.js b/test/fixtures/test-runner/output/name_pattern.js index 10e7619b9cfcb9..883207405c43fd 100644 --- a/test/fixtures/test-runner/output/name_pattern.js +++ b/test/fixtures/test-runner/output/name_pattern.js @@ -1,4 +1,4 @@ -// Flags: --test-name-pattern=enabled --test-name-pattern=yes --test-name-pattern=/pattern/i +// Flags: --test-name-pattern=enabled --test-name-pattern=yes --test-name-pattern=/pattern/i --test-name-pattern=/^DescribeForFullPath\sNestedDescribeForFullPath\sNestedTest$/ 'use strict'; const common = require('../../../common'); const { @@ -65,3 +65,15 @@ describe('no', function() { it('yes', () => {}); }); }); + +describe('DescribeForFullPath', () => { + it('NestedTest', () => common.mustNotCall()); + + describe('NestedDescribeForFullPath', () => { + it('NestedTest', common.mustCall()); + }); +}) + +describe('DescribeForFullPath', () => { + it('NestedTest', () => common.mustNotCall()); +}) diff --git a/test/fixtures/test-runner/output/name_pattern.snapshot b/test/fixtures/test-runner/output/name_pattern.snapshot index bd53cc1fd2b89e..ac8a8b0d82c027 100644 --- a/test/fixtures/test-runner/output/name_pattern.snapshot +++ b/test/fixtures/test-runner/output/name_pattern.snapshot @@ -171,12 +171,48 @@ ok 15 - no duration_ms: * type: 'suite' ... -1..15 -# tests 21 -# suites 10 -# pass 13 +# Subtest: DescribeForFullPath + # Subtest: NestedTest + ok 1 - NestedTest # SKIP test name does not match pattern + --- + duration_ms: * + ... + # Subtest: NestedDescribeForFullPath + # Subtest: NestedTest + ok 1 - NestedTest + --- + duration_ms: * + ... + 1..1 + ok 2 - NestedDescribeForFullPath + --- + duration_ms: * + type: 'suite' + ... + 1..2 +ok 16 - DescribeForFullPath + --- + duration_ms: * + type: 'suite' + ... +# Subtest: DescribeForFullPath + # Subtest: NestedTest + ok 1 - NestedTest # SKIP test name does not match pattern + --- + duration_ms: * + ... + 1..1 +ok 17 - DescribeForFullPath + --- + duration_ms: * + type: 'suite' + ... +1..17 +# tests 24 +# suites 13 +# pass 14 # fail 0 # cancelled 0 -# skipped 8 +# skipped 10 # todo 0 # duration_ms *