Skip to content

Commit

Permalink
feat: CLI argument to filter tests by projects (#8612)
Browse files Browse the repository at this point in the history
  • Loading branch information
yacinehmito committed May 10, 2020
1 parent c6e63f3 commit 0e0eeed
Show file tree
Hide file tree
Showing 18 changed files with 438 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
### Features

- `[jest-config]` Support config files exporting (`async`) `function`s ([#10001](https://github.com/facebook/jest/pull/10001))
- `[jest-cli, jest-core]` Add `--selectProjects` CLI argument to filter test suites by project name ([#8612](https://github.com/facebook/jest/pull/8612))

### Fixes

Expand Down
4 changes: 4 additions & 0 deletions docs/CLI.md
Expand Up @@ -254,6 +254,10 @@ Run tests with specified reporters. [Reporter options](configuration#reporters-a

Alias: `-i`. Run all tests serially in the current process, rather than creating a worker pool of child processes that run tests. This can be useful for debugging.

### `--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.

### `--runTestsByPath`

Run only the tests that were specified with their exact paths.
Expand Down
189 changes: 189 additions & 0 deletions e2e/__tests__/selectProjects.test.ts
@@ -0,0 +1,189 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {resolve} from 'path';

import run, {
RunJestJsonResult,
RunJestResult,
json as runWithJson,
} from '../runJest';

describe('Given a config with two named projects, first-project and second-project', () => {
const dir = resolve(__dirname, '..', 'select-projects');

describe('when Jest is started with `--selectProjects first-project`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects', [
'--selectProjects',
'first-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 `--selectProjects second-project`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects', [
'--selectProjects',
'second-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 second-project`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects', [
'--selectProjects',
'first-project',
'second-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 without providing `--selectProjects`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects', []);
});
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('does not print which projects are run', () => {
expect(result.stderr).not.toMatch(/^Running/);
});
});

describe('when Jest is started with `--selectProjects third-project`', () => {
let result: RunJestResult;
beforeAll(() => {
result = run('select-projects', ['--selectProjects', 'third-project']);
});
it('fails', () => {
expect(result).toHaveProperty('failed', true);
});
it('prints that no project was found', () => {
expect(result.stdout).toMatch(
/^You provided values for --selectProjects but no projects were found matching the selection/,
);
});
});
});

describe('Given a config with two projects, first-project and an unnamed project', () => {
const dir = resolve(__dirname, '..', 'select-projects-missing-name');

describe('when Jest is started with `--selectProjects first-project`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects-missing-name', [
'--selectProjects',
'first-project',
]);
});
it('runs the tests in the first 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__/first-project.test.js'),
]);
});
it('prints that a project does not have a name', () => {
expect(result.stderr).toMatch(
/^You provided values for --selectProjects but a project does not have a name/,
);
});
it('prints that only first-project will run', () => {
const stderrThirdLine = result.stderr.split('\n')[2];
expect(stderrThirdLine).toMatch(/^Running one project: first-project/);
});
});

