Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --ignoreProjects CLI argument to ignore test suites by project name #12620

Merged
merged 13 commits into from Apr 4, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
6 changes: 5 additions & 1 deletion docs/CLI.md
Expand Up @@ -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 <project1> ... <projectN>`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if both this and selectProjects is used? Ideally it should be a configuration error. (and be mentioned in the docs)

Copy link
Contributor Author

@F3n67u F3n67u Apr 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectProjects and ignoreProjects could be used together. now it seems no need to do it because we don't support glob pattern, but I think to allow them to be used together will be better (not include a breaking change) because we maybe support glob in both selectProjects and ignoreProjects. just as Typescript allows you to use include and exclude together.

and I don't think there is any reason to restrict our user to use them together, If they like to, we will allow them to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see how our user's opinion here: #12618 (comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@F3n67u Glob support for both would be awesome as well, especially if you're using https://github.com/jest-community/jest-runner-eslint so you could do ignore *-test when running tests and ignore *-lint when running lint.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind opening up a separate feature request?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!


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.
Expand Down Expand Up @@ -332,7 +336,7 @@ The default regex matching works fine on small runs, but becomes slow if provide

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

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 <path1> ... <pathN>`

Expand Down
153 changes: 153 additions & 0 deletions e2e/__tests__/selectProjects.test.ts
Expand Up @@ -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` ', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimenB here is the test for using --selectProjects and --ignoreProjects together.

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', () => {
Expand Down Expand Up @@ -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: <unnamed project>/,
);
});
});
});
6 changes: 6 additions & 0 deletions packages/jest-cli/src/__tests__/cli/args.test.ts
Expand Up @@ -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',
Expand Down
21 changes: 18 additions & 3 deletions packages/jest-cli/src/cli/args.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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) &&
Expand Down Expand Up @@ -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 ' +
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -695,4 +710,4 @@ export const options = {
'--no-watchman.',
type: 'boolean',
},
} as const;
};
21 changes: 14 additions & 7 deletions packages/jest-core/src/cli/index.ts
Expand Up @@ -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(
Expand Down
32 changes: 29 additions & 3 deletions packages/jest-core/src/getConfigsOfProjectsToRun.ts
Expand Up @@ -9,12 +9,38 @@ import type {Config} from '@jest/types';
import getProjectDisplayName from './getProjectDisplayName';

export default function getConfigsOfProjectsToRun(
namesOfProjectsToRun: Array<string>,
projectConfigs: Array<Config.ProjectConfig>,
opts: {
ignoreProjects: Array<string> | undefined;
selectProjects: Array<string> | undefined;
},
): Array<Config.ProjectConfig> {
const setOfProjectsToRun = new Set<string>(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<string> | undefined;
selectProjects: Array<string> | 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;
}
13 changes: 12 additions & 1 deletion packages/jest-core/src/getProjectNamesMissingWarning.ts
Expand Up @@ -11,15 +11,26 @@ import getProjectDisplayName from './getProjectDisplayName';

export default function getProjectNamesMissingWarning(
projectConfigs: Array<Config.ProjectConfig>,
opts: {
ignoreProjects: Array<string> | undefined;
selectProjects: Array<string> | undefined;
},
): string | undefined {
const numberOfProjectsWithoutAName = projectConfigs.filter(
config => !getProjectDisplayName(config),
).length;
if (numberOfProjectsWithoutAName === 0) {
return undefined;
}
const args: Array<string> = [];
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`
Expand Down