diff --git a/CHANGELOG.md b/CHANGELOG.md index efa038f928b6..88384e57d694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - `[jest-config]` [**BREAKING**] Rename `extraGlobals` to `sandboxInjectedGlobals` ([#10817](https://github.com/facebook/jest/pull/10817)) - `[jest-config]` [**BREAKING**] Throw an error instead of showing a warning if multiple configs are used ([#12510](https://github.com/facebook/jest/pull/12510)) - `[jest-core]` Pass project config to `globalSetup`/`globalTeardown` function as second argument ([#12440](https://github.com/facebook/jest/pull/12440)) +- `[jest-cli, jest-core]` Add `--ignoreProjects` CLI argument to ignore test suites by project name ([#12620](https://github.com/facebook/jest/pull/12620)) - `[jest-environment-jsdom]` [**BREAKING**] Upgrade jsdom to 19.0.0 ([#12290](https://github.com/facebook/jest/pull/12290)) - `[jest-environment-jsdom]` [**BREAKING**] Add default `browser` condition to `exportConditions` for `jsdom` environment ([#11924](https://github.com/facebook/jest/pull/11924)) - `[jest-environment-jsdom]` [**BREAKING**] Pass global config to Jest environment constructor for `jsdom` environment ([#12461](https://github.com/facebook/jest/pull/12461)) diff --git a/docs/CLI.md b/docs/CLI.md index 7263a4676883..b2f7a8297a2d 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -224,6 +224,10 @@ This feature is an escape-hatch. If Jest doesn't exit at the end of a test run, Show the help information, similar to this page. +### `--ignoreProjects ... ` + +Ignore the tests of the specified projects. Jest uses the attribute `displayName` in the configuration to identify each project. If you use this option, you should provide a `displayName` to all your projects. + ### `--init` Generate a basic configuration file. Based on your project, Jest will ask you a few questions that will help to generate a `jest.config.js` file with a short description for each option. @@ -332,7 +336,7 @@ The default regex matching works fine on small runs, but becomes slow if provide ### `--selectProjects ... ` -Run only the tests of the specified projects. Jest uses the attribute `displayName` in the configuration to identify each project. If you use this option, you should provide a `displayName` to all your projects. +Run the tests of the specified projects. Jest uses the attribute `displayName` in the configuration to identify each project. If you use this option, you should provide a `displayName` to all your projects. ### `--setupFilesAfterEnv ... ` diff --git a/e2e/__tests__/selectProjects.test.ts b/e2e/__tests__/selectProjects.test.ts index 2e48085b6cf1..2928fc8c2b18 100644 --- a/e2e/__tests__/selectProjects.test.ts +++ b/e2e/__tests__/selectProjects.test.ts @@ -111,6 +111,131 @@ describe('Given a config with two named projects, first-project and second-proje ); }); }); + + describe('when Jest is started with `--ignoreProjects first-project', () => { + let result: RunJestJsonResult; + beforeAll(() => { + result = runWithJson('select-projects', [ + '--ignoreProjects', + 'first-project', + ]); + }); + it('runs the tests in the second project only', () => { + expect(result.json).toHaveProperty('success', true); + expect(result.json).toHaveProperty('numTotalTests', 1); + expect(result.json.testResults.map(({name}) => name)).toEqual([ + resolve(dir, '__tests__/second-project.test.js'), + ]); + }); + it('prints that only second-project will run', () => { + expect(result.stderr).toMatch(/^Running one project: second-project/); + }); + }); + + describe('when Jest is started with `--ignoreProjects second-project', () => { + let result: RunJestJsonResult; + beforeAll(() => { + result = runWithJson('select-projects', [ + '--ignoreProjects', + 'second-project', + ]); + }); + it('runs the tests in the first project only', () => { + expect(result.json).toHaveProperty('success', true); + expect(result.json).toHaveProperty('numTotalTests', 1); + expect(result.json.testResults.map(({name}) => name)).toEqual([ + resolve(dir, '__tests__/first-project.test.js'), + ]); + }); + it('prints that only first-project will run', () => { + expect(result.stderr).toMatch(/^Running one project: first-project/); + }); + }); + + describe('when Jest is started with `--ignoreProjects third-project`', () => { + let result: RunJestJsonResult; + beforeAll(() => { + result = runWithJson('select-projects', [ + '--ignoreProjects', + 'third-project', + ]); + }); + it('runs the tests in the first and second projects', () => { + expect(result.json).toHaveProperty('success', true); + expect(result.json).toHaveProperty('numTotalTests', 2); + expect(result.json.testResults.map(({name}) => name).sort()).toEqual([ + resolve(dir, '__tests__/first-project.test.js'), + resolve(dir, '__tests__/second-project.test.js'), + ]); + }); + it('prints that both first-project and second-project will run', () => { + expect(result.stderr).toMatch( + /^Running 2 projects:\n- first-project\n- second-project/, + ); + }); + }); + + describe('when Jest is started with `--ignoreProjects first-project second-project`', () => { + let result: RunJestResult; + beforeAll(() => { + result = run('select-projects', [ + '--ignoreProjects', + 'first-project', + 'second-project', + ]); + }); + it('fails', () => { + expect(result).toHaveProperty('failed', true); + }); + it.skip('prints that no project was found', () => { + expect(result.stdout).toMatch( + /^You provided values for --ignoreProjects, but no projects were found matching the selection/, + ); + }); + }); + + describe('when Jest is started with `--selectProjects first-project second-project --ignoreProjects first-project` ', () => { + let result: RunJestJsonResult; + beforeAll(() => { + result = runWithJson('select-projects', [ + '--selectProjects', + 'first-project', + 'second-project', + '--ignoreProjects', + 'first-project', + ]); + }); + it('runs the tests in the second project only', () => { + expect(result.json).toHaveProperty('success', true); + expect(result.json).toHaveProperty('numTotalTests', 1); + expect(result.json.testResults.map(({name}) => name)).toEqual([ + resolve(dir, '__tests__/second-project.test.js'), + ]); + }); + it('prints that only second-project will run', () => { + expect(result.stderr).toMatch(/^Running one project: second-project/); + }); + }); + + describe('when Jest is started with `--selectProjects first-project --ignoreProjects first-project` ', () => { + let result: RunJestResult; + beforeAll(() => { + result = run('select-projects', [ + '--selectProjects', + 'first-project', + '--ignoreProjects', + 'first-project', + ]); + }); + it('fails', () => { + expect(result).toHaveProperty('failed', true); + }); + it.skip('prints that no project was found', () => { + expect(result.stdout).toMatch( + /^You provided values for --selectProjects and --ignoreProjects, but no projects were found matching the selection./, + ); + }); + }); }); describe('Given a config with two projects, first-project and an unnamed project', () => { @@ -185,4 +310,32 @@ describe('Given a config with two projects, first-project and an unnamed project ); }); }); + + describe('when Jest is started with `--ignoreProjects first-project`', () => { + let result: RunJestJsonResult; + beforeAll(() => { + result = runWithJson('select-projects-missing-name', [ + '--ignoreProjects', + 'first-project', + ]); + }); + it('runs the tests in the second project only', () => { + expect(result.json.success).toBe(true); + expect(result.json.numTotalTests).toBe(1); + expect(result.json.testResults.map(({name}) => name)).toEqual([ + resolve(dir, '__tests__/second-project.test.js'), + ]); + }); + it('prints that a project does not have a name', () => { + expect(result.stderr).toMatch( + /^You provided values for --ignoreProjects but a project does not have a name/, + ); + }); + it('prints that only second-project will run', () => { + const stderrThirdLine = result.stderr.split('\n')[2]; + expect(stderrThirdLine).toMatch( + /^Running one project: /, + ); + }); + }); }); diff --git a/packages/jest-cli/src/__tests__/cli/args.test.ts b/packages/jest-cli/src/__tests__/cli/args.test.ts index f40f37aeb873..89593f889ebd 100644 --- a/packages/jest-cli/src/__tests__/cli/args.test.ts +++ b/packages/jest-cli/src/__tests__/cli/args.test.ts @@ -81,6 +81,12 @@ describe('check', () => { ); }); + it('raises an exception if ignoreProjects is not provided any project names', () => { + expect(() => check(argv({ignoreProjects: []}))).toThrow( + 'The --ignoreProjects option requires the name of at least one project to be specified.\n', + ); + }); + it('raises an exception if config is not a valid JSON string', () => { expect(() => check(argv({config: 'x:1'}))).toThrow( 'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .json', diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index 57c55a59f129..e2e0520f30a3 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import type {Options} from 'yargs'; import type {Config} from '@jest/types'; import {constants, isJSONString} from 'jest-config'; @@ -68,6 +69,13 @@ export function check(argv: Config.Argv): true { ); } + if (argv.ignoreProjects && argv.ignoreProjects.length === 0) { + throw new Error( + 'The --ignoreProjects option requires the name of at least one project to be specified.\n' + + 'Example usage: jest --ignoreProjects my-first-project my-second-project', + ); + } + if ( argv.config && !isJSONString(argv.config) && @@ -95,7 +103,7 @@ export const usage = export const docs = 'Documentation: https://jestjs.io/'; // The default values are all set in jest-config -export const options = { +export const options: {[key: string]: Options} = { all: { description: 'The opposite of `onlyChanged`. If `onlyChanged` is set by ' + @@ -301,6 +309,13 @@ export const options = { 'A JSON string with map of variables for the haste module system', type: 'string', }, + ignoreProjects: { + description: + 'Ignore the tests of the specified projects.' + + 'Jest uses the attribute `displayName` in the configuration to identify each project.', + string: true, + type: 'array', + }, init: { description: 'Generate a basic configuration file', type: 'boolean', @@ -502,7 +517,7 @@ export const options = { }, selectProjects: { description: - 'Run only the tests of the specified projects.' + + 'Run the tests of the specified projects.' + 'Jest uses the attribute `displayName` in the configuration to identify each project.', string: true, type: 'array', @@ -695,4 +710,4 @@ export const options = { '--no-watchman.', type: 'boolean', }, -} as const; +}; diff --git a/packages/jest-core/src/cli/index.ts b/packages/jest-core/src/cli/index.ts index 61bdb12d8d8f..b7727a263b93 100644 --- a/packages/jest-core/src/cli/index.ts +++ b/packages/jest-core/src/cli/index.ts @@ -71,17 +71,24 @@ export async function runCLI( exit(0); } - let configsOfProjectsToRun = configs; - if (argv.selectProjects) { - const namesMissingWarning = getProjectNamesMissingWarning(configs); + const configsOfProjectsToRun = getConfigsOfProjectsToRun(configs, { + ignoreProjects: argv.ignoreProjects, + selectProjects: argv.selectProjects, + }); + if (argv.selectProjects || argv.ignoreProjects) { + const namesMissingWarning = getProjectNamesMissingWarning(configs, { + ignoreProjects: argv.ignoreProjects, + selectProjects: argv.selectProjects, + }); if (namesMissingWarning) { outputStream.write(namesMissingWarning); } - configsOfProjectsToRun = getConfigsOfProjectsToRun( - argv.selectProjects, - configs, + outputStream.write( + getSelectProjectsMessage(configsOfProjectsToRun, { + ignoreProjects: argv.ignoreProjects, + selectProjects: argv.selectProjects, + }), ); - outputStream.write(getSelectProjectsMessage(configsOfProjectsToRun)); } await _run10000( diff --git a/packages/jest-core/src/getConfigsOfProjectsToRun.ts b/packages/jest-core/src/getConfigsOfProjectsToRun.ts index b8b8288ccc68..75542c3d2e7d 100644 --- a/packages/jest-core/src/getConfigsOfProjectsToRun.ts +++ b/packages/jest-core/src/getConfigsOfProjectsToRun.ts @@ -9,12 +9,38 @@ import type {Config} from '@jest/types'; import getProjectDisplayName from './getProjectDisplayName'; export default function getConfigsOfProjectsToRun( - namesOfProjectsToRun: Array, projectConfigs: Array, + opts: { + ignoreProjects: Array | undefined; + selectProjects: Array | undefined; + }, ): Array { - const setOfProjectsToRun = new Set(namesOfProjectsToRun); + const projectFilter = createProjectFilter(opts); return projectConfigs.filter(config => { const name = getProjectDisplayName(config); - return name && setOfProjectsToRun.has(name); + return projectFilter(name); }); } + +function createProjectFilter(opts: { + ignoreProjects: Array | undefined; + selectProjects: Array | undefined; +}) { + const {selectProjects, ignoreProjects} = opts; + + const always = () => true; + + const selected = selectProjects + ? (name: string | undefined) => name && selectProjects.includes(name) + : always; + + const notIgnore = ignoreProjects + ? (name: string | undefined) => !(name && ignoreProjects.includes(name)) + : always; + + function test(name: string | undefined) { + return selected(name) && notIgnore(name); + } + + return test; +} diff --git a/packages/jest-core/src/getProjectNamesMissingWarning.ts b/packages/jest-core/src/getProjectNamesMissingWarning.ts index 53f0543d1cc8..947955dfefcf 100644 --- a/packages/jest-core/src/getProjectNamesMissingWarning.ts +++ b/packages/jest-core/src/getProjectNamesMissingWarning.ts @@ -11,6 +11,10 @@ import getProjectDisplayName from './getProjectDisplayName'; export default function getProjectNamesMissingWarning( projectConfigs: Array, + opts: { + ignoreProjects: Array | undefined; + selectProjects: Array | undefined; + }, ): string | undefined { const numberOfProjectsWithoutAName = projectConfigs.filter( config => !getProjectDisplayName(config), @@ -18,8 +22,15 @@ export default function getProjectNamesMissingWarning( if (numberOfProjectsWithoutAName === 0) { return undefined; } + const args: Array = []; + if (opts.selectProjects) { + args.push('--selectProjects'); + } + if (opts.ignoreProjects) { + args.push('--ignoreProjects'); + } return chalk.yellow( - `You provided values for --selectProjects but ${ + `You provided values for ${args.join(' and ')} but ${ numberOfProjectsWithoutAName === 1 ? 'a project does not have a name' : `${numberOfProjectsWithoutAName} projects do not have a name` diff --git a/packages/jest-core/src/getSelectProjectsMessage.ts b/packages/jest-core/src/getSelectProjectsMessage.ts index 5d3fff3577b4..b8a3a9f40c88 100644 --- a/packages/jest-core/src/getSelectProjectsMessage.ts +++ b/packages/jest-core/src/getSelectProjectsMessage.ts @@ -11,24 +11,46 @@ import getProjectDisplayName from './getProjectDisplayName'; export default function getSelectProjectsMessage( projectConfigs: Array, + opts: { + ignoreProjects: Array | undefined; + selectProjects: Array | undefined; + }, ): string { if (projectConfigs.length === 0) { - return getNoSelectionWarning(); + return getNoSelectionWarning(opts); } return getProjectsRunningMessage(projectConfigs); } -function getNoSelectionWarning(): string { - return chalk.yellow( - 'You provided values for --selectProjects but no projects were found matching the selection.\n', - ); +function getNoSelectionWarning(opts: { + ignoreProjects: Array | undefined; + selectProjects: Array | undefined; +}): string { + if (opts.ignoreProjects && opts.selectProjects) { + return chalk.yellow( + 'You provided values for --selectProjects and --ignoreProjects, but no projects were found matching the selection.\n' + + 'Are you ignoring all the selected projects?\n', + ); + } else if (opts.ignoreProjects) { + return chalk.yellow( + 'You provided values for --ignoreProjects, but no projects were found matching the selection.\n' + + 'Are you ignoring all projects?\n', + ); + } else if (opts.selectProjects) { + return chalk.yellow( + 'You provided values for --selectProjects but no projects were found matching the selection.\n', + ); + } else { + return chalk.yellow('No projects were found.\n'); + } } function getProjectsRunningMessage( projectConfigs: Array, ): string { if (projectConfigs.length === 1) { - const name = getProjectDisplayName(projectConfigs[0]); + const name = + getProjectDisplayName(projectConfigs[0]) ?? ''; return `Running one project: ${chalk.bold(name)}\n`; } const projectsList = projectConfigs diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 80243eb45512..d93d77c87b69 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -440,6 +440,7 @@ export type Argv = Arguments< globalSetup: string | null | undefined; globalTeardown: string | null | undefined; haste: string; + ignoreProjects: Array; init: boolean; injectGlobals: boolean; json: boolean;