describe('when Jest is started without providing `--selectProjects`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects-missing-name', []);
});
it('runs the tests in the first and second projects', () => {
expect(result.json.success).toBe(true);
expect(result.json.numTotalTests).toBe(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('does not print that a project has no name', () => {
expect(result.stderr).not.toMatch(
/^You provided values for --selectProjects but a project does not have a name/,
);
});
});

describe('when Jest is started with `--selectProjects third-project`', () => {
let result: RunJestResult;
beforeAll(() => {
result = run('select-projects-missing-name', [
'--selectProjects',
'third-project',
]);
});
it('fails', () => {
expect(result).toHaveProperty('failed', true);
});
it('prints that a project does not have a name', () => {
expect(result.stdout).toMatch(
/^You provided values for --selectProjects but a project does not have a name/,
);
});
it('prints that no project was found', () => {
const stdoutThirdLine = result.stdout.split('\n')[2];
expect(stdoutThirdLine).toMatch(
/^You provided values for --selectProjects but no projects were found matching the selection/,
);
});
});
});
2 changes: 1 addition & 1 deletion e2e/runJest.ts
Expand Up @@ -95,7 +95,7 @@ function spawnJest(

export type RunJestResult = execa.ExecaReturnValue;

interface RunJestJsonResult extends RunJestResult {
export interface RunJestJsonResult extends RunJestResult {
json: FormattedTestResults;
}

Expand Down
10 changes: 10 additions & 0 deletions e2e/select-projects-missing-name/__tests__/first-project.test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

it('should run when first-project appears in selectProjects', () => {
expect(true).toBe(true);
});
10 changes: 10 additions & 0 deletions e2e/select-projects-missing-name/__tests__/second-project.test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

it('should run when second-project appears in selectProjects', () => {
expect(true).toBe(true);
});
20 changes: 20 additions & 0 deletions e2e/select-projects-missing-name/package.json
@@ -0,0 +1,20 @@
{
"description": "Testing the behaviour of --selectProjects when a project does not have a name",
"jest": {
"projects": [
{
"displayName": "first-project",
"testMatch": [
"<rootDir>/__tests__/first-project.test.js"
],
"testEnvironment": "node"
},
{
"testMatch": [
"<rootDir>/__tests__/second-project.test.js"
],
"testEnvironment": "node"
}
]
}
}
10 changes: 10 additions & 0 deletions e2e/select-projects/__tests__/first-project.test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

it('should run when first-project appears in selectProjects', () => {
expect(true).toBe(true);
});
10 changes: 10 additions & 0 deletions e2e/select-projects/__tests__/second-project.test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

it('should run when second-project appears in selectProjects', () => {
expect(true).toBe(true);
});
24 changes: 24 additions & 0 deletions e2e/select-projects/package.json
@@ -0,0 +1,24 @@
{
"description": "Testing the behaviour of --selectProjects",
"jest": {
"projects": [
{
"displayName": "first-project",
"testMatch": [
"<rootDir>/__tests__/first-project.test.js"
],
"testEnvironment": "node"
},
{
"displayName": {
"name": "second-project",
"color": "blue"
},
"testMatch": [
"<rootDir>/__tests__/second-project.test.js"
],
"testEnvironment": "node"
}
]
}
}
7 changes: 7 additions & 0 deletions packages/jest-cli/src/__tests__/cli/args.test.ts
Expand Up @@ -72,6 +72,13 @@ describe('check', () => {
},
);

it('raises an exception if selectProjects is not provided any project names', () => {
const argv: Config.Argv = {selectProjects: []} as Config.Argv;
expect(() => check(argv)).toThrow(
'The --selectProjects 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', () => {
const argv = {config: 'x:1'} as Config.Argv;
expect(() => check(argv)).toThrow(
Expand Down
14 changes: 14 additions & 0 deletions packages/jest-cli/src/cli/args.ts
Expand Up @@ -49,6 +49,13 @@ export function check(argv: Config.Argv): true {
);
}

if (argv.selectProjects && argv.selectProjects.length === 0) {
throw new Error(
'The --selectProjects option requires the name of at least one project to be specified.\n' +
'Example usage: jest --selectProjects my-first-project my-second-project',
);
}

if (
argv.config &&
!isJSONString(argv.config) &&
Expand Down Expand Up @@ -528,6 +535,13 @@ export const options = {
"Allows to use a custom runner instead of Jest's default test runner.",
type: 'string',
},
selectProjects: {
description:
'Run only the tests of the specified projects.' +
'Jest uses the attribute `displayName` in the configuration to identify each project.',
string: true,
type: 'array',
},
setupFiles: {
description:
'A list of paths to modules that run some code to configure or ' +
Expand Down
18 changes: 17 additions & 1 deletion packages/jest-core/src/cli/index.ts
Expand Up @@ -26,6 +26,9 @@ import TestWatcher from '../TestWatcher';
import watch from '../watch';
import pluralize from '../pluralize';
import logDebugMessages from '../lib/log_debug_messages';
import getConfigsOfProjectsToRun from '../getConfigsOfProjectsToRun';
import getProjectNamesMissingWarning from '../getProjectNamesMissingWarning';
import getSelectProjectsMessage from '../getSelectProjectsMessage';

const {print: preRunMessagePrint} = preRunMessage;

Expand Down Expand Up @@ -68,9 +71,22 @@ export async function runCLI(
exit(0);
}

let configsOfProjectsToRun = configs;
if (argv.selectProjects) {
const namesMissingWarning = getProjectNamesMissingWarning(configs);
if (namesMissingWarning) {
outputStream.write(namesMissingWarning);
}
configsOfProjectsToRun = getConfigsOfProjectsToRun(
argv.selectProjects,
configs,
);
outputStream.write(getSelectProjectsMessage(configsOfProjectsToRun));
}

await _run10000(
globalConfig,
configs,
configsOfProjectsToRun,
hasDeprecationWarnings,
outputStream,
r => (results = r),
Expand Down

0 comments on commit 0e0eeed

Please sign in to comment.