diff --git a/CHANGELOG.md b/CHANGELOG.md index df61a441855d..60e2445465f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ - `[jest-runtime]` Properly handle re-exported native modules in ESM via CJS ([#14589](https://github.com/jestjs/jest/pull/14589)) - `[jest-util]` Make sure `isInteractive` works in a browser ([#14552](https://github.com/jestjs/jest/pull/14552)) - `[pretty-format]` [**BREAKING**] Print `ArrayBuffer` and `DataView` correctly ([#14290](https://github.com/facebook/jest/pull/14290)) +- `[jest-cli]` When specifying paths on the command line, only match against the relative paths of the test files ([#12519](https://github.com/facebook/jest/pull/12519)) + - [**BREAKING**] Changes `testPathPattern` configuration option to `testPathPatterns`, which now takes a list of patterns instead of the regex. + - [**BREAKING**] `--testPathPattern` is now `--testPathPatterns` ### Performance diff --git a/docs/CLI.md b/docs/CLI.md index 033fa6a83069..c8cb41e6bf11 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -481,11 +481,11 @@ The regex is matched against the full name, which is a combination of the test n ### `--testPathIgnorePatterns=|[array]` -A single or array of regexp pattern strings that are tested against all tests paths before executing the test. Contrary to `--testPathPattern`, it will only run those tests with a path that does not match with the provided regexp expressions. +A single or array of regexp pattern strings that are tested against all tests paths before executing the test. Contrary to `--testPathPatterns`, it will only run those tests with a path that does not match with the provided regexp expressions. To pass as an array use escaped parentheses and space delimited regexps such as `\(/node_modules/ /tests/e2e/\)`. Alternatively, you can omit parentheses by combining regexps into a single regexp like `/node_modules/|/tests/e2e/`. These two examples are equivalent. -### `--testPathPattern=` +### `--testPathPatterns=` A regexp pattern string that is matched against all tests paths before executing the test. On Windows, you will need to use `/` as a path separator or escape `\` as `\\`. diff --git a/docs/Configuration.md b/docs/Configuration.md index 54c38107fe3a..d557dcee7904 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -787,7 +787,7 @@ While code transformation is applied to the linked setup-file, Jest will **not** ```js title="setup.js" module.exports = async function (globalConfig, projectConfig) { - console.log(globalConfig.testPathPattern); + console.log(globalConfig.testPathPatterns); console.log(projectConfig.cache); // Set reference to mongod in order to close the server during teardown. @@ -797,7 +797,7 @@ module.exports = async function (globalConfig, projectConfig) { ```js title="teardown.js" module.exports = async function (globalConfig, projectConfig) { - console.log(globalConfig.testPathPattern); + console.log(globalConfig.testPathPatterns); console.log(projectConfig.cache); await globalThis.__MONGOD__.stop(); diff --git a/docs/WatchPlugins.md b/docs/WatchPlugins.md index 57ddeaec07e4..451834499593 100644 --- a/docs/WatchPlugins.md +++ b/docs/WatchPlugins.md @@ -172,7 +172,7 @@ For stability and safety reasons, only part of the global configuration keys can - [`onlyFailures`](configuration#onlyfailures-boolean) - [`reporters`](configuration#reporters-arraymodulename--modulename-options) - [`testNamePattern`](cli#--testnamepatternregex) -- [`testPathPattern`](cli#--testpathpatternregex) +- [`testPathPatterns`](cli#--testpathpatternsregex) - [`updateSnapshot`](cli#--updatesnapshot) - [`verbose`](configuration#verbose-boolean) diff --git a/e2e/__tests__/__snapshots__/cliHandlesExactFilenames.test.ts.snap b/e2e/__tests__/__snapshots__/cliHandlesExactFilenames.test.ts.snap index 4eb172dc686b..e8c3c1073714 100644 --- a/e2e/__tests__/__snapshots__/cliHandlesExactFilenames.test.ts.snap +++ b/e2e/__tests__/__snapshots__/cliHandlesExactFilenames.test.ts.snap @@ -13,5 +13,5 @@ exports[`CLI accepts exact file names if matchers matched 2`] = ` Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /.\\/foo\\/bar.spec.js/i." +Ran all test suites matching ./foo/bar.spec.js." `; diff --git a/e2e/__tests__/__snapshots__/customReporters.test.ts.snap b/e2e/__tests__/__snapshots__/customReporters.test.ts.snap index 7506630b07d2..d3fa7a739372 100644 --- a/e2e/__tests__/__snapshots__/customReporters.test.ts.snap +++ b/e2e/__tests__/__snapshots__/customReporters.test.ts.snap @@ -82,7 +82,7 @@ exports[`Custom Reporters Integration default reporters enabled 2`] = ` Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /add.test.js/i." +Ran all test suites matching add.test.js." `; exports[`Custom Reporters Integration default reporters enabled 3`] = ` diff --git a/e2e/__tests__/__snapshots__/emptyDescribeWithHooks.test.ts.snap b/e2e/__tests__/__snapshots__/emptyDescribeWithHooks.test.ts.snap index ad2f6318c6ed..71a0140e2a86 100644 --- a/e2e/__tests__/__snapshots__/emptyDescribeWithHooks.test.ts.snap +++ b/e2e/__tests__/__snapshots__/emptyDescribeWithHooks.test.ts.snap @@ -7,7 +7,7 @@ Object { Tests: 1 skipped, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /hookInDescribeWithSkippedTest.test.js/i.", +Ran all test suites matching hookInDescribeWithSkippedTest.test.js.", } `; @@ -34,7 +34,7 @@ Object { Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /hookInEmptyDescribe.test.js/i.", +Ran all test suites matching hookInEmptyDescribe.test.js.", } `; @@ -61,7 +61,7 @@ Object { Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /hookInEmptyNestedDescribe.test.js/i.", +Ran all test suites matching hookInEmptyNestedDescribe.test.js.", } `; @@ -133,6 +133,6 @@ Object { Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /multipleHooksInEmptyDescribe.test.js/i.", +Ran all test suites matching multipleHooksInEmptyDescribe.test.js.", } `; diff --git a/e2e/__tests__/__snapshots__/findRelatedFiles.test.ts.snap b/e2e/__tests__/__snapshots__/findRelatedFiles.test.ts.snap index d21e158b8bf1..7c1eb192a715 100644 --- a/e2e/__tests__/__snapshots__/findRelatedFiles.test.ts.snap +++ b/e2e/__tests__/__snapshots__/findRelatedFiles.test.ts.snap @@ -5,7 +5,7 @@ exports[`--findRelatedTests flag coverage configuration is applied correctly 1`] Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites related to files matching /a.js|b.js/i." +Ran all test suites related to files matching a.js|b.js." `; exports[`--findRelatedTests flag coverage configuration is applied correctly 2`] = ` @@ -50,7 +50,7 @@ exports[`--findRelatedTests flag generates coverage report for filename 4`] = ` Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites related to files matching /a.js/i." +Ran all test suites related to files matching a.js." `; exports[`--findRelatedTests flag generates coverage report for filename 5`] = ` diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 1f578504e144..96bf4cb3c3f9 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -5,7 +5,7 @@ exports[`does not enforce import assertions 1`] = ` Tests: 2 passed, 2 total Snapshots: 0 total Time: <> -Ran all test suites matching /native-esm-missing-import-assertions.test.js/i." +Ran all test suites matching native-esm-missing-import-assertions.test.js." `; exports[`on node >=16.11.0 support re-exports from CJS of dual packages 1`] = ` @@ -13,7 +13,7 @@ exports[`on node >=16.11.0 support re-exports from CJS of dual packages 1`] = ` Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /native-esm-deep-cjs-reexport.test.js/i." +Ran all test suites matching native-esm-deep-cjs-reexport.test.js." `; exports[`on node >=16.12.0 supports import assertions 1`] = ` @@ -21,7 +21,7 @@ exports[`on node >=16.12.0 supports import assertions 1`] = ` Tests: 2 passed, 2 total Snapshots: 0 total Time: <> -Ran all test suites matching /native-esm-import-assertions.test.js/i." +Ran all test suites matching native-esm-import-assertions.test.js." `; exports[`properly handle re-exported native modules in ESM via CJS 1`] = ` @@ -29,7 +29,7 @@ exports[`properly handle re-exported native modules in ESM via CJS 1`] = ` Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /native-esm-native-module.test.js/i." +Ran all test suites matching native-esm-native-module.test.js." `; exports[`runs WebAssembly (Wasm) test with native ESM 1`] = ` @@ -37,7 +37,7 @@ exports[`runs WebAssembly (Wasm) test with native ESM 1`] = ` Tests: 6 passed, 6 total Snapshots: 0 total Time: <> -Ran all test suites matching /native-esm-wasm.test.js/i." +Ran all test suites matching native-esm-wasm.test.js." `; exports[`runs test with native ESM 1`] = ` @@ -45,7 +45,7 @@ exports[`runs test with native ESM 1`] = ` Tests: 33 passed, 33 total Snapshots: 0 total Time: <> -Ran all test suites matching /native-esm.test.js/i." +Ran all test suites matching native-esm.test.js." `; exports[`support re-exports from CJS of core module 1`] = ` @@ -53,7 +53,7 @@ exports[`support re-exports from CJS of core module 1`] = ` Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /native-esm-core-cjs-reexport.test.js/i." +Ran all test suites matching native-esm-core-cjs-reexport.test.js." `; exports[`supports top-level await 1`] = ` @@ -61,5 +61,5 @@ exports[`supports top-level await 1`] = ` Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /native-esm.tla.test.js/i." +Ran all test suites matching native-esm.tla.test.js." `; diff --git a/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap b/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap index 402a65e570c7..1ff8ca9e0dba 100644 --- a/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap +++ b/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap @@ -22,7 +22,7 @@ Object { Tests: 1 passed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /unhandledRejectionAfterAll.test.js/i.", +Ran all test suites matching unhandledRejectionAfterAll.test.js.", } `; @@ -63,7 +63,7 @@ Object { Tests: 2 failed, 2 total Snapshots: 0 total Time: <> -Ran all test suites matching /unhandledRejectionAfterEach.test.js/i.", +Ran all test suites matching unhandledRejectionAfterEach.test.js.", } `; @@ -89,7 +89,7 @@ Object { Tests: 1 failed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /unhandledRejectionBeforeAll.test.js/i.", +Ran all test suites matching unhandledRejectionBeforeAll.test.js.", } `; @@ -130,7 +130,7 @@ Object { Tests: 2 failed, 2 total Snapshots: 0 total Time: <> -Ran all test suites matching /unhandledRejectionBeforeEach.test.js/i.", +Ran all test suites matching unhandledRejectionBeforeEach.test.js.", } `; @@ -217,7 +217,7 @@ Object { Tests: 4 failed, 4 total Snapshots: 0 total Time: <> -Ran all test suites matching /unhandledRejectionTest.test.js/i.", +Ran all test suites matching unhandledRejectionTest.test.js.", } `; @@ -231,6 +231,6 @@ Object { Tests: 3 passed, 3 total Snapshots: 0 total Time: <> -Ran all test suites matching /rejectionHandled.test.js/i.", +Ran all test suites matching rejectionHandled.test.js.", } `; diff --git a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap index b3a4eb2b913c..d40875b9cab1 100644 --- a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap +++ b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap @@ -139,7 +139,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` "printBasicPrototype": false }, "testFailureExitCode": 1, - "testPathPattern": "", + "testPathPatterns": [], "testSequencer": "<>/jest-test-sequencer/build/index.js", "updateSnapshot": "none", "useStderr": false, diff --git a/e2e/__tests__/__snapshots__/snapshot.test.ts.snap b/e2e/__tests__/__snapshots__/snapshot.test.ts.snap index 811a7727b08a..e6d85efff184 100644 --- a/e2e/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/e2e/__tests__/__snapshots__/snapshot.test.ts.snap @@ -85,7 +85,7 @@ exports[`Snapshot works with escaped characters 1`] = ` Tests: 1 passed, 1 total Snapshots: 1 written, 1 total Time: <> -Ran all test suites matching /snapshot.test.js/i." +Ran all test suites matching snapshot.test.js." `; exports[`Snapshot works with escaped characters 2`] = ` @@ -93,7 +93,7 @@ exports[`Snapshot works with escaped characters 2`] = ` Tests: 2 passed, 2 total Snapshots: 1 written, 1 passed, 2 total Time: <> -Ran all test suites matching /snapshot.test.js/i." +Ran all test suites matching snapshot.test.js." `; exports[`Snapshot works with escaped characters 3`] = ` @@ -101,7 +101,7 @@ exports[`Snapshot works with escaped characters 3`] = ` Tests: 2 passed, 2 total Snapshots: 2 passed, 2 total Time: <> -Ran all test suites matching /snapshot.test.js/i." +Ran all test suites matching snapshot.test.js." `; exports[`Snapshot works with escaped regex 1`] = ` @@ -109,7 +109,7 @@ exports[`Snapshot works with escaped regex 1`] = ` Tests: 2 passed, 2 total Snapshots: 2 written, 2 total Time: <> -Ran all test suites matching /snapshotEscapeRegex.js/i." +Ran all test suites matching snapshotEscapeRegex.js." `; exports[`Snapshot works with escaped regex 2`] = ` @@ -117,7 +117,7 @@ exports[`Snapshot works with escaped regex 2`] = ` Tests: 2 passed, 2 total Snapshots: 2 passed, 2 total Time: <> -Ran all test suites matching /snapshotEscapeRegex.js/i." +Ran all test suites matching snapshotEscapeRegex.js." `; exports[`Snapshot works with template literal substitutions 1`] = ` @@ -125,7 +125,7 @@ exports[`Snapshot works with template literal substitutions 1`] = ` Tests: 1 passed, 1 total Snapshots: 1 written, 1 total Time: <> -Ran all test suites matching /snapshotEscapeSubstitution.test.js/i." +Ran all test suites matching snapshotEscapeSubstitution.test.js." `; exports[`Snapshot works with template literal substitutions 2`] = ` @@ -133,5 +133,5 @@ exports[`Snapshot works with template literal substitutions 2`] = ` Tests: 1 passed, 1 total Snapshots: 1 passed, 1 total Time: <> -Ran all test suites matching /snapshotEscapeSubstitution.test.js/i." +Ran all test suites matching snapshotEscapeSubstitution.test.js." `; diff --git a/e2e/__tests__/__snapshots__/stackTrace.test.ts.snap b/e2e/__tests__/__snapshots__/stackTrace.test.ts.snap index e06101e24809..ecc0b1cbf61c 100644 --- a/e2e/__tests__/__snapshots__/stackTrace.test.ts.snap +++ b/e2e/__tests__/__snapshots__/stackTrace.test.ts.snap @@ -5,7 +5,7 @@ exports[`Stack Trace does not print a stack trace for errors when --noStackTrace Tests: 3 failed, 3 total Snapshots: 0 total Time: <> -Ran all test suites matching /testError.test.js/i." +Ran all test suites matching testError.test.js." `; exports[`Stack Trace does not print a stack trace for matching errors when --noStackTrace is given 1`] = ` @@ -13,7 +13,7 @@ exports[`Stack Trace does not print a stack trace for matching errors when --noS Tests: 1 failed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /stackTrace.test.js/i." +Ran all test suites matching stackTrace.test.js." `; exports[`Stack Trace does not print a stack trace for runtime errors when --noStackTrace is given 1`] = ` @@ -21,7 +21,7 @@ exports[`Stack Trace does not print a stack trace for runtime errors when --noSt Tests: 0 total Snapshots: 0 total Time: <> -Ran all test suites matching /runtimeError.test.js/i." +Ran all test suites matching runtimeError.test.js." `; exports[`Stack Trace prints a stack trace for errors 1`] = ` @@ -29,7 +29,7 @@ exports[`Stack Trace prints a stack trace for errors 1`] = ` Tests: 3 failed, 3 total Snapshots: 0 total Time: <> -Ran all test suites matching /testError.test.js/i." +Ran all test suites matching testError.test.js." `; exports[`Stack Trace prints a stack trace for errors without message in stack trace 1`] = ` @@ -37,7 +37,7 @@ exports[`Stack Trace prints a stack trace for errors without message in stack tr Tests: 1 failed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /stackTraceWithoutMessage.test.js/i." +Ran all test suites matching stackTraceWithoutMessage.test.js." `; exports[`Stack Trace prints a stack trace for matching errors 1`] = ` @@ -45,7 +45,7 @@ exports[`Stack Trace prints a stack trace for matching errors 1`] = ` Tests: 1 failed, 1 total Snapshots: 0 total Time: <> -Ran all test suites matching /stackTrace.test.js/i." +Ran all test suites matching stackTrace.test.js." `; exports[`Stack Trace prints a stack trace for runtime errors 1`] = ` @@ -53,5 +53,5 @@ exports[`Stack Trace prints a stack trace for runtime errors 1`] = ` Tests: 0 total Snapshots: 0 total Time: <> -Ran all test suites matching /runtimeError.test.js/i." +Ran all test suites matching runtimeError.test.js." `; diff --git a/e2e/__tests__/__snapshots__/testTodo.test.ts.snap b/e2e/__tests__/__snapshots__/testTodo.test.ts.snap index a9e1ff123e2b..3f5f1d950276 100644 --- a/e2e/__tests__/__snapshots__/testTodo.test.ts.snap +++ b/e2e/__tests__/__snapshots__/testTodo.test.ts.snap @@ -11,7 +11,7 @@ Test Suites: 1 passed, 1 total Tests: 2 todo, 1 passed, 3 total Snapshots: 0 total Time: <> -Ran all test suites matching /only-todo.test.js/i." +Ran all test suites matching only-todo.test.js." `; exports[`shows error messages when called with invalid argument 1`] = ` diff --git a/e2e/__tests__/__snapshots__/watchModePatterns.test.ts.snap b/e2e/__tests__/__snapshots__/watchModePatterns.test.ts.snap index 1a0e4d7b91ae..185caf8c8f20 100644 --- a/e2e/__tests__/__snapshots__/watchModePatterns.test.ts.snap +++ b/e2e/__tests__/__snapshots__/watchModePatterns.test.ts.snap @@ -66,7 +66,7 @@ exports[`can press "p" to filter by file name: test summary 2`] = ` Tests: 2 passed, 2 total Snapshots: 0 total Time: <> -Ran all test suites matching /bar/i." +Ran all test suites matching bar." `; exports[`can press "t" to filter by test name 1`] = ` diff --git a/e2e/__tests__/findRelatedFiles.test.ts b/e2e/__tests__/findRelatedFiles.test.ts index f889ac8a4573..525fadf59394 100644 --- a/e2e/__tests__/findRelatedFiles.test.ts +++ b/e2e/__tests__/findRelatedFiles.test.ts @@ -33,7 +33,7 @@ describe('--findRelatedTests flag', () => { const {stderr} = runJest(DIR, ['--findRelatedTests', 'a.js']); expect(stderr).toMatch('PASS __tests__/test.test.js'); - const summaryMsg = 'Ran all test suites related to files matching /a.js/i.'; + const summaryMsg = 'Ran all test suites related to files matching a.js.'; expect(stderr).toMatch(summaryMsg); }); @@ -59,7 +59,7 @@ describe('--findRelatedTests flag', () => { const {stderr} = runJest(DIR, ['--findRelatedTests', 'A.JS']); expect(stderr).toMatch('PASS __tests__/test.test.js'); - const summaryMsg = 'Ran all test suites related to files matching /A.JS/i.'; + const summaryMsg = 'Ran all test suites related to files matching A.JS.'; expect(stderr).toMatch(summaryMsg); }); @@ -112,7 +112,7 @@ describe('--findRelatedTests flag', () => { expect(stderr).toMatch('PASS __tests__/test.test.js'); expect(stderr).not.toMatch('PASS __tests__/test-skip-deps.test.js'); - const summaryMsg = 'Ran all test suites related to files matching /a.js/i.'; + const summaryMsg = 'Ran all test suites related to files matching a.js.'; expect(stderr).toMatch(summaryMsg); }); @@ -162,7 +162,7 @@ describe('--findRelatedTests flag', () => { expect(stderr).toMatch('PASS __tests__/test.test.js'); expect(stderr).not.toMatch('PASS __tests__/test-skip-deps.test.js'); - const summaryMsg = 'Ran all test suites related to files matching /a.js/i.'; + const summaryMsg = 'Ran all test suites related to files matching a.js.'; expect(stderr).toMatch(summaryMsg); }); diff --git a/e2e/__tests__/globalSetup.test.ts b/e2e/__tests__/globalSetup.test.ts index f7c4a29eb641..454b5f76e8bf 100644 --- a/e2e/__tests__/globalSetup.test.ts +++ b/e2e/__tests__/globalSetup.test.ts @@ -56,7 +56,7 @@ test('globalSetup is triggered once before all test suites', () => { const setupPath = path.join(e2eDir, 'setup.js'); const result = runWithJson(e2eDir, [ `--globalSetup=${setupPath}`, - '--testPathPattern=__tests__', + '--testPathPatterns=__tests__', ]); expect(result.exitCode).toBe(0); @@ -70,7 +70,7 @@ test('jest throws an error when globalSetup does not export a function', () => { const setupPath = path.resolve(__dirname, '../global-setup/invalidSetup.js'); const {exitCode, stderr} = runJest(e2eDir, [ `--globalSetup=${setupPath}`, - '--testPathPattern=__tests__', + '--testPathPatterns=__tests__', ]); expect(exitCode).toBe(1); @@ -83,15 +83,13 @@ test('jest throws an error when globalSetup does not export a function', () => { test('globalSetup function gets global config object and project config as parameters', () => { const setupPath = path.resolve(e2eDir, 'setupWithConfig.js'); - const testPathPattern = 'pass'; - const result = runJest(e2eDir, [ `--globalSetup=${setupPath}`, - `--testPathPattern=${testPathPattern}`, + '--testPathPatterns=pass', '--cache=true', ]); - expect(result.stdout).toBe(`${testPathPattern}\ntrue`); + expect(result.stdout).toBe("[ 'pass' ]\ntrue"); }); test('should call globalSetup function of multiple projects', () => { @@ -111,7 +109,7 @@ test('should not call a globalSetup of a project if there are no tests to run fr const result = runWithJson(e2eDir, [ `--config=${configPath}`, - '--testPathPattern=project-1', + '--testPathPatterns=project-1', ]); expect(result.exitCode).toBe(0); @@ -140,15 +138,13 @@ test('should not call any globalSetup if there are no tests to run', () => { test('globalSetup works with default export', () => { const setupPath = path.resolve(e2eDir, 'setupWithDefaultExport.js'); - const testPathPattern = 'pass'; - const result = runJest(e2eDir, [ `--globalSetup=${setupPath}`, - `--testPathPattern=${testPathPattern}`, + '--testPathPatterns=pass', '--cache=true', ]); - expect(result.stdout).toBe(`${testPathPattern}\ntrue`); + expect(result.stdout).toBe("[ 'pass' ]\ntrue"); }); test('globalSetup throws with named export', () => { @@ -156,7 +152,7 @@ test('globalSetup throws with named export', () => { const {exitCode, stderr} = runJest(e2eDir, [ `--globalSetup=${setupPath}`, - '--testPathPattern=__tests__', + '--testPathPatterns=__tests__', ]); expect(exitCode).toBe(1); diff --git a/e2e/__tests__/globalTeardown.test.ts b/e2e/__tests__/globalTeardown.test.ts index 360200f0a583..cefd7e88accb 100644 --- a/e2e/__tests__/globalTeardown.test.ts +++ b/e2e/__tests__/globalTeardown.test.ts @@ -40,7 +40,7 @@ test('globalTeardown is triggered once after all test suites', () => { const teardownPath = path.resolve(e2eDir, 'teardown.js'); const result = runWithJson('global-teardown', [ `--globalTeardown=${teardownPath}`, - '--testPathPattern=__tests__', + '--testPathPatterns=__tests__', ]); expect(result.exitCode).toBe(0); @@ -54,7 +54,7 @@ test('jest throws an error when globalTeardown does not export a function', () = const teardownPath = path.resolve(e2eDir, 'invalidTeardown.js'); const {exitCode, stderr} = runJest(e2eDir, [ `--globalTeardown=${teardownPath}`, - '--testPathPattern=__tests__', + '--testPathPatterns=__tests__', ]); expect(exitCode).toBe(1); @@ -67,15 +67,13 @@ test('jest throws an error when globalTeardown does not export a function', () = test('globalSetup function gets global config object and project config as parameters', () => { const teardownPath = path.resolve(e2eDir, 'teardownWithConfig.js'); - const testPathPattern = 'pass'; - const result = runJest(e2eDir, [ `--globalTeardown=${teardownPath}`, - `--testPathPattern=${testPathPattern}`, + '--testPathPatterns=pass', '--cache=true', ]); - expect(result.stdout).toBe(`${testPathPattern}\ntrue`); + expect(result.stdout).toBe("[ 'pass' ]\ntrue"); }); test('should call globalTeardown function of multiple projects', () => { @@ -95,7 +93,7 @@ test('should not call a globalTeardown of a project if there are no tests to run const result = runWithJson('global-teardown', [ `--config=${configPath}`, - '--testPathPattern=project-1', + '--testPathPatterns=project-1', ]); expect(result.exitCode).toBe(0); @@ -108,15 +106,13 @@ test('should not call a globalTeardown of a project if there are no tests to run test('globalTeardown works with default export', () => { const teardownPath = path.resolve(e2eDir, 'teardownWithDefaultExport.js'); - const testPathPattern = 'pass'; - const result = runJest(e2eDir, [ `--globalTeardown=${teardownPath}`, - `--testPathPattern=${testPathPattern}`, + '--testPathPatterns=pass', '--cache=true', ]); - expect(result.stdout).toBe(`${testPathPattern}\ntrue`); + expect(result.stdout).toBe("[ 'pass' ]\ntrue"); }); test('globalTeardown throws with named export', () => { @@ -127,7 +123,7 @@ test('globalTeardown throws with named export', () => { const {exitCode, stderr} = runJest(e2eDir, [ `--globalTeardown=${teardownPath}`, - '--testPathPattern=__tests__', + '--testPathPatterns=__tests__', ]); expect(exitCode).toBe(1); diff --git a/e2e/__tests__/noTestsFound.test.ts b/e2e/__tests__/noTestsFound.test.ts index 0c90d3f240dc..f65d01b4f4a4 100644 --- a/e2e/__tests__/noTestsFound.test.ts +++ b/e2e/__tests__/noTestsFound.test.ts @@ -13,7 +13,7 @@ const DIR = path.resolve(__dirname, '../no-tests-found-test'); describe('No tests are found', () => { test('fails the test suite in standard situation', () => { const {exitCode, stdout} = runJest(DIR, [ - '--testPathPattern', + '--testPathPatterns', '/non/existing/path/', ]); @@ -26,7 +26,7 @@ describe('No tests are found', () => { test("doesn't fail the test suite if --passWithNoTests passed", () => { const {exitCode, stdout} = runJest(DIR, [ - '--testPathPattern', + '--testPathPatterns', '/non/existing/path/', '--passWithNoTests', ]); diff --git a/e2e/__tests__/onlyChanged.test.ts b/e2e/__tests__/onlyChanged.test.ts index be77ee63a5c6..b40619f26dc6 100644 --- a/e2e/__tests__/onlyChanged.test.ts +++ b/e2e/__tests__/onlyChanged.test.ts @@ -246,7 +246,7 @@ test('collect test coverage when using onlyChanged', () => { expect(exitCode).toBe(0); }); -test('onlyChanged in config is overwritten by --all or testPathPattern', () => { +test('onlyChanged in config is overwritten by --all or testPathPatterns', () => { writeFiles(DIR, { '.watchmanconfig': '', '__tests__/file1.test.js': "require('../file1'); test('file1', () => {});", diff --git a/e2e/__tests__/testPathPatternReporterMessage.test.ts b/e2e/__tests__/testPathPatternReporterMessage.test.ts index 6349dc087e82..2f9a2d3c5714 100644 --- a/e2e/__tests__/testPathPatternReporterMessage.test.ts +++ b/e2e/__tests__/testPathPatternReporterMessage.test.ts @@ -25,14 +25,14 @@ test('prints a message with path pattern at the end', () => { let stderr; ({stderr} = runJest(DIR, ['a'])); - expect(stderr).toMatch('Ran all test suites matching /a/i'); + expect(stderr).toMatch('Ran all test suites matching a'); ({stderr} = runJest(DIR, ['a', 'b'])); - expect(stderr).toMatch('Ran all test suites matching /a|b/i'); + expect(stderr).toMatch('Ran all test suites matching a|b'); - ({stderr} = runJest(DIR, ['--testPathPattern', 'a'])); - expect(stderr).toMatch('Ran all test suites matching /a/i'); + ({stderr} = runJest(DIR, ['--testPathPatterns', 'a'])); + expect(stderr).toMatch('Ran all test suites matching a'); - ({stderr} = runJest(DIR, ['--testPathPattern', 'a|b'])); - expect(stderr).toMatch('Ran all test suites matching /a|b/i'); + ({stderr} = runJest(DIR, ['--testPathPatterns', 'a|b'])); + expect(stderr).toMatch('Ran all test suites matching a|b'); }); diff --git a/e2e/global-setup/invalidSetupWithNamedExport.js b/e2e/global-setup/invalidSetupWithNamedExport.js index 1ba0caead847..5501dc11d1c2 100644 --- a/e2e/global-setup/invalidSetupWithNamedExport.js +++ b/e2e/global-setup/invalidSetupWithNamedExport.js @@ -6,7 +6,7 @@ */ function invalidSetupWithNamedExport(jestConfig): void { - console.log(jestConfig.testPathPattern); + console.log(jestConfig.testPathPatterns); } export {invalidSetupWithNamedExport}; diff --git a/e2e/global-setup/setupWithConfig.js b/e2e/global-setup/setupWithConfig.js index 465ea45407c5..ff9b308107b1 100644 --- a/e2e/global-setup/setupWithConfig.js +++ b/e2e/global-setup/setupWithConfig.js @@ -6,6 +6,6 @@ */ module.exports = function (globalConfig, projectConfig) { - console.log(globalConfig.testPathPattern); + console.log(globalConfig.testPathPatterns); console.log(projectConfig.cache); }; diff --git a/e2e/global-setup/setupWithDefaultExport.js b/e2e/global-setup/setupWithDefaultExport.js index 4f5b9b90f4e6..85b2c298a4d7 100644 --- a/e2e/global-setup/setupWithDefaultExport.js +++ b/e2e/global-setup/setupWithDefaultExport.js @@ -6,6 +6,6 @@ */ export default function (globalConfig, projectConfig): void { - console.log(globalConfig.testPathPattern); + console.log(globalConfig.testPathPatterns); console.log(projectConfig.cache); } diff --git a/e2e/global-teardown/invalidTeardownWithNamedExport.js b/e2e/global-teardown/invalidTeardownWithNamedExport.js index 718656b4bddf..cc1abef111cd 100644 --- a/e2e/global-teardown/invalidTeardownWithNamedExport.js +++ b/e2e/global-teardown/invalidTeardownWithNamedExport.js @@ -6,7 +6,7 @@ */ function invalidTeardownWithNamedExport(jestConfig): void { - console.log(jestConfig.testPathPattern); + console.log(jestConfig.testPathPatterns); } export {invalidTeardownWithNamedExport}; diff --git a/e2e/global-teardown/teardownWithConfig.js b/e2e/global-teardown/teardownWithConfig.js index 465ea45407c5..ff9b308107b1 100644 --- a/e2e/global-teardown/teardownWithConfig.js +++ b/e2e/global-teardown/teardownWithConfig.js @@ -6,6 +6,6 @@ */ module.exports = function (globalConfig, projectConfig) { - console.log(globalConfig.testPathPattern); + console.log(globalConfig.testPathPatterns); console.log(projectConfig.cache); }; diff --git a/e2e/global-teardown/teardownWithDefaultExport.js b/e2e/global-teardown/teardownWithDefaultExport.js index 4f5b9b90f4e6..85b2c298a4d7 100644 --- a/e2e/global-teardown/teardownWithDefaultExport.js +++ b/e2e/global-teardown/teardownWithDefaultExport.js @@ -6,6 +6,6 @@ */ export default function (globalConfig, projectConfig): void { - console.log(globalConfig.testPathPattern); + console.log(globalConfig.testPathPatterns); console.log(projectConfig.cache); } diff --git a/packages/jest-cli/src/args.ts b/packages/jest-cli/src/args.ts index 869f1a0a707f..4f5b13d7c718 100644 --- a/packages/jest-cli/src/args.ts +++ b/packages/jest-cli/src/args.ts @@ -97,7 +97,7 @@ export function check(argv: Config.Argv): true { } export const usage = - 'Usage: $0 [--config=] [TestPathPattern]'; + 'Usage: $0 [--config=] [TestPathPatterns]'; export const docs = 'Documentation: https://jestjs.io/'; // The default values are all set in jest-config @@ -431,7 +431,7 @@ export const options: {[key: string]: Options} = { }, passWithNoTests: { description: - 'Will not fail if no tests are found (for example while using `--testPathPattern`.)', + 'Will not fail if no tests are found (for example while using `--testPathPatterns`.)', type: 'boolean', }, preset: { @@ -609,7 +609,7 @@ export const options: {[key: string]: Options} = { string: true, type: 'array', }, - testPathPattern: { + testPathPatterns: { description: 'A regexp pattern string that is matched against all tests ' + 'paths before executing the test.', diff --git a/packages/jest-config/src/Deprecated.ts b/packages/jest-config/src/Deprecated.ts index be90f76d75d6..81dd9dc68cd8 100644 --- a/packages/jest-config/src/Deprecated.ts +++ b/packages/jest-config/src/Deprecated.ts @@ -8,6 +8,15 @@ import chalk = require('chalk'); import type {DeprecatedOptions} from 'jest-validate'; +function formatDeprecation(message: string): string { + const lines = [ + message.replace(/\*(.+?)\*/g, (_, s) => chalk.bold(`"${s}"`)), + '', + 'Please update your configuration.', + ]; + return lines.map(s => ` ${s}`).join('\n'); +} + const deprecatedOptions: DeprecatedOptions = { browser: () => ` Option ${chalk.bold( @@ -78,6 +87,11 @@ const deprecatedOptions: DeprecatedOptions = { Please update your configuration. `, + testPathPattern: () => + formatDeprecation( + 'Option *testPathPattern* was replaced by *testPathPatterns*.', + ), + testURL: (_options: {testURL?: string}) => ` Option ${chalk.bold( '"testURL"', )} was replaced by passing the URL via ${chalk.bold( diff --git a/packages/jest-config/src/__tests__/__snapshots__/normalize.test.ts.snap b/packages/jest-config/src/__tests__/__snapshots__/normalize.test.ts.snap index 3a7d0dff6d45..990000454eeb 100644 --- a/packages/jest-config/src/__tests__/__snapshots__/normalize.test.ts.snap +++ b/packages/jest-config/src/__tests__/__snapshots__/normalize.test.ts.snap @@ -482,9 +482,9 @@ exports[`testMatch throws if testRegex and testMatch are both specified 1`] = ` " `; -exports[`testPathPattern ignores invalid regular expressions and logs a warning 1`] = `" Invalid testPattern a( supplied. Running all tests instead."`; +exports[`testPathPatterns ignores invalid regular expressions and logs a warning 1`] = `" Invalid testPattern a( supplied. Running all tests instead."`; -exports[`testPathPattern --testPathPattern ignores invalid regular expressions and logs a warning 1`] = `" Invalid testPattern a( supplied. Running all tests instead."`; +exports[`testPathPatterns --testPathPatterns ignores invalid regular expressions and logs a warning 1`] = `" Invalid testPattern a( supplied. Running all tests instead."`; exports[`testTimeout should throw an error if timeout is a negative number 1`] = ` "Validation Error: diff --git a/packages/jest-config/src/__tests__/normalize.test.ts b/packages/jest-config/src/__tests__/normalize.test.ts index 61429eab0208..cbadba455f16 100644 --- a/packages/jest-config/src/__tests__/normalize.test.ts +++ b/packages/jest-config/src/__tests__/normalize.test.ts @@ -1585,7 +1585,7 @@ describe('watchPlugins', () => { }); }); -describe('testPathPattern', () => { +describe('testPathPatterns', () => { const initialOptions = {rootDir: '/root'}; const consoleLog = console.log; @@ -1600,11 +1600,11 @@ describe('testPathPattern', () => { it('defaults to empty', async () => { const {options} = await normalize(initialOptions, {} as Config.Argv); - expect(options.testPathPattern).toBe(''); + expect(options.testPathPatterns).toEqual([]); }); const cliOptions = [ - {name: '--testPathPattern', property: 'testPathPattern'}, + {name: '--testPathPatterns', property: 'testPathPatterns'}, {name: '', property: '_'}, ]; for (const opt of cliOptions) { @@ -1613,14 +1613,14 @@ describe('testPathPattern', () => { const argv = {[opt.property]: ['a/b']} as Config.Argv; const {options} = await normalize(initialOptions, argv); - expect(options.testPathPattern).toBe('a/b'); + expect(options.testPathPatterns).toEqual(['a/b']); }); it('ignores invalid regular expressions and logs a warning', async () => { const argv = {[opt.property]: ['a(']} as Config.Argv; const {options} = await normalize(initialOptions, argv); - expect(options.testPathPattern).toBe(''); + expect(options.testPathPatterns).toEqual([]); expect(jest.mocked(console.log).mock.calls[0][0]).toMatchSnapshot(); }); @@ -1628,78 +1628,24 @@ describe('testPathPattern', () => { const argv = {[opt.property]: ['a/b', 'c/d']} as Config.Argv; const {options} = await normalize(initialOptions, argv); - expect(options.testPathPattern).toBe('a/b|c/d'); - }); - - it('coerces all patterns to strings', async () => { - const argv = {[opt.property]: [1]} as Config.Argv; - const {options} = await normalize(initialOptions, argv); - - expect(options.testPathPattern).toBe('1'); - }); - - describe('posix', () => { - it('should not escape the pattern', async () => { - const argv = { - [opt.property]: ['a\\/b', 'a/b', 'a\\b', 'a\\\\b'], - } as Config.Argv; - const {options} = await normalize(initialOptions, argv); - - expect(options.testPathPattern).toBe('a\\/b|a/b|a\\b|a\\\\b'); - }); - }); - - describe('win32', () => { - beforeEach(() => { - jest.mock( - 'path', - () => jest.requireActual('path').win32, - ); - ( - require('jest-resolve') as typeof import('jest-resolve') - ).default.findNodeModule = findNodeModule; - }); - - afterEach(() => { - jest.resetModules(); - }); - - it('preserves any use of "\\"', async () => { - const argv = {[opt.property]: ['a\\b', 'c\\\\d']} as Config.Argv; - const {options} = await ( - require('../normalize') as typeof import('../normalize') - ).default(initialOptions, argv); - - expect(options.testPathPattern).toBe('a\\b|c\\\\d'); - }); - - it('replaces POSIX path separators', async () => { - const argv = {[opt.property]: ['a/b']} as Config.Argv; - const {options} = await ( - require('../normalize') as typeof import('../normalize') - ).default(initialOptions, argv); - - expect(options.testPathPattern).toBe('a\\\\b'); - }); - - it('replaces POSIX paths in multiple args', async () => { - const argv = {[opt.property]: ['a/b', 'c/d']} as Config.Argv; - const {options} = await ( - require('../normalize') as typeof import('../normalize') - ).default(initialOptions, argv); - - expect(options.testPathPattern).toBe('a\\\\b|c\\\\d'); - }); + expect(options.testPathPatterns).toEqual(['a/b', 'c/d']); }); }); } + it('coerces patterns to strings', async () => { + const argv = {_: [1]} as Config.Argv; + const {options} = await normalize(initialOptions, argv); + + expect(options.testPathPatterns).toEqual(['1']); + }); + it('joins multiple --testPathPatterns and ', async () => { const {options} = await normalize(initialOptions, { _: ['a', 'b'], - testPathPattern: ['c', 'd'], + testPathPatterns: ['c', 'd'], } as Config.Argv); - expect(options.testPathPattern).toBe('a|b|c|d'); + expect(options.testPathPatterns).toEqual(['a', 'b', 'c', 'd']); }); it('gives precedence to --all', async () => { diff --git a/packages/jest-config/src/__tests__/validatePattern.test.ts b/packages/jest-config/src/__tests__/validatePattern.test.ts deleted file mode 100644 index e664f70bfa7b..000000000000 --- a/packages/jest-config/src/__tests__/validatePattern.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import validatePattern from '../validatePattern'; - -describe('validate pattern function', () => { - it('without passed args returns true', () => { - const isValid = validatePattern(); - - expect(isValid).toBeTruthy(); - }); - - it('returns true for empty pattern', () => { - const isValid = validatePattern(''); - - expect(isValid).toBeTruthy(); - }); - - it('returns true for valid pattern', () => { - const isValid = validatePattern('abc+'); - - expect(isValid).toBeTruthy(); - }); - - it('returns false for invalid pattern', () => { - const isValid = validatePattern('\\'); - - expect(isValid).toBeFalsy(); - }); -}); diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 886e32003df9..7be4d3708258 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -131,7 +131,7 @@ const groupOptions = ( snapshotFormat: options.snapshotFormat, testFailureExitCode: options.testFailureExitCode, testNamePattern: options.testNamePattern, - testPathPattern: options.testPathPattern, + testPathPatterns: options.testPathPatterns, testResultsProcessor: options.testResultsProcessor, testSequencer: options.testSequencer, testTimeout: options.testTimeout, diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index 6ca2b3e6c847..5a2d7fb51b9d 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -22,6 +22,7 @@ import Resolver, { resolveWatchPlugin, } from 'jest-resolve'; import { + TestPathPatterns, clearLine, replacePathSepForGlob, requireOrImportModule, @@ -49,7 +50,6 @@ import { replaceRootDirInPath, resolve, } from './utils'; -import validatePattern from './validatePattern'; const ERROR = `${BULLET}Validation Error`; const PRESET_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs']; @@ -391,44 +391,39 @@ const normalizeReporters = ({ }); }; -const buildTestPathPattern = (argv: Config.Argv): string => { +const buildTestPathPatterns = ( + argv: Config.Argv, + rootDir: string, +): TestPathPatterns => { const patterns = []; if (argv._) { - patterns.push(...argv._); + patterns.push(...argv._.map(x => x.toString())); } - if (argv.testPathPattern) { - patterns.push(...argv.testPathPattern); + if (argv.testPathPatterns) { + patterns.push(...argv.testPathPatterns); } - const replacePosixSep = (pattern: string | number) => { - // yargs coerces positional args into numbers - const patternAsString = pattern.toString(); - if (path.sep === '/') { - return patternAsString; - } - return patternAsString.replace(/\//g, '\\\\'); - }; + const config = {rootDir}; + const testPathPatterns = new TestPathPatterns(patterns, config); - const testPathPattern = patterns.map(replacePosixSep).join('|'); - if (validatePattern(testPathPattern)) { - return testPathPattern; - } else { - showTestPathPatternError(testPathPattern); - return ''; - } -}; + try { + testPathPatterns.validate(); + } catch { + clearLine(process.stdout); + + // eslint-disable-next-line no-console + console.log( + chalk.red( + ` Invalid testPattern ${testPathPatterns.toPretty()} supplied. ` + + 'Running all tests instead.', + ), + ); -const showTestPathPatternError = (testPathPattern: string) => { - clearLine(process.stdout); + return new TestPathPatterns([], config); + } - // eslint-disable-next-line no-console - console.log( - chalk.red( - ` Invalid testPattern ${testPathPattern} supplied. ` + - 'Running all tests instead.', - ), - ); + return testPathPatterns; }; function validateExtensionsToTreatAsEsm( @@ -1007,7 +1002,8 @@ export default async function normalize( } newOptions.nonFlagArgs = argv._?.map(arg => `${arg}`); - newOptions.testPathPattern = buildTestPathPattern(argv); + const testPathPatterns = buildTestPathPatterns(argv, options.rootDir); + newOptions.testPathPatterns = testPathPatterns.patterns; newOptions.json = !!argv.json; newOptions.testFailureExitCode = parseInt( @@ -1026,7 +1022,7 @@ export default async function normalize( if (argv.all) { newOptions.onlyChanged = false; newOptions.onlyFailures = false; - } else if (newOptions.testPathPattern) { + } else if (testPathPatterns.isSet()) { // When passing a test path pattern we don't want to only monitor changed // files unless `--watch` is also passed. newOptions.onlyChanged = newOptions.watch; diff --git a/packages/jest-config/src/validatePattern.ts b/packages/jest-config/src/validatePattern.ts deleted file mode 100644 index 90f3a2c3abe3..000000000000 --- a/packages/jest-config/src/validatePattern.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -export default function validatePattern(pattern?: string): boolean { - if (pattern) { - try { - // eslint-disable-next-line no-new - new RegExp(pattern, 'i'); - } catch { - return false; - } - } - - return true; -} diff --git a/packages/jest-core/src/SearchSource.ts b/packages/jest-core/src/SearchSource.ts index b9227587868b..681464ec7f44 100644 --- a/packages/jest-core/src/SearchSource.ts +++ b/packages/jest-core/src/SearchSource.ts @@ -15,7 +15,7 @@ import {replaceRootDirInPath} from 'jest-config'; import {escapePathForRegex} from 'jest-regex-util'; import {DependencyResolver} from 'jest-resolve-dependencies'; import {buildSnapshotResolver} from 'jest-snapshot'; -import {globsToMatcher, testPathPatternToRegExp} from 'jest-util'; +import {TestPathPatterns, globsToMatcher} from 'jest-util'; import type {Filter, Stats, TestPathCases} from './types'; export type SearchResult = { @@ -110,7 +110,7 @@ export default class SearchSource { private _filterTestPathsWithStats( allPaths: Array, - testPathPattern: string, + testPathPatterns: TestPathPatterns, ): SearchResult { const data: { stats: Stats; @@ -128,13 +128,12 @@ export default class SearchSource { }; const testCases = Array.from(this._testPathCases); // clone - if (testPathPattern) { - const regex = testPathPatternToRegExp(testPathPattern); + if (testPathPatterns.isSet()) { testCases.push({ - isMatch: (path: string) => regex.test(path), - stat: 'testPathPattern', + isMatch: (path: string) => testPathPatterns.isMatch(path), + stat: 'testPathPatterns', }); - data.stats.testPathPattern = 0; + data.stats.testPathPatterns = 0; } data.tests = allPaths.filter(test => { @@ -152,10 +151,10 @@ export default class SearchSource { return data; } - private _getAllTestPaths(testPathPattern: string): SearchResult { + private _getAllTestPaths(testPathPatterns: TestPathPatterns): SearchResult { return this._filterTestPathsWithStats( toTests(this._context, this._context.hasteFS.getAllFiles()), - testPathPattern, + testPathPatterns, ); } @@ -163,8 +162,8 @@ export default class SearchSource { return this._testPathCases.every(testCase => testCase.isMatch(path)); } - findMatchingTests(testPathPattern: string): SearchResult { - return this._getAllTestPaths(testPathPattern); + findMatchingTests(testPathPatterns: TestPathPatterns): SearchResult { + return this._getAllTestPaths(testPathPatterns); } async findRelatedTests( @@ -287,10 +286,10 @@ export default class SearchSource { paths, globalConfig.collectCoverage, ); - } else if (globalConfig.testPathPattern == null) { - return {tests: []}; } else { - return this.findMatchingTests(globalConfig.testPathPattern); + return this.findMatchingTests( + TestPathPatterns.fromGlobalConfig(globalConfig), + ); } } diff --git a/packages/jest-core/src/__tests__/SearchSource.test.ts b/packages/jest-core/src/__tests__/SearchSource.test.ts index 833240c2fde9..fc93a8f03d7f 100644 --- a/packages/jest-core/src/__tests__/SearchSource.test.ts +++ b/packages/jest-core/src/__tests__/SearchSource.test.ts @@ -109,7 +109,8 @@ describe('SearchSource', () => { const {searchSource, config} = await initSearchSource(initialOptions); const {tests: paths} = await searchSource.getTestPaths({ ...config, - testPathPattern: '', + ...initialOptions, + testPathPatterns: [], }); return paths.map(({path: p}) => path.relative(rootDir, p)).sort(); }; diff --git a/packages/jest-core/src/__tests__/__snapshots__/watchFilenamePatternMode.test.js.snap b/packages/jest-core/src/__tests__/__snapshots__/watchFilenamePatternMode.test.js.snap index 9e5b82d69a78..724b415277b6 100644 --- a/packages/jest-core/src/__tests__/__snapshots__/watchFilenamePatternMode.test.js.snap +++ b/packages/jest-core/src/__tests__/__snapshots__/watchFilenamePatternMode.test.js.snap @@ -92,7 +92,10 @@ exports[`Watch mode flows Pressing "P" enters pattern mode 9`] = ` Object { "onlyChanged": false, "passWithNoTests": true, - "testPathPattern": "p.*3", + "rootDir": "", + "testPathPatterns": Array [ + "p.*3", + ], "watch": true, "watchAll": false, } diff --git a/packages/jest-core/src/__tests__/getNoTestsFoundMessage.test.ts b/packages/jest-core/src/__tests__/getNoTestsFoundMessage.test.ts index 994db8068d8b..19fd684dea0a 100644 --- a/packages/jest-core/src/__tests__/getNoTestsFoundMessage.test.ts +++ b/packages/jest-core/src/__tests__/getNoTestsFoundMessage.test.ts @@ -18,7 +18,7 @@ describe('getNoTestsFoundMessage', () => { function createGlobalConfig(options?: Partial) { return makeGlobalConfig({ rootDir: '/root/dir', - testPathPattern: '/path/pattern', + testPathPatterns: ['/path/pattern'], ...options, }); } diff --git a/packages/jest-core/src/__tests__/runJest.test.js b/packages/jest-core/src/__tests__/runJest.test.js index 7661503ae95a..559a8bdb8baf 100644 --- a/packages/jest-core/src/__tests__/runJest.test.js +++ b/packages/jest-core/src/__tests__/runJest.test.js @@ -22,6 +22,8 @@ describe('runJest', () => { changedFilesPromise: Promise.resolve({repos: {git: {size: 0}}}), contexts: [], globalConfig: { + rootDir: '', + testPathPatterns: [], testSequencer: require.resolve('@jest/test-sequencer'), watch: true, }, diff --git a/packages/jest-core/src/__tests__/watch.test.js b/packages/jest-core/src/__tests__/watch.test.js index a29b1d285b4b..4425b8285800 100644 --- a/packages/jest-core/src/__tests__/watch.test.js +++ b/packages/jest-core/src/__tests__/watch.test.js @@ -140,7 +140,11 @@ describe('Watch mode flows', () => { testRegex: [], }; pipe = {write: jest.fn()}; - globalConfig = {watch: true}; + globalConfig = { + rootDir: '', + testPathPatterns: [], + watch: true, + }; hasteMapInstances = [{on: () => {}}]; contexts = [{config}]; stdin = new MockStdin(); @@ -152,7 +156,7 @@ describe('Watch mode flows', () => { }); it('Correctly passing test path pattern', async () => { - globalConfig.testPathPattern = 'test-*'; + globalConfig.testPathPatterns = ['test-*']; await watch(globalConfig, contexts, pipe, hasteMapInstances, stdin); @@ -671,7 +675,7 @@ describe('Watch mode flows', () => { ${'✖︎'} | ${'skipFilter'} ${'✖︎'} | ${'testFailureExitCode'} ${'✔︎'} | ${'testNamePattern'} - ${'✔︎'} | ${'testPathPattern'} + ${'✔︎'} | ${'testPathPatterns'} ${'✖︎'} | ${'testResultsProcessor'} ${'✔︎'} | ${'updateSnapshot'} ${'✖︎'} | ${'useStderr'} @@ -898,7 +902,7 @@ describe('Watch mode flows', () => { await nextTick(); expect(runJestMock.mock.calls[0][0].globalConfig).toMatchObject({ - testPathPattern: 'file', + testPathPatterns: ['file'], watch: true, watchAll: false, }); @@ -922,7 +926,7 @@ describe('Watch mode flows', () => { expect(runJestMock.mock.calls[1][0].globalConfig).toMatchObject({ testNamePattern: 'test', - testPathPattern: 'file', + testPathPatterns: ['file'], watch: true, watchAll: false, }); diff --git a/packages/jest-core/src/__tests__/watchFilenamePatternMode.test.js b/packages/jest-core/src/__tests__/watchFilenamePatternMode.test.js index 9d84afe3c8df..2900fe6d02e8 100644 --- a/packages/jest-core/src/__tests__/watchFilenamePatternMode.test.js +++ b/packages/jest-core/src/__tests__/watchFilenamePatternMode.test.js @@ -70,7 +70,11 @@ const watch = require('../watch').default; const nextTick = () => new Promise(res => process.nextTick(res)); -const globalConfig = {watch: true}; +const globalConfig = { + rootDir: '', + testPathPatterns: [], + watch: true, +}; afterEach(runJestMock.mockReset); diff --git a/packages/jest-core/src/__tests__/watchTestNamePatternMode.test.js b/packages/jest-core/src/__tests__/watchTestNamePatternMode.test.js index c07e78f65d38..0763d17bf0a0 100644 --- a/packages/jest-core/src/__tests__/watchTestNamePatternMode.test.js +++ b/packages/jest-core/src/__tests__/watchTestNamePatternMode.test.js @@ -83,6 +83,8 @@ jest.doMock( const watch = require('../watch').default; const globalConfig = { + rootDir: '', + testPathPatterns: [], watch: true, }; diff --git a/packages/jest-core/src/getNoTestFound.ts b/packages/jest-core/src/getNoTestFound.ts index 931de697aa24..cbfe05cbf6e9 100644 --- a/packages/jest-core/src/getNoTestFound.ts +++ b/packages/jest-core/src/getNoTestFound.ts @@ -7,7 +7,7 @@ import chalk = require('chalk'); import type {Config} from '@jest/types'; -import {pluralize} from 'jest-util'; +import {TestPathPatterns, pluralize} from 'jest-util'; import type {TestRunData} from './types'; export default function getNoTestFound( @@ -26,8 +26,9 @@ export default function getNoTestFound( .map(p => `"${p}"`) .join(', ')}`; } else { + const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig); dataMessage = `Pattern: ${chalk.yellow( - globalConfig.testPathPattern, + testPathPatterns.toPretty(), )} - 0 matches`; } diff --git a/packages/jest-core/src/getNoTestFoundVerbose.ts b/packages/jest-core/src/getNoTestFoundVerbose.ts index 0f9ef7f1de00..9c80b38a4e5f 100644 --- a/packages/jest-core/src/getNoTestFoundVerbose.ts +++ b/packages/jest-core/src/getNoTestFoundVerbose.ts @@ -7,7 +7,7 @@ import chalk = require('chalk'); import type {Config} from '@jest/types'; -import {pluralize} from 'jest-util'; +import {TestPathPatterns, pluralize} from 'jest-util'; import type {Stats, TestRunData} from './types'; export default function getNoTestFoundVerbose( @@ -56,8 +56,9 @@ export default function getNoTestFoundVerbose( .map(p => `"${p}"`) .join(', ')}`; } else { + const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig); dataMessage = `Pattern: ${chalk.yellow( - globalConfig.testPathPattern, + testPathPatterns.toPretty(), )} - 0 matches`; } diff --git a/packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap b/packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap index 1f288c9a76d1..0087fa7f5351 100644 --- a/packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap +++ b/packages/jest-core/src/lib/__tests__/__snapshots__/logDebugMessages.test.ts.snap @@ -109,7 +109,7 @@ exports[`prints the config object 1`] = ` "snapshotFormat": {}, "testFailureExitCode": 1, "testNamePattern": "", - "testPathPattern": "", + "testPathPatterns": [], "testSequencer": "@jest/test-sequencer", "testTimeout": 5000, "updateSnapshot": "none", diff --git a/packages/jest-core/src/lib/activeFiltersMessage.ts b/packages/jest-core/src/lib/activeFiltersMessage.ts index 714adcff7d86..170760456cda 100644 --- a/packages/jest-core/src/lib/activeFiltersMessage.ts +++ b/packages/jest-core/src/lib/activeFiltersMessage.ts @@ -7,14 +7,15 @@ import chalk = require('chalk'); import type {Config} from '@jest/types'; -import {isNonNullable} from 'jest-util'; +import {TestPathPatterns, isNonNullable} from 'jest-util'; const activeFilters = (globalConfig: Config.GlobalConfig): string => { - const {testNamePattern, testPathPattern} = globalConfig; - if (testNamePattern || testPathPattern) { + const {testNamePattern} = globalConfig; + const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig); + if (testNamePattern || testPathPatterns.isSet()) { const filters = [ - testPathPattern - ? chalk.dim('filename ') + chalk.yellow(`/${testPathPattern}/`) + testPathPatterns.isSet() + ? chalk.dim('filename ') + chalk.yellow(testPathPatterns.toPretty()) : null, testNamePattern ? chalk.dim('test name ') + chalk.yellow(`/${testNamePattern}/`) diff --git a/packages/jest-core/src/lib/updateGlobalConfig.ts b/packages/jest-core/src/lib/updateGlobalConfig.ts index a81990358755..a05dc5ae9fcd 100644 --- a/packages/jest-core/src/lib/updateGlobalConfig.ts +++ b/packages/jest-core/src/lib/updateGlobalConfig.ts @@ -6,7 +6,7 @@ */ import type {Config} from '@jest/types'; -import {replacePathSepForRegex} from 'jest-regex-util'; +import {TestPathPatterns} from 'jest-util'; import type {AllowedConfigOptions} from 'jest-watcher'; type ExtraConfigOptions = Partial< @@ -31,15 +31,14 @@ export default function updateGlobalConfig( newConfig.testNamePattern = options.testNamePattern || ''; } - if (options.testPathPattern !== undefined) { - newConfig.testPathPattern = - replacePathSepForRegex(options.testPathPattern) || ''; + if (options.testPathPatterns !== undefined) { + newConfig.testPathPatterns = options.testPathPatterns; } newConfig.onlyChanged = !newConfig.watchAll && !newConfig.testNamePattern && - !newConfig.testPathPattern; + !TestPathPatterns.fromGlobalConfig(newConfig).isSet(); if (typeof options.bail === 'boolean') { newConfig.bail = options.bail ? 1 : 0; diff --git a/packages/jest-core/src/plugins/FailedTestsInteractive.ts b/packages/jest-core/src/plugins/FailedTestsInteractive.ts index 39dd8756e8c8..864f78a7d86c 100644 --- a/packages/jest-core/src/plugins/FailedTestsInteractive.ts +++ b/packages/jest-core/src/plugins/FailedTestsInteractive.ts @@ -58,7 +58,7 @@ export default class FailedTestsInteractivePlugin extends BaseWatchPlugin { updateConfigAndRun({ mode: 'watch', testNamePattern: failure ? `^${failure.fullName}$` : '', - testPathPattern: failure?.path || '', + testPathPatterns: failure ? [failure.path] : [], }); if (!this._manager.isActive()) { diff --git a/packages/jest-core/src/plugins/TestPathPattern.ts b/packages/jest-core/src/plugins/TestPathPattern.ts index a57d76488f68..c777b7183027 100644 --- a/packages/jest-core/src/plugins/TestPathPattern.ts +++ b/packages/jest-core/src/plugins/TestPathPattern.ts @@ -48,7 +48,7 @@ class TestPathPatternPlugin extends BaseWatchPlugin { testPathPatternPrompt.run( (value: string) => { - updateConfigAndRun({mode: 'watch', testPathPattern: value}); + updateConfigAndRun({mode: 'watch', testPathPatterns: [value]}); res(); }, rej, diff --git a/packages/jest-core/src/plugins/UpdateSnapshotsInteractive.ts b/packages/jest-core/src/plugins/UpdateSnapshotsInteractive.ts index 25c8d3516cd9..44eeaf20c572 100644 --- a/packages/jest-core/src/plugins/UpdateSnapshotsInteractive.ts +++ b/packages/jest-core/src/plugins/UpdateSnapshotsInteractive.ts @@ -70,7 +70,7 @@ class UpdateSnapshotInteractivePlugin extends BaseWatchPlugin { updateConfigAndRun({ mode: 'watch', testNamePattern: assertion ? `^${assertion.fullName}$` : '', - testPathPattern: assertion ? assertion.path : '', + testPathPatterns: assertion ? [assertion.path] : [], updateSnapshot: shouldUpdateSnapshot ? 'all' : 'none', }); diff --git a/packages/jest-core/src/plugins/__tests__/FailedTestsInteractive.test.js b/packages/jest-core/src/plugins/__tests__/FailedTestsInteractive.test.js index c416c423d1cd..4b64a2a2c3dc 100644 --- a/packages/jest-core/src/plugins/__tests__/FailedTestsInteractive.test.js +++ b/packages/jest-core/src/plugins/__tests__/FailedTestsInteractive.test.js @@ -40,7 +40,7 @@ describe('FailedTestsInteractive', () => { expect(mockUpdate).toHaveBeenCalledWith({ mode: 'watch', testNamePattern: `^${testAggregate.testResults[0].testResults[0].fullName}$`, - testPathPattern: testAggregate.testResults[0].testFilePath, + testPathPatterns: [testAggregate.testResults[0].testFilePath], }); }); }); diff --git a/packages/jest-core/src/types.ts b/packages/jest-core/src/types.ts index 378cfde4973e..3aaece51f060 100644 --- a/packages/jest-core/src/types.ts +++ b/packages/jest-core/src/types.ts @@ -12,7 +12,7 @@ export type Stats = { testMatch: number; testPathIgnorePatterns: number; testRegex: number; - testPathPattern?: number; + testPathPatterns?: number; }; export type TestRunData = Array<{ @@ -31,7 +31,7 @@ export type TestPathCases = Array<{ }>; export type TestPathCasesWithPathPattern = TestPathCases & { - testPathPattern: (path: string) => boolean; + testPathPatterns: (path: string) => boolean; }; export type FilterResult = { diff --git a/packages/jest-core/src/watch.ts b/packages/jest-core/src/watch.ts index 6ba601a39a64..696ed903040d 100644 --- a/packages/jest-core/src/watch.ts +++ b/packages/jest-core/src/watch.ts @@ -15,6 +15,7 @@ import type {Config} from '@jest/types'; import type {IHasteMap as HasteMap} from 'jest-haste-map'; import {formatExecError} from 'jest-message-util'; import { + TestPathPatterns, isInteractive, preRunMessage, requireOrImportModule, @@ -120,7 +121,7 @@ export default async function watch( onlyFailures, reporters, testNamePattern, - testPathPattern, + testPathPatterns, updateSnapshot, verbose, }: AllowedConfigOptions = {}) => { @@ -140,7 +141,7 @@ export default async function watch( onlyFailures, reporters, testNamePattern, - testPathPattern, + testPathPatterns, updateSnapshot, verbose, }); @@ -227,9 +228,12 @@ export default async function watch( const emitFileChange = () => { if (hooks.isUsed('onFileChange')) { + const testPathPatterns = new TestPathPatterns([], globalConfig); const projects = searchSources.map(({context, searchSource}) => ({ config: context.config, - testPaths: searchSource.findMatchingTests('').tests.map(t => t.path), + testPaths: searchSource + .findMatchingTests(testPathPatterns) + .tests.map(t => t.path), })); hooks.getEmitter().onFileChange({projects}); } @@ -404,7 +408,7 @@ export default async function watch( globalConfig = updateGlobalConfig(globalConfig, { mode: 'watchAll', testNamePattern: '', - testPathPattern: '', + testPathPatterns: [], }); startRun(globalConfig); break; @@ -412,7 +416,7 @@ export default async function watch( updateConfigAndRun({ mode: 'watch', testNamePattern: '', - testPathPattern: '', + testPathPatterns: [], }); break; case 'f': @@ -425,7 +429,7 @@ export default async function watch( globalConfig = updateGlobalConfig(globalConfig, { mode: 'watch', testNamePattern: '', - testPathPattern: '', + testPathPatterns: [], }); startRun(globalConfig); break; @@ -528,10 +532,11 @@ const usage = ( watchPlugins: Array, delimiter = '\n', ) => { + const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig); const messages = [ activeFilters(globalConfig), - globalConfig.testPathPattern || globalConfig.testNamePattern + testPathPatterns.isSet() || globalConfig.testNamePattern ? `${chalk.dim(' \u203A Press ')}c${chalk.dim(' to clear filters.')}` : null, `\n${chalk.bold('Watch Usage')}`, @@ -549,7 +554,7 @@ const usage = ( )}`, (globalConfig.watchAll || - globalConfig.testPathPattern || + testPathPatterns.isSet() || globalConfig.testNamePattern) && !globalConfig.noSCM ? `${chalk.dim(' \u203A Press ')}o${chalk.dim( diff --git a/packages/jest-reporters/src/SummaryReporter.ts b/packages/jest-reporters/src/SummaryReporter.ts index 3c43b62367d9..9a34e1cb7a9f 100644 --- a/packages/jest-reporters/src/SummaryReporter.ts +++ b/packages/jest-reporters/src/SummaryReporter.ts @@ -12,7 +12,7 @@ import type { TestContext, } from '@jest/test-result'; import type {Config} from '@jest/types'; -import {testPathPatternToRegExp} from 'jest-util'; +import {TestPathPatterns} from 'jest-util'; import BaseReporter from './BaseReporter'; import getResultHeader from './getResultHeader'; import getSnapshotSummary from './getSnapshotSummary'; @@ -212,15 +212,14 @@ export default class SummaryReporter extends BaseReporter { testContexts: Set, globalConfig: Config.GlobalConfig, ) { + const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig); + const getMatchingTestsInfo = () => { const prefix = globalConfig.findRelatedTests ? ' related to files matching ' : ' matching '; - return ( - chalk.dim(prefix) + - testPathPatternToRegExp(globalConfig.testPathPattern).toString() - ); + return chalk.dim(prefix) + testPathPatterns.toPretty(); }; let testInfo = ''; @@ -229,7 +228,7 @@ export default class SummaryReporter extends BaseReporter { testInfo = chalk.dim(' within paths'); } else if (globalConfig.onlyChanged) { testInfo = chalk.dim(' related to changed files'); - } else if (globalConfig.testPathPattern) { + } else if (testPathPatterns.isSet()) { testInfo = getMatchingTestsInfo(); } diff --git a/packages/jest-reporters/src/__tests__/SummaryReporter.test.js b/packages/jest-reporters/src/__tests__/SummaryReporter.test.js index fb83c448194a..6ace91cd6c5f 100644 --- a/packages/jest-reporters/src/__tests__/SummaryReporter.test.js +++ b/packages/jest-reporters/src/__tests__/SummaryReporter.test.js @@ -13,6 +13,7 @@ const now = Date.now; const write = process.stderr.write; const globalConfig = { rootDir: 'root', + testPathPatterns: [], watch: false, }; diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 589e7b6cd3f3..34afe821412e 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -412,7 +412,7 @@ export type GlobalConfig = { errorOnDeprecated: boolean; testFailureExitCode: number; testNamePattern?: string; - testPathPattern: string; + testPathPatterns: Array; testResultsProcessor?: string; testSequencer: string; testTimeout?: number; @@ -569,7 +569,7 @@ export type Argv = Arguments< testMatch: Array; testNamePattern: string; testPathIgnorePatterns: Array; - testPathPattern: Array; + testPathPatterns: Array; testRegex: string | Array; testResultsProcessor: string; testRunner: string; diff --git a/packages/jest-util/Readme.md b/packages/jest-util/Readme.md index 15f6daa4ad81..8b03e1ef7862 100644 --- a/packages/jest-util/Readme.md +++ b/packages/jest-util/Readme.md @@ -78,9 +78,9 @@ Used to set properties with specified values within a global object. It is desig It defines constants and conditional values for handling platform-specific behaviors in a terminal environment. It determines if the current platform is Windows ('win32') and sets up constants for various symbols and terminal screen clearing escape sequences accordingly, ensuring proper display and behavior on both Windows and non-Windows operating systems. -## `testPathPatternToRegExp` +## `TestPathPatterns` -This function is used for consistency when serializing/deserializing global configurations and ensures that consistent regular expressions are produced for matching test paths. +This class takes test patterns and provides the API for deciding if a test matches any of the patterns. ## `tryRealpath` diff --git a/packages/jest-util/src/TestPathPatterns.ts b/packages/jest-util/src/TestPathPatterns.ts new file mode 100644 index 000000000000..2c5856e4932a --- /dev/null +++ b/packages/jest-util/src/TestPathPatterns.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type {Config} from '@jest/types'; +import {escapePathForRegex, replacePathSepForRegex} from 'jest-regex-util'; + +type PatternsConfig = { + rootDir: string; +}; + +export default class TestPathPatterns { + private _regexString: string | null = null; + + constructor( + readonly patterns: Array, + private readonly config: PatternsConfig, + ) {} + + static fromGlobalConfig(globalConfig: Config.GlobalConfig): TestPathPatterns { + return new TestPathPatterns(globalConfig.testPathPatterns, globalConfig); + } + + private get regexString(): string { + if (this._regexString !== null) { + return this._regexString; + } + + const rootDir = this.config.rootDir.replace(/\/*$/, '/'); + const rootDirRegex = escapePathForRegex(rootDir); + + const regexString = this.patterns + .map(p => { + // absolute paths passed on command line should stay same + if (p.match(/^\//)) { + return p; + } + + // explicit relative paths should resolve against rootDir + if (p.match(/^\.\//)) { + return p.replace(/^\.\//, rootDirRegex); + } + + // all other patterns should only match the relative part of the test + return `${rootDirRegex}(.*)?${p}`; + }) + .map(replacePathSepForRegex) + .join('|'); + + this._regexString = regexString; + return regexString; + } + + private toRegex(): RegExp { + return new RegExp(this.regexString, 'i'); + } + + /** + * Return true if there are any patterns. + */ + isSet(): boolean { + return this.patterns.length > 0; + } + + /** + * Throw an error if the patterns don't form a valid regex. + */ + validate(): void { + this.toRegex(); + } + + /** + * Return true if the given ABSOLUTE path matches the patterns. + * + * Throws an error if the patterns form an invalid regex (see `validate`). + */ + isMatch(path: string): boolean { + return this.toRegex().test(path); + } + + /** + * Return a human-friendly version of the pattern regex. + */ + toPretty(): string { + return this.patterns.join('|'); + } +} diff --git a/packages/jest-util/src/__tests__/TestPathPatterns.test.ts b/packages/jest-util/src/__tests__/TestPathPatterns.test.ts new file mode 100644 index 000000000000..39c8f272efa1 --- /dev/null +++ b/packages/jest-util/src/__tests__/TestPathPatterns.test.ts @@ -0,0 +1,163 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type * as path from 'path'; +import TestPathPatterns from '../TestPathPatterns'; + +const mockSep = jest.fn(); +jest.mock('path', () => { + return { + ...(jest.requireActual('path') as typeof path), + get sep() { + return mockSep() || '/'; + }, + }; +}); +beforeEach(() => { + jest.resetAllMocks(); +}); + +const config = {rootDir: ''}; + +describe('TestPathPatterns', () => { + describe('isSet', () => { + it('returns false if no patterns specified', () => { + const testPathPatterns = new TestPathPatterns([], config); + expect(testPathPatterns.isSet()).toBe(false); + }); + + it('returns true if patterns specified', () => { + const testPathPatterns = new TestPathPatterns(['a'], config); + expect(testPathPatterns.isSet()).toBe(true); + }); + }); + + describe('validate', () => { + it('succeeds for empty patterns', () => { + const testPathPatterns = new TestPathPatterns([], config); + expect(() => testPathPatterns.validate()).not.toThrow(); + }); + + it('succeeds for valid patterns', () => { + const testPathPatterns = new TestPathPatterns(['abc+', 'z.*'], config); + expect(() => testPathPatterns.validate()).not.toThrow(); + }); + + it('fails for at least one invalid pattern', () => { + const testPathPatterns = new TestPathPatterns( + ['abc+', '(', 'z.*'], + config, + ); + expect(() => testPathPatterns.validate()).toThrow( + 'Invalid regular expression', + ); + }); + }); + + describe('isMatch', () => { + it('returns true with no patterns', () => { + const testPathPatterns = new TestPathPatterns([], config); + expect(testPathPatterns.isMatch('/a/b')).toBe(true); + }); + + it('returns true for same path', () => { + const testPathPatterns = new TestPathPatterns(['/a/b'], config); + expect(testPathPatterns.isMatch('/a/b')).toBe(true); + }); + + it('returns true for same path with case insensitive', () => { + const testPathPatternsUpper = new TestPathPatterns(['/A/B'], config); + expect(testPathPatternsUpper.isMatch('/a/b')).toBe(true); + expect(testPathPatternsUpper.isMatch('/A/B')).toBe(true); + + const testPathPatternsLower = new TestPathPatterns(['/a/b'], config); + expect(testPathPatternsLower.isMatch('/A/B')).toBe(true); + expect(testPathPatternsLower.isMatch('/a/b')).toBe(true); + }); + + it('returns true for contained path', () => { + const testPathPatterns = new TestPathPatterns(['b/c'], config); + expect(testPathPatterns.isMatch('/a/b/c/d')).toBe(true); + }); + + it('returns true for explicit relative path', () => { + const testPathPatterns = new TestPathPatterns(['./b/c'], { + rootDir: '/a', + }); + expect(testPathPatterns.isMatch('/a/b/c')).toBe(true); + }); + + it('returns true for partial file match', () => { + const testPathPatterns = new TestPathPatterns(['aaa'], config); + expect(testPathPatterns.isMatch('/foo/..aaa..')).toBe(true); + expect(testPathPatterns.isMatch('/foo/..aaa')).toBe(true); + expect(testPathPatterns.isMatch('/foo/aaa..')).toBe(true); + }); + + it('returns true for path suffix', () => { + const testPathPatterns = new TestPathPatterns(['c/d'], config); + expect(testPathPatterns.isMatch('/a/b/c/d')).toBe(true); + }); + + it('returns true if regex matches', () => { + const testPathPatterns = new TestPathPatterns(['ab*c?'], config); + + expect(testPathPatterns.isMatch('/foo/a')).toBe(true); + expect(testPathPatterns.isMatch('/foo/ab')).toBe(true); + expect(testPathPatterns.isMatch('/foo/abb')).toBe(true); + expect(testPathPatterns.isMatch('/foo/ac')).toBe(true); + expect(testPathPatterns.isMatch('/foo/abc')).toBe(true); + expect(testPathPatterns.isMatch('/foo/abbc')).toBe(true); + + expect(testPathPatterns.isMatch('/foo/bc')).toBe(false); + }); + + it('returns true only if matches relative path', () => { + const testPathPatterns = new TestPathPatterns(['home'], { + rootDir: '/home/myuser/', + }); + expect(testPathPatterns.isMatch('/home/myuser/LoginPage.js')).toBe(false); + expect(testPathPatterns.isMatch('/home/myuser/HomePage.js')).toBe(true); + }); + + it('matches absolute paths regardless of rootDir', () => { + const testPathPatterns = new TestPathPatterns(['/a/b'], { + rootDir: '/foo/bar', + }); + expect(testPathPatterns.isMatch('/a/b')).toBe(true); + }); + + it('returns true if match any paths', () => { + const testPathPatterns = new TestPathPatterns(['a/b', 'c/d'], config); + + expect(testPathPatterns.isMatch('/foo/a/b')).toBe(true); + expect(testPathPatterns.isMatch('/foo/c/d')).toBe(true); + + expect(testPathPatterns.isMatch('/foo/a')).toBe(false); + expect(testPathPatterns.isMatch('/foo/b/c')).toBe(false); + }); + + it('does not normalize Windows paths on POSIX', () => { + mockSep.mockReturnValue('/'); + const testPathPatterns = new TestPathPatterns(['a\\z', 'a\\\\z'], config); + expect(testPathPatterns.isMatch('/foo/a/z')).toBe(false); + }); + + it('normalizes paths for Windows', () => { + mockSep.mockReturnValue('\\'); + const testPathPatterns = new TestPathPatterns(['a/b'], config); + expect(testPathPatterns.isMatch('\\foo\\a\\b')).toBe(true); + }); + }); + + describe('toPretty', () => { + it('renders a human-readable string', () => { + const testPathPatterns = new TestPathPatterns(['a/b', 'c/d'], config); + expect(testPathPatterns.toPretty()).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/jest-util/src/__tests__/__snapshots__/TestPathPatterns.test.ts.snap b/packages/jest-util/src/__tests__/__snapshots__/TestPathPatterns.test.ts.snap new file mode 100644 index 000000000000..5b97f3ca2f59 --- /dev/null +++ b/packages/jest-util/src/__tests__/__snapshots__/TestPathPatterns.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TestPathPatterns toPretty renders a human-readable string 1`] = `"a/b|c/d"`; diff --git a/packages/jest-util/src/index.ts b/packages/jest-util/src/index.ts index 531acfe4aa3a..ca4ea9ec29d1 100644 --- a/packages/jest-util/src/index.ts +++ b/packages/jest-util/src/index.ts @@ -21,7 +21,7 @@ export {default as deepCyclicCopy} from './deepCyclicCopy'; export {default as convertDescriptorToString} from './convertDescriptorToString'; export {specialChars}; export {default as replacePathSepForGlob} from './replacePathSepForGlob'; -export {default as testPathPatternToRegExp} from './testPathPatternToRegExp'; +export {default as TestPathPatterns} from './TestPathPatterns'; export {default as globsToMatcher} from './globsToMatcher'; export {preRunMessage}; export {default as pluralize} from './pluralize'; diff --git a/packages/jest-util/src/testPathPatternToRegExp.ts b/packages/jest-util/src/testPathPatternToRegExp.ts deleted file mode 100644 index 433779ab206d..000000000000 --- a/packages/jest-util/src/testPathPatternToRegExp.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import type {Config} from '@jest/types'; - -// Because we serialize/deserialize globalConfig when we spawn workers, -// we can't pass regular expression. Using this shared function on both sides -// will ensure that we produce consistent regexp for testPathPattern. -export default function testPathPatternToRegExp( - testPathPattern: Config.GlobalConfig['testPathPattern'], -): RegExp { - return new RegExp(testPathPattern, 'i'); -} diff --git a/packages/jest-watcher/src/types.ts b/packages/jest-watcher/src/types.ts index ed3c1950f6ef..fe69d836187b 100644 --- a/packages/jest-watcher/src/types.ts +++ b/packages/jest-watcher/src/types.ts @@ -62,7 +62,7 @@ export type AllowedConfigOptions = Partial< | 'onlyFailures' | 'reporters' | 'testNamePattern' - | 'testPathPattern' + | 'testPathPatterns' | 'updateSnapshot' | 'verbose' > & {mode: 'watch' | 'watchAll'} diff --git a/packages/test-utils/src/config.ts b/packages/test-utils/src/config.ts index a27bf9c874b1..5aff9e0a15af 100644 --- a/packages/test-utils/src/config.ts +++ b/packages/test-utils/src/config.ts @@ -55,7 +55,7 @@ const DEFAULT_GLOBAL_CONFIG: Config.GlobalConfig = { snapshotFormat: {}, testFailureExitCode: 1, testNamePattern: '', - testPathPattern: '', + testPathPatterns: [], testResultsProcessor: undefined, testSequencer: '@jest/test-sequencer', testTimeout: 5000,