diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf53aac5ced..8951b25c58ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[jest-circus, jest-config, jest-runtime]` Add new `injectGlobals` config and CLI option to disable injecting global variables into the runtime ([#10484](https://github.com/facebook/jest/pull/10484)) + ### Fixes ### Chore & Maintenance diff --git a/docs/CLI.md b/docs/CLI.md index cb0c9c1b070e..099d31d15473 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -194,6 +194,22 @@ Show the help information, similar to this page. 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. +### `--injectGlobals` + +Insert Jest's globals (`expect`, `test`, `describe`, `beforeEach` etc.) into the global environment. If you set this to `false`, you should import from `@jest/globals`, e.g. + +```ts +import {expect, jest, test} from '@jest/globals'; + +jest.useFakeTimers(); + +test('some test', () => { + expect(Date.now()).toBe(0); +}); +``` + +_Note: This option is only supported using `jest-circus`._ + ### `--json` Prints the test results in JSON. This mode will send all other test output and user messages to stderr. diff --git a/docs/Configuration.md b/docs/Configuration.md index 0a43c9b9412f..c1fbe728e023 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -444,6 +444,24 @@ _Note: A global teardown module configured in a project (using multi-project run _Note: The same caveat concerning transformation of `node_modules` as for `globalSetup` applies to `globalTeardown`._ +### `injectGlobals` [boolean] + +Default: `true` + +Insert Jest's globals (`expect`, `test`, `describe`, `beforeEach` etc.) into the global environment. If you set this to `false`, you should import from `@jest/globals`, e.g. + +```ts +import {expect, jest, test} from '@jest/globals'; + +jest.useFakeTimers(); + +test('some test', () => { + expect(Date.now()).toBe(0); +}); +``` + +_Note: This option is only supported using `jest-circus`._ + ### `maxConcurrency` [number] Default: `5` diff --git a/e2e/__tests__/__snapshots__/injectGlobals.test.ts.snap b/e2e/__tests__/__snapshots__/injectGlobals.test.ts.snap new file mode 100644 index 000000000000..ca8a6fffa99b --- /dev/null +++ b/e2e/__tests__/__snapshots__/injectGlobals.test.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`globals are undefined if passed \`false\` from CLI 1`] = ` +PASS __tests__/test.js + ✓ no globals injected +`; + +exports[`globals are undefined if passed \`false\` from CLI 2`] = ` +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites. +`; + +exports[`globals are undefined if passed \`false\` from config 1`] = ` +PASS __tests__/test.js + ✓ no globals injected +`; + +exports[`globals are undefined if passed \`false\` from config 2`] = ` +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites. +`; diff --git a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap index 77708ecfc22e..61f2d402cf2d 100644 --- a/e2e/__tests__/__snapshots__/showConfig.test.ts.snap +++ b/e2e/__tests__/__snapshots__/showConfig.test.ts.snap @@ -22,6 +22,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` "computeSha1": false, "throwOnModuleCollision": false }, + "injectGlobals": true, "moduleDirectories": [ "node_modules" ], diff --git a/e2e/__tests__/injectGlobals.test.ts b/e2e/__tests__/injectGlobals.test.ts new file mode 100644 index 000000000000..95f625250aaa --- /dev/null +++ b/e2e/__tests__/injectGlobals.test.ts @@ -0,0 +1,57 @@ +/** + * 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 * as path from 'path'; +import {tmpdir} from 'os'; +import {wrap} from 'jest-snapshot-serializer-raw'; +import {skipSuiteOnJasmine} from '@jest/test-utils'; +import {json as runJest} from '../runJest'; +import { + cleanup, + createEmptyPackage, + extractSummary, + writeFiles, +} from '../Utils'; + +const DIR = path.resolve(tmpdir(), 'injectGlobalVariables.test'); +const TEST_DIR = path.resolve(DIR, '__tests__'); + +skipSuiteOnJasmine(); + +beforeEach(() => { + cleanup(DIR); + createEmptyPackage(DIR); + + const content = ` + const {expect: importedExpect, test: importedTest} = require('@jest/globals'); + + importedTest('no globals injected', () =>{ + importedExpect(typeof expect).toBe('undefined'); + importedExpect(typeof test).toBe('undefined'); + importedExpect(typeof jest).toBe('undefined'); + importedExpect(typeof beforeEach).toBe('undefined'); + }); + `; + + writeFiles(TEST_DIR, {'test.js': content}); +}); + +afterAll(() => cleanup(DIR)); + +test.each` + configSource | args + ${'CLI'} | ${['--inject-globals', 'false']} + ${'config'} | ${['--config', JSON.stringify({injectGlobals: false})]} +`('globals are undefined if passed `false` from $configSource', ({args}) => { + const {json, stderr, exitCode} = runJest(DIR, args); + + const {summary, rest} = extractSummary(stderr); + expect(wrap(rest)).toMatchSnapshot(); + expect(wrap(summary)).toMatchSnapshot(); + expect(exitCode).toBe(0); + expect(json.numPassedTests).toBe(1); +}); diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index e666b2230434..5f07f2c09100 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -32,9 +32,9 @@ const jestAdapter = async ( FRAMEWORK_INITIALIZER, ); - runtime + const expect = runtime .requireInternalModule(EXPECT_INITIALIZER) - .default({expand: globalConfig.expand}); + .default(globalConfig); const getPrettier = () => config.prettierPath ? require(config.prettierPath) : null; @@ -52,6 +52,14 @@ const jestAdapter = async ( testPath, }); + const runtimeGlobals = {expect, ...globals}; + runtime.setGlobalsForRuntime(runtimeGlobals); + + // TODO: `jest-circus` might be newer than `jest-config` - remove `??` for Jest 27 + if (config.injectGlobals ?? true) { + Object.assign(environment.global, runtimeGlobals); + } + if (config.timers === 'fake' || config.timers === 'legacy') { // during setup, this cannot be null (and it's fine to explode if it is) environment.fakeTimers!.useFakeTimers(); diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index 7841fccd2e0c..bedd74bb922d 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -113,9 +113,6 @@ export const initialize = async ({ return concurrent; })(globalsObject.test); - const nodeGlobal = global as Global.Global; - Object.assign(nodeGlobal, globalsObject); - addEventHandler(eventHandler); if (environment.handleTestEvent) { diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts index 064f1d800595..3b67860c255e 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import type {Config} from '@jest/types'; import expect = require('expect'); import { @@ -15,8 +16,7 @@ import { toThrowErrorMatchingSnapshot, } from 'jest-snapshot'; -export default (config: {expand: boolean}): void => { - global.expect = expect; +export default (config: Pick): typeof expect => { expect.setState({expand: config.expand}); expect.extend({ toMatchInlineSnapshot, @@ -26,4 +26,6 @@ export default (config: {expand: boolean}): void => { }); expect.addSnapshotSerializer = addSerializer; + + return expect; }; diff --git a/packages/jest-cli/src/cli/args.ts b/packages/jest-cli/src/cli/args.ts index f7018d51ab10..18b8088f2f32 100644 --- a/packages/jest-cli/src/cli/args.ts +++ b/packages/jest-cli/src/cli/args.ts @@ -320,6 +320,10 @@ export const options = { description: 'Generate a basic configuration file', type: 'boolean', }, + injectGlobals: { + description: 'Should Jest inject global variables or not', + type: 'boolean', + }, json: { default: undefined, description: diff --git a/packages/jest-config/src/Defaults.ts b/packages/jest-config/src/Defaults.ts index 8f496952ea0e..3ed8152dbd82 100644 --- a/packages/jest-config/src/Defaults.ts +++ b/packages/jest-config/src/Defaults.ts @@ -32,6 +32,7 @@ const defaultOptions: Config.DefaultOptions = { computeSha1: false, throwOnModuleCollision: false, }, + injectGlobals: true, maxConcurrency: 5, maxWorkers: '50%', moduleDirectories: ['node_modules'], diff --git a/packages/jest-config/src/ValidConfig.ts b/packages/jest-config/src/ValidConfig.ts index 096416b383e8..c22337e0feaf 100644 --- a/packages/jest-config/src/ValidConfig.ts +++ b/packages/jest-config/src/ValidConfig.ts @@ -58,6 +58,7 @@ const initialOptions: Config.InitialOptions = { platforms: ['ios', 'android'], throwOnModuleCollision: false, }, + injectGlobals: true, json: false, lastCommit: false, logHeapUsage: true, diff --git a/packages/jest-config/src/index.ts b/packages/jest-config/src/index.ts index abbbdeea68fa..c0cc2326a7ec 100644 --- a/packages/jest-config/src/index.ts +++ b/packages/jest-config/src/index.ts @@ -186,6 +186,7 @@ const groupOptions = ( globalTeardown: options.globalTeardown, globals: options.globals, haste: options.haste, + injectGlobals: options.injectGlobals, moduleDirectories: options.moduleDirectories, moduleFileExtensions: options.moduleFileExtensions, moduleLoader: options.moduleLoader, diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index db26b8740ab2..d732abda33b0 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -885,6 +885,7 @@ export default function normalize( case 'findRelatedTests': case 'forceCoverageMatch': case 'forceExit': + case 'injectGlobals': case 'lastCommit': case 'listTests': case 'logHeapUsage': diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 547afb313373..482a460ef879 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -30,7 +30,7 @@ export type ModuleWrapper = ( __dirname: string, __filename: Module['filename'], global: Global.Global, - jest: Jest, + jest?: Jest, ...extraGlobals: Array ) => unknown; diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index bedb854ee461..9853c3bb4476 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -179,6 +179,7 @@ class Runtime { private _virtualMocks: BooleanMap; private _moduleImplementation?: typeof nativeModule.Module; private jestObjectCaches: Map; + private jestGlobals?: JestGlobals; constructor( config: Config.ProjectConfig, @@ -1049,6 +1050,19 @@ class Runtime { this.jestObjectCaches.set(filename, jestObject); + const lastArgs: [Jest | undefined, ...Array] = [ + this._config.injectGlobals ? jestObject : undefined, // jest object + this._config.extraGlobals.map(globalVariable => { + if (this._environment.global[globalVariable]) { + return this._environment.global[globalVariable]; + } + + throw new Error( + `You have requested '${globalVariable}' as a global variable, but it was not present. Please check your config or your global environment.`, + ); + }), + ]; + try { compiledFunction.call( localModule.exports, @@ -1058,16 +1072,7 @@ class Runtime { dirname, // __dirname filename, // __filename this._environment.global, // global object - jestObject, // jest object - ...this._config.extraGlobals.map(globalVariable => { - if (this._environment.global[globalVariable]) { - return this._environment.global[globalVariable]; - } - - throw new Error( - `You have requested '${globalVariable}' as a global variable, but it was not present. Please check your config or your global environment.`, - ); - }), + ...lastArgs.filter(notEmpty), ); } catch (error) { this.handleExecutionError(error, localModule); @@ -1609,7 +1614,7 @@ class Runtime { ); } - private constructInjectedModuleParameters() { + private constructInjectedModuleParameters(): Array { return [ 'module', 'exports', @@ -1617,9 +1622,9 @@ class Runtime { '__dirname', '__filename', 'global', - 'jest', + this._config.injectGlobals ? 'jest' : undefined, ...this._config.extraGlobals, - ]; + ].filter(notEmpty); } private handleExecutionError(e: Error, module: InitialModule): never { @@ -1686,6 +1691,10 @@ class Runtime { } private getGlobalsFromEnvironment(): JestGlobals { + if (this.jestGlobals) { + return {...this.jestGlobals}; + } + return { afterAll: this._environment.global.afterAll, afterEach: this._environment.global.afterEach, @@ -1714,6 +1723,10 @@ class Runtime { return source; } + + setGlobalsForRuntime(globals: JestGlobals): void { + this.jestGlobals = globals; + } } function invariant(condition: unknown, message?: string): asserts condition { @@ -1722,4 +1735,8 @@ function invariant(condition: unknown, message?: string): asserts condition { } } +function notEmpty(value: T | null | undefined): value is T { + return value !== null && value !== undefined; +} + export = Runtime; diff --git a/packages/jest-types/src/Config.ts b/packages/jest-types/src/Config.ts index 45cbd05a8c38..bfae5927dd3a 100644 --- a/packages/jest-types/src/Config.ts +++ b/packages/jest-types/src/Config.ts @@ -60,6 +60,7 @@ export type DefaultOptions = { forceCoverageMatch: Array; globals: ConfigGlobals; haste: HasteConfig; + injectGlobals: boolean; maxConcurrency: number; maxWorkers: number | string; moduleDirectories: Array; @@ -144,6 +145,7 @@ export type InitialOptions = Partial<{ globalSetup: string | null | undefined; globalTeardown: string | null | undefined; haste: HasteConfig; + injectGlobals: boolean; reporters: Array; logHeapUsage: boolean; lastCommit: boolean; @@ -329,6 +331,7 @@ export type ProjectConfig = { globalTeardown?: string; globals: ConfigGlobals; haste: HasteConfig; + injectGlobals: boolean; moduleDirectories: Array; moduleFileExtensions: Array; moduleLoader?: Path; @@ -399,6 +402,7 @@ export type Argv = Arguments< globalTeardown: string | null | undefined; haste: string; init: boolean; + injectGlobals: boolean; json: boolean; lastCommit: boolean; logHeapUsage: boolean;