diff --git a/CHANGELOG.md b/CHANGELOG.md index 9593cce495e4..b05ac8075472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - `[jest-haste-map]` [**BREAKING**] Remove name from hash in `HasteMap.getCacheFilePath` ([#7218](https://github.com/facebook/jest/pull/7218)) - `[babel-preset-jest]` [**BREAKING**] Export a function instead of an object for Babel 7 compatibility ([#7203](https://github.com/facebook/jest/pull/7203)) - `[jest-haste-map]` [**BREAKING**] Expose relative paths when getting the file iterator ([#7321](https://github.com/facebook/jest/pull/7321)) +- `[jest-cli]` Add Support for `globalSetup` and `globalTeardown` in projects ([#6865](https://github.com/facebook/jest/pull/6865)) - `[jest-runtime]` Add `extraGlobals` to config to load extra global variables into the execution vm ([#7454](https://github.com/facebook/jest/pull/7454)) - `[jest-util]` Export `specialChars` containing Unicode characters and ANSI escapes for console output ([#7532](https://github.com/facebook/jest/pull/7532)) - `[jest-config]` Handle typescript (`ts` and `tsx`) by default ([#7533](https://github.com/facebook/jest/pull/7533)) diff --git a/TestUtils.js b/TestUtils.js index 8023e5189826..8711d432eefa 100644 --- a/TestUtils.js +++ b/TestUtils.js @@ -81,6 +81,8 @@ const DEFAULT_PROJECT_CONFIG: ProjectConfig = { extraGlobals: [], filter: null, forceCoverageMatch: [], + globalSetup: null, + globalTeardown: null, globals: {}, haste: { providesModuleNodeModules: [], diff --git a/docs/Configuration.md b/docs/Configuration.md index 786319d019e0..a31b5be57dc1 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -350,6 +350,8 @@ Default: `undefined` This option allows the use of a custom global setup module which exports an async function that is triggered once before all test suites. This function gets Jest's `globalConfig` object as a parameter. +_Note: A global setup module configured in a project (using multi-project runner) will be triggered only when you run at least one test from this project._ + _Note: Any global variables that are defined through `globalSetup` can only be read in `globalTeardown`. You cannot retrieve globals defined here in your test suites._ Example: @@ -376,6 +378,8 @@ Default: `undefined` This option allows the use of a custom global teardown module which exports an async function that is triggered once after all test suites. This function gets Jest's `globalConfig` object as a parameter. +_Note: A global teardown module configured in a project (using multi-project runner) will be triggered only when you run at least one test from this project._ + ### `moduleDirectories` [array] Default: `["node_modules"]` diff --git a/e2e/__tests__/__snapshots__/show_config.test.js.snap b/e2e/__tests__/__snapshots__/show_config.test.js.snap index 3464293d2b45..b7b8f2690bce 100644 --- a/e2e/__tests__/__snapshots__/show_config.test.js.snap +++ b/e2e/__tests__/__snapshots__/show_config.test.js.snap @@ -19,6 +19,8 @@ exports[`--showConfig outputs config info and exits 1`] = ` \\"errorOnDeprecated\\": false, \\"filter\\": null, \\"forceCoverageMatch\\": [], + \\"globalSetup\\": null, + \\"globalTeardown\\": null, \\"globals\\": {}, \\"haste\\": { \\"computeSha1\\": false, diff --git a/e2e/__tests__/global_setup.test.js b/e2e/__tests__/global_setup.test.js index be0d626cc350..9842d842569a 100644 --- a/e2e/__tests__/global_setup.test.js +++ b/e2e/__tests__/global_setup.test.js @@ -14,13 +14,27 @@ import runJest, {json as runWithJson} from '../runJest'; import {cleanup} from '../Utils'; const DIR = path.join(os.tmpdir(), 'jest-global-setup'); +const project1DIR = path.join(os.tmpdir(), 'jest-global-setup-project-1'); +const project2DIR = path.join(os.tmpdir(), 'jest-global-setup-project-2'); -beforeEach(() => cleanup(DIR)); -afterAll(() => cleanup(DIR)); +beforeEach(() => { + cleanup(DIR); + cleanup(project1DIR); + cleanup(project2DIR); +}); +afterAll(() => { + cleanup(DIR); + cleanup(project1DIR); + cleanup(project2DIR); +}); test('globalSetup is triggered once before all test suites', () => { const setupPath = path.resolve(__dirname, '../global-setup/setup.js'); - const result = runWithJson('global-setup', [`--globalSetup=${setupPath}`]); + const result = runWithJson('global-setup', [ + `--globalSetup=${setupPath}`, + `--testPathPattern=__tests__`, + ]); + expect(result.status).toBe(0); const files = fs.readdirSync(DIR); expect(files).toHaveLength(1); @@ -32,6 +46,7 @@ test('jest throws an error when globalSetup does not export a function', () => { const setupPath = path.resolve(__dirname, '../global-setup/invalid_setup.js'); const {status, stderr} = runJest('global-setup', [ `--globalSetup=${setupPath}`, + `--testPathPattern=__tests__`, ]); expect(status).toBe(1); @@ -55,3 +70,36 @@ test('globalSetup function gets jest config object as a parameter', () => { expect(result.stdout).toBe(testPathPattern); }); + +test('should call globalSetup function of multiple projects', () => { + const configPath = path.resolve( + __dirname, + '../global-setup/projects.jest.config.js', + ); + + const result = runWithJson('global-setup', [`--config=${configPath}`]); + + expect(result.status).toBe(0); + + expect(fs.existsSync(DIR)).toBe(true); + expect(fs.existsSync(project1DIR)).toBe(true); + expect(fs.existsSync(project2DIR)).toBe(true); +}); + +test('should not call a globalSetup of a project if there are no tests to run from this project', () => { + const configPath = path.resolve( + __dirname, + '../global-setup/projects.jest.config.js', + ); + + const result = runWithJson('global-setup', [ + `--config=${configPath}`, + '--testPathPattern=project-1', + ]); + + expect(result.status).toBe(0); + + expect(fs.existsSync(DIR)).toBe(true); + expect(fs.existsSync(project1DIR)).toBe(true); + expect(fs.existsSync(project2DIR)).toBe(false); +}); diff --git a/e2e/__tests__/global_teardown.test.js b/e2e/__tests__/global_teardown.test.js index 7408a1293155..ca138dee7075 100644 --- a/e2e/__tests__/global_teardown.test.js +++ b/e2e/__tests__/global_teardown.test.js @@ -15,9 +15,19 @@ import runJest, {json as runWithJson} from '../runJest'; import {cleanup} from '../Utils'; const DIR = path.join(os.tmpdir(), 'jest-global-teardown'); +const project1DIR = path.join(os.tmpdir(), 'jest-global-teardown-project-1'); +const project2DIR = path.join(os.tmpdir(), 'jest-global-teardown-project-2'); -beforeEach(() => cleanup(DIR)); -afterAll(() => cleanup(DIR)); +beforeEach(() => { + cleanup(DIR); + cleanup(project1DIR); + cleanup(project2DIR); +}); +afterAll(() => { + cleanup(DIR); + cleanup(project1DIR); + cleanup(project2DIR); +}); test('globalTeardown is triggered once after all test suites', () => { mkdirp.sync(DIR); @@ -27,7 +37,9 @@ test('globalTeardown is triggered once after all test suites', () => { ); const result = runWithJson('global-teardown', [ `--globalTeardown=${teardownPath}`, + `--testPathPattern=__tests__`, ]); + expect(result.status).toBe(0); const files = fs.readdirSync(DIR); expect(files).toHaveLength(1); @@ -42,6 +54,7 @@ test('jest throws an error when globalTeardown does not export a function', () = ); const {status, stderr} = runJest('global-teardown', [ `--globalTeardown=${teardownPath}`, + `--testPathPattern=__tests__`, ]); expect(status).toBe(1); @@ -65,3 +78,36 @@ test('globalTeardown function gets jest config object as a parameter', () => { expect(result.stdout).toBe(testPathPattern); }); + +test('should call globalTeardown function of multiple projects', () => { + const configPath = path.resolve( + __dirname, + '../global-teardown/projects.jest.config.js', + ); + + const result = runWithJson('global-teardown', [`--config=${configPath}`]); + + expect(result.status).toBe(0); + + expect(fs.existsSync(DIR)).toBe(true); + expect(fs.existsSync(project1DIR)).toBe(true); + expect(fs.existsSync(project2DIR)).toBe(true); +}); + +test('should not call a globalTeardown of a project if there are no tests to run from this project', () => { + const configPath = path.resolve( + __dirname, + '../global-teardown/projects.jest.config.js', + ); + + const result = runWithJson('global-teardown', [ + `--config=${configPath}`, + '--testPathPattern=project-1', + ]); + + expect(result.status).toBe(0); + + expect(fs.existsSync(DIR)).toBe(true); + expect(fs.existsSync(project1DIR)).toBe(true); + expect(fs.existsSync(project2DIR)).toBe(false); +}); diff --git a/e2e/global-setup/project-1/setup.js b/e2e/global-setup/project-1/setup.js new file mode 100644 index 000000000000..aa6897cf9645 --- /dev/null +++ b/e2e/global-setup/project-1/setup.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ +const crypto = require('crypto'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const os = require('os'); +const path = require('path'); + +const DIR = path.join(os.tmpdir(), 'jest-global-setup-project-1'); + +module.exports = function() { + return new Promise((resolve, reject) => { + mkdirp.sync(DIR); + const fileId = crypto.randomBytes(20).toString('hex'); + fs.writeFileSync(path.join(DIR, fileId), 'setup'); + resolve(); + }); +}; diff --git a/e2e/global-setup/project-1/setup.test.js b/e2e/global-setup/project-1/setup.test.js new file mode 100644 index 000000000000..aa8e94403a3f --- /dev/null +++ b/e2e/global-setup/project-1/setup.test.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const DIR = path.join(os.tmpdir(), 'jest-global-setup-project-1'); + +test('should exist setup file', () => { + const files = fs.readdirSync(DIR); + expect(files).toHaveLength(1); + const setup = fs.readFileSync(path.join(DIR, files[0]), 'utf8'); + expect(setup).toBe('setup'); +}); diff --git a/e2e/global-setup/project-2/setup.js b/e2e/global-setup/project-2/setup.js new file mode 100644 index 000000000000..e47105b98f13 --- /dev/null +++ b/e2e/global-setup/project-2/setup.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ +const crypto = require('crypto'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const os = require('os'); +const path = require('path'); + +const DIR = path.join(os.tmpdir(), 'jest-global-setup-project-2'); + +module.exports = function() { + return new Promise((resolve, reject) => { + mkdirp.sync(DIR); + const fileId = crypto.randomBytes(20).toString('hex'); + fs.writeFileSync(path.join(DIR, fileId), 'setup'); + resolve(); + }); +}; diff --git a/e2e/global-setup/project-2/setup.test.js b/e2e/global-setup/project-2/setup.test.js new file mode 100644 index 000000000000..082cba8cfa98 --- /dev/null +++ b/e2e/global-setup/project-2/setup.test.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const DIR = path.join(os.tmpdir(), 'jest-global-setup-project-2'); + +test('should exist setup file', () => { + const files = fs.readdirSync(DIR); + expect(files).toHaveLength(1); + const setup = fs.readFileSync(path.join(DIR, files[0]), 'utf8'); + expect(setup).toBe('setup'); +}); diff --git a/e2e/global-setup/projects.jest.config.js b/e2e/global-setup/projects.jest.config.js new file mode 100644 index 000000000000..5d05d3df0a7e --- /dev/null +++ b/e2e/global-setup/projects.jest.config.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ + +const path = require('path'); + +module.exports = { + globalSetup: '/setup.js', + projects: [ + { + displayName: 'project-1', + globalSetup: '/setup.js', + rootDir: path.resolve(__dirname, './project-1'), + testMatch: ['/**/*.test.js'], + }, + { + displayName: 'project-2', + globalSetup: '/setup.js', + rootDir: path.resolve(__dirname, './project-2'), + testMatch: ['/**/*.test.js'], + }, + ], +}; diff --git a/e2e/global-teardown/project-1/teardown.js b/e2e/global-teardown/project-1/teardown.js new file mode 100644 index 000000000000..7486fde66812 --- /dev/null +++ b/e2e/global-teardown/project-1/teardown.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ +const crypto = require('crypto'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const os = require('os'); +const path = require('path'); + +const DIR = path.join(os.tmpdir(), 'jest-global-teardown-project-1'); + +module.exports = function() { + return new Promise((resolve, reject) => { + mkdirp.sync(DIR); + const fileId = crypto.randomBytes(20).toString('hex'); + fs.writeFileSync(path.join(DIR, fileId), 'teardown'); + resolve(); + }); +}; diff --git a/e2e/global-teardown/project-1/teardown.test.js b/e2e/global-teardown/project-1/teardown.test.js new file mode 100644 index 000000000000..ac42013e1b05 --- /dev/null +++ b/e2e/global-teardown/project-1/teardown.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const DIR = path.join(os.tmpdir(), 'jest-global-teardown-project-1'); + +test('should not exist teardown file', () => { + expect(fs.existsSync(DIR)).toBe(false); +}); diff --git a/e2e/global-teardown/project-2/teardown.js b/e2e/global-teardown/project-2/teardown.js new file mode 100644 index 000000000000..10c44b943571 --- /dev/null +++ b/e2e/global-teardown/project-2/teardown.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ +const crypto = require('crypto'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const os = require('os'); +const path = require('path'); + +const DIR = path.join(os.tmpdir(), 'jest-global-teardown-project-2'); + +module.exports = function() { + return new Promise((resolve, reject) => { + mkdirp.sync(DIR); + const fileId = crypto.randomBytes(20).toString('hex'); + fs.writeFileSync(path.join(DIR, fileId), 'teardown'); + resolve(); + }); +}; diff --git a/e2e/global-teardown/project-2/teardown.test.js b/e2e/global-teardown/project-2/teardown.test.js new file mode 100644 index 000000000000..55577b686d78 --- /dev/null +++ b/e2e/global-teardown/project-2/teardown.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const DIR = path.join(os.tmpdir(), 'jest-global-teardown-project-2'); + +test('teardown file should not exist', () => { + expect(fs.existsSync(DIR)).toBe(false); +}); diff --git a/e2e/global-teardown/projects.jest.config.js b/e2e/global-teardown/projects.jest.config.js new file mode 100644 index 000000000000..9d14d87059fc --- /dev/null +++ b/e2e/global-teardown/projects.jest.config.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + */ + +const path = require('path'); + +module.exports = { + globalTeardown: '/teardown.js', + projects: [ + { + displayName: 'project-1', + globalTeardown: '/teardown.js', + rootDir: path.resolve(__dirname, './project-1'), + testMatch: ['/**/*.test.js'], + }, + { + displayName: 'project-2', + globalTeardown: '/teardown.js', + rootDir: path.resolve(__dirname, './project-2'), + testMatch: ['/**/*.test.js'], + }, + ], +}; diff --git a/jest.config.js b/jest.config.js index 9a7e9b0808ce..5a398f1cd095 100644 --- a/jest.config.js +++ b/jest.config.js @@ -32,6 +32,8 @@ module.exports = { '/node_modules/', '/examples/', '/e2e/.*/__tests__', + '/e2e/global-setup', + '/e2e/global-teardown', '\\.snap$', '/packages/.*/build', '/packages/.*/build-es5', diff --git a/packages/jest-cli/src/runGlobalHook.js b/packages/jest-cli/src/runGlobalHook.js new file mode 100644 index 000000000000..4fec38ce2a43 --- /dev/null +++ b/packages/jest-cli/src/runGlobalHook.js @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. 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. + * + * @flow + */ +import type {GlobalConfig} from 'types/Config'; +import type {Test} from 'types/TestRunner'; + +export default ({ + allTests, + globalConfig, + moduleName, +}: { + allTests: Array, + globalConfig: GlobalConfig, + moduleName: 'globalSetup' | 'globalTeardown', +}): Promise => { + const globalModulePaths = new Set( + allTests.map(test => test.context.config[moduleName]), + ); + + if (globalConfig[moduleName]) { + globalModulePaths.add(globalConfig[moduleName]); + } + + if (globalModulePaths.size > 0) { + return Promise.all( + Array.from(globalModulePaths).map(async modulePath => { + if (!modulePath) { + return null; + } + + // $FlowFixMe + const globalModule = require(modulePath); + + if (typeof globalModule !== 'function') { + throw new TypeError( + `${moduleName} file must export a function at ${modulePath}`, + ); + } + + return globalModule(globalConfig); + }), + ); + } + + return Promise.resolve(); +}; diff --git a/packages/jest-cli/src/runJest.js b/packages/jest-cli/src/runJest.js index 9eb7007a2518..18093943f075 100644 --- a/packages/jest-cli/src/runJest.js +++ b/packages/jest-cli/src/runJest.js @@ -21,6 +21,7 @@ import {Console, formatTestResults} from 'jest-util'; import exit from 'exit'; import fs from 'graceful-fs'; import getNoTestsFoundMessage from './getNoTestsFoundMessage'; +import runGlobalHook from './runGlobalHook'; import SearchSource from './SearchSource'; import TestScheduler from './TestScheduler'; import TestSequencer from './TestSequencer'; @@ -254,19 +255,8 @@ export default (async function runJest({ collectHandles = collectNodeHandles(); } - if (globalConfig.globalSetup) { - // $FlowFixMe - const globalSetup = require(globalConfig.globalSetup); - if (typeof globalSetup !== 'function') { - throw new TypeError( - `globalSetup file must export a function at ${ - globalConfig.globalSetup - }`, - ); - } + await runGlobalHook({allTests, globalConfig, moduleName: 'globalSetup'}); - await globalSetup(globalConfig); - } const results = await new TestScheduler( globalConfig, { @@ -277,19 +267,12 @@ export default (async function runJest({ sequencer.cacheResults(allTests, results); - if (globalConfig.globalTeardown) { - // $FlowFixMe - const globalTeardown = require(globalConfig.globalTeardown); - if (typeof globalTeardown !== 'function') { - throw new TypeError( - `globalTeardown file must export a function at ${ - globalConfig.globalTeardown - }`, - ); - } + await runGlobalHook({ + allTests, + globalConfig, + moduleName: 'globalTeardown', + }); - await globalTeardown(globalConfig); - } return processResults(results, { collectHandles, isJSON: globalConfig.json, diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index 118677010793..6991118a8190 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -169,6 +169,8 @@ const groupOptions = ( extraGlobals: options.extraGlobals, filter: options.filter, forceCoverageMatch: options.forceCoverageMatch, + globalSetup: options.globalSetup, + globalTeardown: options.globalTeardown, globals: options.globals, haste: options.haste, moduleDirectories: options.moduleDirectories, diff --git a/types/Config.js b/types/Config.js index d301dcf69b38..a491d5206f00 100644 --- a/types/Config.js +++ b/types/Config.js @@ -262,6 +262,8 @@ export type ProjectConfig = {| extraGlobals: Array, filter: ?Path, forceCoverageMatch: Array, + globalSetup: ?string, + globalTeardown: ?string, globals: ConfigGlobals, haste: HasteConfig, moduleDirectories: Array,