diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaa1257f146..6fdcf30187aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[@jest/core, @jest/cli, @jest/resolve-dependencies]` Add `--maxRelatedTestsDepth` flag to control the depth of related test selection ([#15495](https://github.com/jestjs/jest/pull/15495)) - `[babel-jest]` Add option `excludeJestPreset` to allow opting out of `babel-preset-jest` ([#15164](https://github.com/jestjs/jest/pull/15164)) - `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681)) - `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738)) diff --git a/docs/CLI.md b/docs/CLI.md index 59fcd897806e..3a7fd6bcade1 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -34,6 +34,12 @@ Run tests related to `path/to/fileA.js` and `path/to/fileB.js`: jest --findRelatedTests path/to/fileA.js path/to/fileB.js ``` +Used with `--findRelatedTests`, sets the maximum import depth that it will accept tests from. + +```bash +jest --maxRelatedTestsDepth=5 +``` + Run tests that match this spec name (match against the name in `describe` or `test`, basically). ```bash @@ -213,6 +219,10 @@ module.exports = testPaths => { Find and run the tests that cover a space separated list of source files that were passed in as arguments. Useful for pre-commit hook integration to run the minimal amount of tests necessary. Can be used together with `--coverage` to include a test coverage for the source files, no duplicate `--collectCoverageFrom` arguments needed. +### `--maxRelatedTestsDepth=` + +Used with `--findRelatedTests`, limits how deep Jest will traverse the dependency graph when finding related tests. A test at depth 1 directly imports the changed file, a test at depth 2 imports a file that imports the changed file, and so on. This is useful for large projects where you want to limit the scope of related test execution. + ### `--forceExit` Force Jest to exit after all tests have completed running. This is useful when resources set up by test code cannot be adequately cleaned up. diff --git a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap index b28bc91cfb4a..af7d9773995a 100644 --- a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap +++ b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap @@ -126,6 +126,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` "listTests": false, "logHeapUsage": false, "maxConcurrency": 5, + "maxRelatedTestsDepth": Infinity, "maxWorkers": "[maxWorkers]", "noStackTrace": false, "nonFlagArgs": [], diff --git a/e2e/__tests__/findRelatedFiles.test.ts b/e2e/__tests__/findRelatedFiles.test.ts index f1ba71fe4c61..502c2a69516f 100644 --- a/e2e/__tests__/findRelatedFiles.test.ts +++ b/e2e/__tests__/findRelatedFiles.test.ts @@ -262,4 +262,50 @@ describe('--findRelatedTests flag', () => { expect(stdout).toMatch('No tests found'); expect(stderr).toBe(''); }); + + test.each([ + [4, ['a', 'b', 'b2', 'c', 'd']], + [3, ['a', 'b', 'b2', 'c']], + [2, ['a', 'b', 'b2']], + [1, ['a']], + ])( + 'runs tests with dependency chain and --maxRelatedTestsDepth=%d', + (depth, expectedTests) => { + writeFiles(DIR, { + '.watchmanconfig': '{}', + '__tests__/a.test.js': + "const a = require('../a'); test('a', () => {expect(a).toBe(\"value\")});", + '__tests__/b.test.js': + "const b = require('../b'); test('b', () => {expect(b).toBe(\"value\")});", + '__tests__/b2.test.js': + "const b = require('../b2'); test('b', () => {expect(b).toBe(\"value\")});", + '__tests__/c.test.js': + "const c = require('../c'); test('c', () => {expect(c).toBe(\"value\")});", + '__tests__/d.test.js': + "const d = require('../d'); test('d', () => {expect(d).toBe(\"value\")});", + 'a.js': 'module.exports = "value";', + 'b.js': 'module.exports = require("./a");', + 'b2.js': 'module.exports = require("./a");', + 'c.js': 'module.exports = require("./b");', + 'd.js': 'module.exports = require("./c");', + 'package.json': JSON.stringify({jest: {testEnvironment: 'node'}}), + }); + + const {stderr} = runJest(DIR, [ + `--maxRelatedTestsDepth=${depth}`, + '--findRelatedTests', + 'a.js', + ]); + + for (const testFile of expectedTests) { + expect(stderr).toMatch(`PASS __tests__/${testFile}.test.js`); + } + + for (const testFile of ['a', 'b', 'b2', 'c', 'd']) { + if (!expectedTests.includes(testFile)) { + expect(stderr).not.toMatch(`PASS __tests__/${testFile}.test.js`); + } + } + }, + ); }); diff --git a/packages/jest-cli/src/args.ts b/packages/jest-cli/src/args.ts index f6993afcd784..0771bb0d79e1 100644 --- a/packages/jest-cli/src/args.ts +++ b/packages/jest-cli/src/args.ts @@ -48,6 +48,22 @@ export function check(argv: Config.Argv): true { ); } + if (argv.maxRelatedTestsDepth && !argv.findRelatedTests) { + throw new Error( + 'The --maxRelatedTestsDepth option requires --findRelatedTests is being used.', + ); + } + + if ( + Object.prototype.hasOwnProperty.call(argv, 'maxRelatedTestsDepth') && + typeof argv.maxRelatedTestsDepth !== 'number' + ) { + throw new Error( + 'The --maxRelatedTestsDepth option must be a number.\n' + + 'Example usage: jest --findRelatedTests --maxRelatedTestsDepth 2', + ); + } + if ( Object.prototype.hasOwnProperty.call(argv, 'maxWorkers') && argv.maxWorkers === undefined @@ -349,6 +365,12 @@ export const options: {[key: string]: Options} = { requiresArg: true, type: 'number', }, + maxRelatedTestsDepth: { + description: + 'Specifies the maximum depth for finding related tests. Requires ' + + '--findRelatedTests to be used. Helps limit the scope of related test detection.', + type: 'number', + }, maxWorkers: { alias: 'w', description: diff --git a/packages/jest-config/src/ValidConfig.ts b/packages/jest-config/src/ValidConfig.ts index 73a89e7333bf..d87f7f13e9bd 100644 --- a/packages/jest-config/src/ValidConfig.ts +++ b/packages/jest-config/src/ValidConfig.ts @@ -94,6 +94,7 @@ export const initialOptions: Config.InitialOptions = { listTests: false, logHeapUsage: true, maxConcurrency: 5, + maxRelatedTestsDepth: Infinity, maxWorkers: '50%', moduleDirectories: ['node_modules'], moduleFileExtensions: [ diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index 8523938629ca..fa9a57b0d982 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -105,6 +105,7 @@ const groupOptions = ( listTests: options.listTests, logHeapUsage: options.logHeapUsage, maxConcurrency: options.maxConcurrency, + maxRelatedTestsDepth: options.maxRelatedTestsDepth, maxWorkers: options.maxWorkers, noSCM: undefined, noStackTrace: options.noStackTrace, diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index a0724cc09754..ae58084d7c07 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -900,6 +900,7 @@ export default async function normalize( case 'globals': case 'fakeTimers': case 'findRelatedTests': + case 'maxRelatedTestsDepth': case 'forceCoverageMatch': case 'forceExit': case 'injectGlobals': diff --git a/packages/jest-core/src/SearchSource.ts b/packages/jest-core/src/SearchSource.ts index 772feeff6b08..4b9a249ad0fe 100644 --- a/packages/jest-core/src/SearchSource.ts +++ b/packages/jest-core/src/SearchSource.ts @@ -178,6 +178,7 @@ export default class SearchSource { async findRelatedTests( allPaths: Set, collectCoverage: boolean, + maxDepth: number, ): Promise { const dependencyResolver = await this._getOrBuildDependencyResolver(); @@ -188,7 +189,10 @@ export default class SearchSource { dependencyResolver.resolveInverse( allPaths, this.isTestFilePath.bind(this), - {skipNodeResolution: this._context.config.skipNodeResolution}, + { + maxDepth, + skipNodeResolution: this._context.config.skipNodeResolution, + }, ), ), }; @@ -197,7 +201,10 @@ export default class SearchSource { const testModulesMap = dependencyResolver.resolveInverseModuleMap( allPaths, this.isTestFilePath.bind(this), - {skipNodeResolution: this._context.config.skipNodeResolution}, + { + maxDepth, + skipNodeResolution: this._context.config.skipNodeResolution, + }, ); const allPathsAbsolute = new Set([...allPaths].map(p => path.resolve(p))); @@ -246,12 +253,17 @@ export default class SearchSource { async findRelatedTestsFromPattern( paths: Array, collectCoverage: boolean, + maxDepth: number, ): Promise { if (Array.isArray(paths) && paths.length > 0) { const resolvedPaths = paths.map(p => path.resolve(this._context.config.cwd, p), ); - return this.findRelatedTests(new Set(resolvedPaths), collectCoverage); + return this.findRelatedTests( + new Set(resolvedPaths), + collectCoverage, + maxDepth, + ); } return {tests: []}; } @@ -259,12 +271,13 @@ export default class SearchSource { async findTestRelatedToChangedFiles( changedFilesInfo: ChangedFiles, collectCoverage: boolean, + maxDepth: number, ): Promise { if (!hasSCM(changedFilesInfo)) { return {noSCM: true, tests: []}; } const {changedFiles} = changedFilesInfo; - return this.findRelatedTests(changedFiles, collectCoverage); + return this.findRelatedTests(changedFiles, collectCoverage, maxDepth); } private async _getTestPaths( @@ -280,6 +293,7 @@ export default class SearchSource { return this.findTestRelatedToChangedFiles( changedFiles, globalConfig.collectCoverage, + globalConfig.maxRelatedTestsDepth, ); } @@ -295,6 +309,7 @@ export default class SearchSource { return this.findRelatedTestsFromPattern( paths, globalConfig.collectCoverage, + globalConfig.maxRelatedTestsDepth, ); } else { return this.findMatchingTests( 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 ebe9a4456480..d959b4cf1e66 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 @@ -102,6 +102,7 @@ exports[`prints the config object 1`] = ` "listTests": false, "logHeapUsage": false, "maxConcurrency": 5, + "maxRelatedTestsDepth": Infinity, "maxWorkers": 2, "noStackTrace": false, "nonFlagArgs": [], diff --git a/packages/jest-core/src/lib/updateGlobalConfig.ts b/packages/jest-core/src/lib/updateGlobalConfig.ts index 115e53142cfd..3faddccc8f40 100644 --- a/packages/jest-core/src/lib/updateGlobalConfig.ts +++ b/packages/jest-core/src/lib/updateGlobalConfig.ts @@ -70,6 +70,10 @@ export default function updateGlobalConfig( newConfig.findRelatedTests = options.findRelatedTests; } + if (options.maxRelatedTestsDepth !== undefined) { + newConfig.maxRelatedTestsDepth = options.maxRelatedTestsDepth; + } + if (options.nonFlagArgs !== undefined) { newConfig.nonFlagArgs = options.nonFlagArgs; } diff --git a/packages/jest-core/src/watch.ts b/packages/jest-core/src/watch.ts index 2f173263f516..0c93f17e97c3 100644 --- a/packages/jest-core/src/watch.ts +++ b/packages/jest-core/src/watch.ts @@ -115,6 +115,7 @@ export default async function watch( coverageDirectory, coverageReporters, findRelatedTests, + maxRelatedTestsDepth, mode, nonFlagArgs, notify, @@ -135,6 +136,7 @@ export default async function watch( coverageDirectory, coverageReporters, findRelatedTests, + maxRelatedTestsDepth, mode, nonFlagArgs, notify, diff --git a/packages/jest-resolve-dependencies/src/index.ts b/packages/jest-resolve-dependencies/src/index.ts index 2057367291a1..8e547ca3cfe1 100644 --- a/packages/jest-resolve-dependencies/src/index.ts +++ b/packages/jest-resolve-dependencies/src/index.ts @@ -112,7 +112,10 @@ export class DependencyResolver { ) => { const visitedModules = new Set(); const result: Array = []; - while (changed.size > 0) { + let depth = 0; + const maxDepth = options?.maxDepth ?? Infinity; + + while (changed.size > 0 && depth < maxDepth) { changed = new Set( moduleMap.reduce>((acc, module) => { if ( @@ -132,6 +135,7 @@ export class DependencyResolver { return acc; }, []), ); + depth++; } return [ ...result, @@ -159,6 +163,7 @@ export class DependencyResolver { file, }); } + return collectModules(relatedPaths, modules, changed); } diff --git a/packages/jest-resolve/src/resolver.ts b/packages/jest-resolve/src/resolver.ts index 32bb4d8cd9f9..70f0e413587e 100644 --- a/packages/jest-resolve/src/resolver.ts +++ b/packages/jest-resolve/src/resolver.ts @@ -37,6 +37,7 @@ export type ResolveModuleConfig = { conditions?: Array; skipNodeResolution?: boolean; paths?: Array; + maxDepth?: number; }; const NATIVE_PLATFORM = 'native'; diff --git a/packages/jest-schemas/src/raw-types.ts b/packages/jest-schemas/src/raw-types.ts index 34b9af7a873d..429c7c3647b1 100644 --- a/packages/jest-schemas/src/raw-types.ts +++ b/packages/jest-schemas/src/raw-types.ts @@ -251,6 +251,7 @@ export const InitialOptions = Type.Partial( fakeTimers: FakeTimers, filter: Type.String(), findRelatedTests: Type.Boolean(), + maxRelatedTestsDepth: Type.Number(), forceCoverageMatch: Type.Array(Type.String()), forceExit: Type.Boolean(), json: Type.Boolean(), diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 154e4bfc1043..263584afca4e 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -173,6 +173,7 @@ export type DefaultOptions = { injectGlobals: boolean; listTests: boolean; maxConcurrency: number; + maxRelatedTestsDepth: number; maxWorkers: number | string; moduleDirectories: Array; moduleFileExtensions: Array; @@ -271,6 +272,7 @@ export type GlobalConfig = { expand: boolean; filter?: string; findRelatedTests: boolean; + maxRelatedTestsDepth: number; forceExit: boolean; json: boolean; globalSetup?: string; @@ -424,6 +426,7 @@ export type Argv = Arguments< env: string; expand: boolean; findRelatedTests: boolean; + maxRelatedTestsDepth: number; forceExit: boolean; globals: string; globalSetup: string | null | undefined; diff --git a/packages/jest-watcher/src/types.ts b/packages/jest-watcher/src/types.ts index 3096326bc497..423e3abe81b2 100644 --- a/packages/jest-watcher/src/types.ts +++ b/packages/jest-watcher/src/types.ts @@ -57,6 +57,7 @@ export type AllowedConfigOptions = Partial< | 'coverageDirectory' | 'coverageReporters' | 'findRelatedTests' + | 'maxRelatedTestsDepth' | 'nonFlagArgs' | 'notify' | 'notifyMode' diff --git a/packages/test-utils/src/config.ts b/packages/test-utils/src/config.ts index 868c3cfce9f8..030aea45519b 100644 --- a/packages/test-utils/src/config.ts +++ b/packages/test-utils/src/config.ts @@ -33,6 +33,7 @@ const DEFAULT_GLOBAL_CONFIG: Config.GlobalConfig = { listTests: false, logHeapUsage: false, maxConcurrency: 5, + maxRelatedTestsDepth: Infinity, maxWorkers: 2, noSCM: undefined, noStackTrace: false,