Skip to content

Commit eb2146f

Browse files
authored
Add --ignoreProjects CLI argument to ignore test suites by project name (#12620)
1 parent f88b7db commit eb2146f

File tree

10 files changed

+267
-21
lines changed

10 files changed

+267
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- `[jest-config]` [**BREAKING**] Rename `extraGlobals` to `sandboxInjectedGlobals` ([#10817](https://github.com/facebook/jest/pull/10817))
1515
- `[jest-config]` [**BREAKING**] Throw an error instead of showing a warning if multiple configs are used ([#12510](https://github.com/facebook/jest/pull/12510))
1616
- `[jest-core]` Pass project config to `globalSetup`/`globalTeardown` function as second argument ([#12440](https://github.com/facebook/jest/pull/12440))
17+
- `[jest-cli, jest-core]` Add `--ignoreProjects` CLI argument to ignore test suites by project name ([#12620](https://github.com/facebook/jest/pull/12620))
1718
- `[jest-environment-jsdom]` [**BREAKING**] Upgrade jsdom to 19.0.0 ([#12290](https://github.com/facebook/jest/pull/12290))
1819
- `[jest-environment-jsdom]` [**BREAKING**] Add default `browser` condition to `exportConditions` for `jsdom` environment ([#11924](https://github.com/facebook/jest/pull/11924))
1920
- `[jest-environment-jsdom]` [**BREAKING**] Pass global config to Jest environment constructor for `jsdom` environment ([#12461](https://github.com/facebook/jest/pull/12461))

docs/CLI.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ This feature is an escape-hatch. If Jest doesn't exit at the end of a test run,
224224

225225
Show the help information, similar to this page.
226226

227+
### `--ignoreProjects <project1> ... <projectN>`
228+
229+
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.
230+
227231
### `--init`
228232

229233
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
332336

333337
### `--selectProjects <project1> ... <projectN>`
334338

335-
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.
339+
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.
336340

337341
### `--setupFilesAfterEnv <path1> ... <pathN>`
338342

e2e/__tests__/selectProjects.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,131 @@ describe('Given a config with two named projects, first-project and second-proje
111111
);
112112
});
113113
});
114+
115+
describe('when Jest is started with `--ignoreProjects first-project', () => {
116+
let result: RunJestJsonResult;
117+
beforeAll(() => {
118+
result = runWithJson('select-projects', [
119+
'--ignoreProjects',
120+
'first-project',
121+
]);
122+
});
123+
it('runs the tests in the second project only', () => {
124+
expect(result.json).toHaveProperty('success', true);
125+
expect(result.json).toHaveProperty('numTotalTests', 1);
126+
expect(result.json.testResults.map(({name}) => name)).toEqual([
127+
resolve(dir, '__tests__/second-project.test.js'),
128+
]);
129+
});
130+
it('prints that only second-project will run', () => {
131+
expect(result.stderr).toMatch(/^Running one project: second-project/);
132+
});
133+
});
134+
135+
describe('when Jest is started with `--ignoreProjects second-project', () => {
136+
let result: RunJestJsonResult;
137+
beforeAll(() => {
138+
result = runWithJson('select-projects', [
139+
'--ignoreProjects',
140+
'second-project',
141+
]);
142+
});
143+
it('runs the tests in the first project only', () => {
144+
expect(result.json).toHaveProperty('success', true);
145+
expect(result.json).toHaveProperty('numTotalTests', 1);
146+
expect(result.json.testResults.map(({name}) => name)).toEqual([
147+
resolve(dir, '__tests__/first-project.test.js'),
148+
]);
149+
});
150+
it('prints that only first-project will run', () => {
151+
expect(result.stderr).toMatch(/^Running one project: first-project/);
152+
});
153+
});
154+
155+
describe('when Jest is started with `--ignoreProjects third-project`', () => {
156+
let result: RunJestJsonResult;
157+
beforeAll(() => {
158+
result = runWithJson('select-projects', [
159+
'--ignoreProjects',
160+
'third-project',
161+
]);
162+
});
163+
it('runs the tests in the first and second projects', () => {
164+
expect(result.json).toHaveProperty('success', true);
165+
expect(result.json).toHaveProperty('numTotalTests', 2);
166+
expect(result.json.testResults.map(({name}) => name).sort()).toEqual([
167+
resolve(dir, '__tests__/first-project.test.js'),
168+
resolve(dir, '__tests__/second-project.test.js'),
169+
]);
170+
});
171+
it('prints that both first-project and second-project will run', () => {
172+
expect(result.stderr).toMatch(
173+
/^Running 2 projects:\n- first-project\n- second-project/,
174+
);
175+
});
176+
});
177+
178+
describe('when Jest is started with `--ignoreProjects first-project second-project`', () => {
179+
let result: RunJestResult;
180+
beforeAll(() => {
181+
result = run('select-projects', [
182+
'--ignoreProjects',
183+
'first-project',
184+
'second-project',
185+
]);
186+
});
187+
it('fails', () => {
188+
expect(result).toHaveProperty('failed', true);
189+
});
190+
it.skip('prints that no project was found', () => {
191+
expect(result.stdout).toMatch(
192+
/^You provided values for --ignoreProjects, but no projects were found matching the selection/,
193+
);
194+
});
195+
});
196+
197+
describe('when Jest is started with `--selectProjects first-project second-project --ignoreProjects first-project` ', () => {
198+
let result: RunJestJsonResult;
199+
beforeAll(() => {
200+
result = runWithJson('select-projects', [
201+
'--selectProjects',
202+
'first-project',
203+
'second-project',
204+
'--ignoreProjects',
205+
'first-project',
206+
]);
207+
});
208+
it('runs the tests in the second project only', () => {
209+
expect(result.json).toHaveProperty('success', true);
210+
expect(result.json).toHaveProperty('numTotalTests', 1);
211+
expect(result.json.testResults.map(({name}) => name)).toEqual([
212+
resolve(dir, '__tests__/second-project.test.js'),
213+
]);
214+
});
215+
it('prints that only second-project will run', () => {
216+
expect(result.stderr).toMatch(/^Running one project: second-project/);
217+
});
218+
});
219+
220+
describe('when Jest is started with `--selectProjects first-project --ignoreProjects first-project` ', () => {
221+
let result: RunJestResult;
222+
beforeAll(() => {
223+
result = run('select-projects', [
224+
'--selectProjects',
225+
'first-project',
226+
'--ignoreProjects',
227+
'first-project',
228+
]);
229+
});
230+
it('fails', () => {
231+
expect(result).toHaveProperty('failed', true);
232+
});
233+
it.skip('prints that no project was found', () => {
234+
expect(result.stdout).toMatch(
235+
/^You provided values for --selectProjects and --ignoreProjects, but no projects were found matching the selection./,
236+
);
237+
});
238+
});
114239
});
115240

116241
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
185310
);
186311
});
187312
});
313+
314+
describe('when Jest is started with `--ignoreProjects first-project`', () => {
315+
let result: RunJestJsonResult;
316+
beforeAll(() => {
317+
result = runWithJson('select-projects-missing-name', [
318+
'--ignoreProjects',
319+
'first-project',
320+
]);
321+
});
322+
it('runs the tests in the second project only', () => {
323+
expect(result.json.success).toBe(true);
324+
expect(result.json.numTotalTests).toBe(1);
325+
expect(result.json.testResults.map(({name}) => name)).toEqual([
326+
resolve(dir, '__tests__/second-project.test.js'),
327+
]);
328+
});
329+
it('prints that a project does not have a name', () => {
330+
expect(result.stderr).toMatch(
331+
/^You provided values for --ignoreProjects but a project does not have a name/,
332+
);
333+
});
334+
it('prints that only second-project will run', () => {
335+
const stderrThirdLine = result.stderr.split('\n')[2];
336+
expect(stderrThirdLine).toMatch(
337+
/^Running one project: <unnamed project>/,
338+
);
339+
});
340+
});
188341
});

packages/jest-cli/src/__tests__/cli/args.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ describe('check', () => {
8181
);
8282
});
8383

84+
it('raises an exception if ignoreProjects is not provided any project names', () => {
85+
expect(() => check(argv({ignoreProjects: []}))).toThrow(
86+
'The --ignoreProjects option requires the name of at least one project to be specified.\n',
87+
);
88+
});
89+
8490
it('raises an exception if config is not a valid JSON string', () => {
8591
expect(() => check(argv({config: 'x:1'}))).toThrow(
8692
'The --config option requires a JSON string literal, or a file path with one of these extensions: .js, .ts, .mjs, .cjs, .json',

packages/jest-cli/src/cli/args.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import type {Options} from 'yargs';
89
import type {Config} from '@jest/types';
910
import {constants, isJSONString} from 'jest-config';
1011

@@ -68,6 +69,13 @@ export function check(argv: Config.Argv): true {
6869
);
6970
}
7071

72+
if (argv.ignoreProjects && argv.ignoreProjects.length === 0) {
73+
throw new Error(
74+
'The --ignoreProjects option requires the name of at least one project to be specified.\n' +
75+
'Example usage: jest --ignoreProjects my-first-project my-second-project',
76+
);
77+
}
78+
7179
if (
7280
argv.config &&
7381
!isJSONString(argv.config) &&
@@ -95,7 +103,7 @@ export const usage =
95103
export const docs = 'Documentation: https://jestjs.io/';
96104

97105
// The default values are all set in jest-config
98-
export const options = {
106+
export const options: {[key: string]: Options} = {
99107
all: {
100108
description:
101109
'The opposite of `onlyChanged`. If `onlyChanged` is set by ' +
@@ -301,6 +309,13 @@ export const options = {
301309
'A JSON string with map of variables for the haste module system',
302310
type: 'string',
303311
},
312+
ignoreProjects: {
313+
description:
314+
'Ignore the tests of the specified projects.' +
315+
'Jest uses the attribute `displayName` in the configuration to identify each project.',
316+
string: true,
317+
type: 'array',
318+
},
304319
init: {
305320
description: 'Generate a basic configuration file',
306321
type: 'boolean',
@@ -502,7 +517,7 @@ export const options = {
502517
},
503518
selectProjects: {
504519
description:
505-
'Run only the tests of the specified projects.' +
520+
'Run the tests of the specified projects.' +
506521
'Jest uses the attribute `displayName` in the configuration to identify each project.',
507522
string: true,
508523
type: 'array',
@@ -695,4 +710,4 @@ export const options = {
695710
'--no-watchman.',
696711
type: 'boolean',
697712
},
698-
} as const;
713+
};

packages/jest-core/src/cli/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,24 @@ export async function runCLI(
7171
exit(0);
7272
}
7373

74-
let configsOfProjectsToRun = configs;
75-
if (argv.selectProjects) {
76-
const namesMissingWarning = getProjectNamesMissingWarning(configs);
74+
const configsOfProjectsToRun = getConfigsOfProjectsToRun(configs, {
75+
ignoreProjects: argv.ignoreProjects,
76+
selectProjects: argv.selectProjects,
77+
});
78+
if (argv.selectProjects || argv.ignoreProjects) {
79+
const namesMissingWarning = getProjectNamesMissingWarning(configs, {
80+
ignoreProjects: argv.ignoreProjects,
81+
selectProjects: argv.selectProjects,
82+
});
7783
if (namesMissingWarning) {
7884
outputStream.write(namesMissingWarning);
7985
}
80-
configsOfProjectsToRun = getConfigsOfProjectsToRun(
81-
argv.selectProjects,
82-
configs,
86+
outputStream.write(
87+
getSelectProjectsMessage(configsOfProjectsToRun, {
88+
ignoreProjects: argv.ignoreProjects,
89+
selectProjects: argv.selectProjects,
90+
}),
8391
);
84-
outputStream.write(getSelectProjectsMessage(configsOfProjectsToRun));
8592
}
8693

8794
await _run10000(

packages/jest-core/src/getConfigsOfProjectsToRun.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,38 @@ import type {Config} from '@jest/types';
99
import getProjectDisplayName from './getProjectDisplayName';
1010

1111
export default function getConfigsOfProjectsToRun(
12-
namesOfProjectsToRun: Array<string>,
1312
projectConfigs: Array<Config.ProjectConfig>,
13+
opts: {
14+
ignoreProjects: Array<string> | undefined;
15+
selectProjects: Array<string> | undefined;
16+
},
1417
): Array<Config.ProjectConfig> {
15-
const setOfProjectsToRun = new Set<string>(namesOfProjectsToRun);
18+
const projectFilter = createProjectFilter(opts);
1619
return projectConfigs.filter(config => {
1720
const name = getProjectDisplayName(config);
18-
return name && setOfProjectsToRun.has(name);
21+
return projectFilter(name);
1922
});
2023
}
24+
25+
function createProjectFilter(opts: {
26+
ignoreProjects: Array<string> | undefined;
27+
selectProjects: Array<string> | undefined;
28+
}) {
29+
const {selectProjects, ignoreProjects} = opts;
30+
31+
const always = () => true;
32+
33+
const selected = selectProjects
34+
? (name: string | undefined) => name && selectProjects.includes(name)
35+
: always;
36+
37+
const notIgnore = ignoreProjects
38+
? (name: string | undefined) => !(name && ignoreProjects.includes(name))
39+
: always;
40+
41+
function test(name: string | undefined) {
42+
return selected(name) && notIgnore(name);
43+
}
44+
45+
return test;
46+
}

packages/jest-core/src/getProjectNamesMissingWarning.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,26 @@ import getProjectDisplayName from './getProjectDisplayName';
1111

1212
export default function getProjectNamesMissingWarning(
1313
projectConfigs: Array<Config.ProjectConfig>,
14+
opts: {
15+
ignoreProjects: Array<string> | undefined;
16+
selectProjects: Array<string> | undefined;
17+
},
1418
): string | undefined {
1519
const numberOfProjectsWithoutAName = projectConfigs.filter(
1620
config => !getProjectDisplayName(config),
1721
).length;
1822
if (numberOfProjectsWithoutAName === 0) {
1923
return undefined;
2024
}
25+
const args: Array<string> = [];
26+
if (opts.selectProjects) {
27+
args.push('--selectProjects');
28+
}
29+
if (opts.ignoreProjects) {
30+
args.push('--ignoreProjects');
31+
}
2132
return chalk.yellow(
22-
`You provided values for --selectProjects but ${
33+
`You provided values for ${args.join(' and ')} but ${
2334
numberOfProjectsWithoutAName === 1
2435
? 'a project does not have a name'
2536
: `${numberOfProjectsWithoutAName} projects do not have a name`

0 commit comments

Comments
 (0)