diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f526e2ace31..4fe79a7283a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ ([#5154](https://github.com/facebook/jest/pull/5154)) * `[jest-jasmine2]` Support generator functions as specs. ([#5166](https://github.com/facebook/jest/pull/5166)) +* `[jest-config]` Allow configuration objects inside `projects` array + ([#5176](https://github.com/facebook/jest/pull/5176)) ### Chore & Maintenance diff --git a/docs/Configuration.md b/docs/Configuration.md index ad7c3d76d79f..d1b77e5c4b46 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -427,7 +427,7 @@ Default: `undefined` A preset that is used as a base for Jest's configuration. A preset should point to an npm module that exports a `jest-preset.json` module on its top level. -### `projects` [array] +### `projects` [array] Default: `undefined` @@ -446,6 +446,27 @@ This example configuration will run Jest in the root directory as well as in every folder in the examples directory. You can have an unlimited amount of projects running in the same Jest instance. +The projects feature can also be used to run multiple configurations or multiple +[runners](#runner-string). For this purpose you can pass an array of +configuration objects. For example, to run both tests and ESLint (via +[jest-runner-eslint](https://github.com/jest-community/jest-runner-eslint)) in +the same invocation of Jest: + +```json +{ + "projects": [ + { + "displayName": "test" + }, + { + "displayName": "lint", + "runner": "jest-runner-eslint", + "testMatch": ["/**/*.js"] + } + ] +} +``` + ### `clearMocks` [boolean] Default: `false` @@ -619,6 +640,34 @@ _Note: By default, `roots` has a single entry `` but there are cases where you may want to have multiple roots within one project, for example `roots: ["/src/", "/tests/"]`._ +### `runner` [string] + +##### available in Jest **21.0.0+** + +Default: `"jest-runner"` + +This option allows you to use a custom runner instead of Jest's default test +runner. Examples of runners include: + +* [`jest-runner-eslint`](https://github.com/jest-community/jest-runner-eslint) +* [`jest-runner-mocha`](https://github.com/rogeliog/jest-runner-mocha) +* [`jest-runner-tsc`](https://github.com/azz/jest-runner-tsc) +* [`jest-runner-prettier`](https://github.com/keplersj/jest-runner-prettier) + +To write a test-runner, export a class with which accepts `globalConfig` in the +constructor, and has a `runTests` method with the signature: + +```ts +async runTests( + tests: Array, + watcher: TestWatcher, + onStart: OnTestStart, + onResult: OnTestSuccess, + onFailure: OnTestFailure, + options: TestRunnerOptions, +): Promise +``` + ### `setupFiles` [array] Default: `[]` diff --git a/integration_tests/__tests__/multi_project_runner.test.js b/integration_tests/__tests__/multi_project_runner.test.js index 3bbe28227842..a4ffc3f837aa 100644 --- a/integration_tests/__tests__/multi_project_runner.test.js +++ b/integration_tests/__tests__/multi_project_runner.test.js @@ -154,6 +154,52 @@ test('"No tests found" message for projects', () => { ); }); +test('objects in project configuration', () => { + writeFiles(DIR, { + '__tests__/file1.test.js': ` + test('foo', () => {}); + `, + '__tests__/file2.test.js': ` + test('bar', () => {}); + `, + 'jest.config.js': `module.exports = { + projects: [ + { testMatch: ['/__tests__/file1.test.js'] }, + { testMatch: ['/__tests__/file2.test.js'] }, + ] + };`, + 'package.json': '{}', + }); + + const {stdout, stderr, status} = runJest(DIR); + expect(stderr).toContain('Test Suites: 2 passed, 2 total'); + expect(stderr).toContain('PASS __tests__/file1.test.js'); + expect(stderr).toContain('PASS __tests__/file2.test.js'); + expect(stderr).toContain('Ran all test suites in 2 projects.'); + expect(stdout).toEqual(''); + expect(status).toEqual(0); +}); + +test('allows a single project', () => { + writeFiles(DIR, { + '__tests__/file1.test.js': ` + test('foo', () => {}); + `, + 'jest.config.js': `module.exports = { + projects: [ + { testMatch: ['/__tests__/file1.test.js'] }, + ] + };`, + 'package.json': '{}', + }); + + const {stdout, stderr, status} = runJest(DIR); + expect(stderr).toContain('PASS __tests__/file1.test.js'); + expect(stderr).toContain('Test Suites: 1 passed, 1 total'); + expect(stdout).toEqual(''); + expect(status).toEqual(0); +}); + test('resolves projects and their properly', () => { writeFiles(DIR, { '.watchmanconfig': '', diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index ea4755411219..6c7b3f45eb92 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -199,7 +199,9 @@ const ensureNoDuplicateConfigs = (parsedConfigs, projects) => { }); throw new Error(message); } - configPathSet.add(configPath); + if (configPath !== null) { + configPathSet.add(configPath); + } } }; @@ -225,9 +227,11 @@ const getConfigs = ( let hasDeprecationWarnings; let configs: Array = []; let projects = projectsFromCLIArgs; + let configPath: ?Path; if (projectsFromCLIArgs.length === 1) { const parsedConfig = readConfig(argv, projects[0]); + configPath = parsedConfig.configPath; if (parsedConfig.globalConfig.projects) { // If this was a single project, and its config has `projects` @@ -246,7 +250,9 @@ const getConfigs = ( } if (projects.length > 1) { - const parsedConfigs = projects.map(root => readConfig(argv, root, true)); + const parsedConfigs = projects.map(root => + readConfig(argv, root, true, configPath), + ); ensureNoDuplicateConfigs(parsedConfigs, projects); configs = parsedConfigs.map(({projectConfig}) => projectConfig); if (!hasDeprecationWarnings) { diff --git a/packages/jest-config/src/__tests__/read_config.test.js b/packages/jest-config/src/__tests__/read_config.test.js new file mode 100644 index 000000000000..e4d053ebad5e --- /dev/null +++ b/packages/jest-config/src/__tests__/read_config.test.js @@ -0,0 +1,14 @@ +import {readConfig} from '../index'; + +test('readConfig() throws when an object is passed without a file path', () => { + expect(() => { + readConfig( + null /* argv */, + {} /* packageRootOrConfig */, + false /* skipArgvConfigOption */, + null /* parentConfigPath */, + ); + }).toThrowError( + 'Jest: Cannot use configuration as an object without a file path', + ); +}); diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index 7cd8eccbfce5..739b6e69bbec 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -8,21 +8,31 @@ */ import type {Argv} from 'types/Argv'; -import type {GlobalConfig, Path, ProjectConfig} from 'types/Config'; +import type { + GlobalConfig, + InitialOptions, + Path, + ProjectConfig, +} from 'types/Config'; -import {getTestEnvironment, isJSONString} from './utils'; +import path from 'path'; +import {isJSONString} from './utils'; import normalize from './normalize'; import resolveConfigPath from './resolve_config_path'; import readConfigFileAndSetRootDir from './read_config_file_and_set_root_dir'; -function readConfig( +export {getTestEnvironment, isJSONString} from './utils'; +export {default as normalize} from './normalize'; + +export function readConfig( argv: Argv, - packageRoot: string, + packageRootOrConfig: Path | InitialOptions, // Whether it needs to look into `--config` arg passed to CLI. // It only used to read initial config. If the initial config contains // `project` property, we don't want to read `--config` value and rather // read individual configs for every project. skipArgvConfigOption?: boolean, + parentConfigPath: ?Path, ): { configPath: ?Path, globalConfig: GlobalConfig, @@ -30,11 +40,20 @@ function readConfig( projectConfig: ProjectConfig, } { let rawOptions; - let configPath; + let configPath = null; - // A JSON string was passed to `--config` argument and we can parse it - // and use as is. - if (isJSONString(argv.config)) { + if (typeof packageRootOrConfig !== 'string') { + if (parentConfigPath) { + rawOptions = packageRootOrConfig; + rawOptions.rootDir = path.dirname(parentConfigPath); + } else { + throw new Error( + 'Jest: Cannot use configuration as an object without a file path.', + ); + } + } else if (isJSONString(argv.config)) { + // A JSON string was passed to `--config` argument and we can parse it + // and use as is. let config; try { config = JSON.parse(argv.config); @@ -45,7 +64,7 @@ function readConfig( } // NOTE: we might need to resolve this dir to an absolute path in the future - config.rootDir = config.rootDir || packageRoot; + config.rootDir = config.rootDir || packageRootOrConfig; rawOptions = config; // A string passed to `--config`, which is either a direct path to the config // or a path to directory containing `package.json` or `jest.conf.js` @@ -54,7 +73,7 @@ function readConfig( rawOptions = readConfigFileAndSetRootDir(configPath); } else { // Otherwise just try to find config in the current rootDir. - configPath = resolveConfigPath(packageRoot, process.cwd()); + configPath = resolveConfigPath(packageRootOrConfig, process.cwd()); rawOptions = readConfigFileAndSetRootDir(configPath); } @@ -166,10 +185,3 @@ const getConfigs = ( }), }; }; - -module.exports = { - getTestEnvironment, - isJSONString, - normalize, - readConfig, -}; diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index e7fba4434f83..7fcec43e6fbc 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -437,7 +437,8 @@ export default function normalize(options: InitialOptions, argv: Argv) { // Project can be specified as globs. If a glob matches any files, // We expand it to these paths. If not, we keep the original path // for the future resolution. - const globMatches = glob.sync(project); + const globMatches = + typeof project === 'string' ? glob.sync(project) : []; return projects.concat(globMatches.length ? globMatches : project); }, []); break;