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

feat: add new injectGlobals config and CLI option to disable injecting global variables into the runtime #10484

Merged
merged 3 commits into from Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions docs/CLI.md
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions docs/Configuration.md
Expand Up @@ -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`
Expand Down
27 changes: 27 additions & 0 deletions 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: <<REPLACED>>
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: <<REPLACED>>
Ran all test suites.
`;
1 change: 1 addition & 0 deletions e2e/__tests__/__snapshots__/showConfig.test.ts.snap
Expand Up @@ -22,6 +22,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
"computeSha1": false,
"throwOnModuleCollision": false
},
"injectGlobals": true,
"moduleDirectories": [
"node_modules"
],
Expand Down
57 changes: 57 additions & 0 deletions 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);
});
12 changes: 10 additions & 2 deletions packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts
Expand Up @@ -32,9 +32,9 @@ const jestAdapter = async (
FRAMEWORK_INITIALIZER,
);

runtime
const expect = runtime
.requireInternalModule<typeof import('./jestExpect')>(EXPECT_INITIALIZER)
.default({expand: globalConfig.expand});
.default(globalConfig);

const getPrettier = () =>
config.prettierPath ? require(config.prettierPath) : null;
Expand All @@ -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();
Expand Down
Expand Up @@ -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) {
Expand Down
Expand Up @@ -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 {
Expand All @@ -15,8 +16,7 @@ import {
toThrowErrorMatchingSnapshot,
} from 'jest-snapshot';

export default (config: {expand: boolean}): void => {
global.expect = expect;
export default (config: Pick<Config.GlobalConfig, 'expand'>): typeof expect => {
expect.setState({expand: config.expand});
expect.extend({
toMatchInlineSnapshot,
Expand All @@ -26,4 +26,6 @@ export default (config: {expand: boolean}): void => {
});

expect.addSnapshotSerializer = addSerializer;

return expect;
};
4 changes: 4 additions & 0 deletions packages/jest-cli/src/cli/args.ts
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/Defaults.ts
Expand Up @@ -32,6 +32,7 @@ const defaultOptions: Config.DefaultOptions = {
computeSha1: false,
throwOnModuleCollision: false,
},
injectGlobals: true,
maxConcurrency: 5,
maxWorkers: '50%',
moduleDirectories: ['node_modules'],
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/ValidConfig.ts
Expand Up @@ -58,6 +58,7 @@ const initialOptions: Config.InitialOptions = {
platforms: ['ios', 'android'],
throwOnModuleCollision: false,
},
injectGlobals: true,
json: false,
lastCommit: false,
logHeapUsage: true,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.ts
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/normalize.ts
Expand Up @@ -885,6 +885,7 @@ export default function normalize(
case 'findRelatedTests':
case 'forceCoverageMatch':
case 'forceExit':
case 'injectGlobals':
case 'lastCommit':
case 'listTests':
case 'logHeapUsage':
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-environment/src/index.ts
Expand Up @@ -30,7 +30,7 @@ export type ModuleWrapper = (
__dirname: string,
__filename: Module['filename'],
global: Global.Global,
jest: Jest,
jest?: Jest,
...extraGlobals: Array<Global.Global[keyof Global.Global]>
) => unknown;

Expand Down
43 changes: 30 additions & 13 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -179,6 +179,7 @@ class Runtime {
private _virtualMocks: BooleanMap;
private _moduleImplementation?: typeof nativeModule.Module;
private jestObjectCaches: Map<string, Jest>;
private jestGlobals?: JestGlobals;

constructor(
config: Config.ProjectConfig,
Expand Down Expand Up @@ -1049,6 +1050,19 @@ class Runtime {

this.jestObjectCaches.set(filename, jestObject);

const lastArgs: [Jest | undefined, ...Array<any>] = [
this._config.injectGlobals ? jestObject : undefined, // jest object
this._config.extraGlobals.map<unknown>(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,
Expand All @@ -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);
Expand Down Expand Up @@ -1609,17 +1614,17 @@ class Runtime {
);
}

private constructInjectedModuleParameters() {
private constructInjectedModuleParameters(): Array<string> {
return [
'module',
'exports',
'require',
'__dirname',
'__filename',
'global',
'jest',
this._config.injectGlobals ? 'jest' : undefined,
...this._config.extraGlobals,
];
].filter(notEmpty);
}

private handleExecutionError(e: Error, module: InitialModule): never {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1714,6 +1723,10 @@ class Runtime {

return source;
}

setGlobalsForRuntime(globals: JestGlobals): void {
this.jestGlobals = globals;
}
}

function invariant(condition: unknown, message?: string): asserts condition {
Expand All @@ -1722,4 +1735,8 @@ function invariant(condition: unknown, message?: string): asserts condition {
}
}

function notEmpty<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}

export = Runtime;
4 changes: 4 additions & 0 deletions packages/jest-types/src/Config.ts
Expand Up @@ -60,6 +60,7 @@ export type DefaultOptions = {
forceCoverageMatch: Array<Glob>;
globals: ConfigGlobals;
haste: HasteConfig;
injectGlobals: boolean;
maxConcurrency: number;
maxWorkers: number | string;
moduleDirectories: Array<string>;
Expand Down Expand Up @@ -144,6 +145,7 @@ export type InitialOptions = Partial<{
globalSetup: string | null | undefined;
globalTeardown: string | null | undefined;
haste: HasteConfig;
injectGlobals: boolean;
reporters: Array<string | ReporterConfig>;
logHeapUsage: boolean;
lastCommit: boolean;
Expand Down Expand Up @@ -329,6 +331,7 @@ export type ProjectConfig = {
globalTeardown?: string;
globals: ConfigGlobals;
haste: HasteConfig;
injectGlobals: boolean;
moduleDirectories: Array<string>;
moduleFileExtensions: Array<string>;
moduleLoader?: Path;
Expand Down Expand Up @@ -399,6 +402,7 @@ export type Argv = Arguments<
globalTeardown: string | null | undefined;
haste: string;
init: boolean;
injectGlobals: boolean;
json: boolean;
lastCommit: boolean;
logHeapUsage: boolean;
Expand Down