Skip to content

Commit

Permalink
feat(jest-environment): Pass global config to Jest environment constr…
Browse files Browse the repository at this point in the history
…uctor

Closes #12271
  • Loading branch information
ahnpnl committed Feb 22, 2022
1 parent f9d4fc4 commit 7e5a402
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 80 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,7 +9,9 @@
- `[jest-config, @jest/types]` Add `ci` to `GlobalConfig` ([#12378](https://github.com/facebook/jest/pull/12378))
- `[jest-environment-jsdom]` [**BREAKING**] Upgrade jsdom to 19.0.0 ([#12290](https://github.com/facebook/jest/pull/12290))
- `[jest-environment-jsdom]` [**BREAKING**] Add default `browser` condition to `exportConditions` for `jsdom` environment ([#11924](https://github.com/facebook/jest/pull/11924))
- `[jest-environment-jsdom]` [**BREAKING**] Pass global config to Jest environment constructor for `jsdom` environment ([#12461](https://github.com/facebook/jest/pull/12461))
- `[jest-environment-node]` [**BREAKING**] Add default `node` and `node-addon` conditions to `exportConditions` for `node` environment ([#11924](https://github.com/facebook/jest/pull/11924))
- `[jest-environment-node]` [**BREAKING**] Pass global config to Jest environment constructor for `node` environment ([#12461](https://github.com/facebook/jest/pull/12461))
- `[@jest/expect]` New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404), [#12410](https://github.com/facebook/jest/pull/12410), [#12418](https://github.com/facebook/jest/pull/12418))
- `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323))
- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442))
Expand Down
4 changes: 3 additions & 1 deletion docs/Configuration.md
Expand Up @@ -1083,7 +1083,7 @@ test('use jsdom in this test file', () => {
});
```

You can create your own module that will be used for setting up the test environment. The module must export a class with `setup`, `teardown` and `getVmContext` methods. You can also pass variables from this module to your test suites by assigning them to `this.global` object – this will make them available in your test suites as global variables.
You can create your own module that will be used for setting up the test environment. The module must export a class with `setup`, `teardown` and `getVmContext` methods. You can also pass variables from this module to your test suites by assigning them to `this.global` object – this will make them available in your test suites as global variables. Besides, you can access [global config](https://github.com/facebook/jest/blob/491e7cb0f2daa8263caccc72d48bdce7ba759b11/packages/jest-types/src/Config.ts#L284) and [project config](https://github.com/facebook/jest/blob/491e7cb0f2daa8263caccc72d48bdce7ba759b11/packages/jest-types/src/Config.ts#L349) in its constructor.

The class may optionally expose an asynchronous `handleTestEvent` method to bind to events fired by [`jest-circus`](https://github.com/facebook/jest/tree/main/packages/jest-circus). Normally, `jest-circus` test runner would pause until a promise returned from `handleTestEvent` gets fulfilled, **except for the next events**: `start_describe_definition`, `finish_describe_definition`, `add_hook`, `add_test` or `error` (for the up-to-date list you can look at [SyncEvent type in the types definitions](https://github.com/facebook/jest/tree/main/packages/jest-types/src/Circus.ts)). That is caused by backward compatibility reasons and `process.on('unhandledRejection', callback)` signature, but that usually should not be a problem for most of the use cases.

Expand All @@ -1108,6 +1108,8 @@ const NodeEnvironment = require('jest-environment-node').default;
class CustomEnvironment extends NodeEnvironment {
constructor(config, context) {
super(config, context);
console.log(config.globalConfig);
console.log(config.projectConfig);
this.testPath = context.testPath;
this.docblockPragmas = context.docblockPragmas;
}
Expand Down
2 changes: 2 additions & 0 deletions e2e/test-environment/EsmDefaultEnvironment.js
Expand Up @@ -14,6 +14,8 @@ class Env extends NodeEnvironment {
constructor(config, options) {
super(config, options);
this.global.property = 'value';
this.global.var1 = config.globalConfig.watch;
this.global.var2 = config.projectConfig.cache;
}
}

Expand Down
2 changes: 2 additions & 0 deletions e2e/test-environment/__tests__/esmDefault.test.js
Expand Up @@ -10,4 +10,6 @@

test('access env', () => {
expect(property).toBe('value'); // eslint-disable-line no-undef
expect(var1).toBe(false); // eslint-disable-line no-undef
expect(var2).toBe(true); // eslint-disable-line no-undef
});
Expand Up @@ -5,12 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/

import {makeProjectConfig} from '@jest/test-utils';
import {makeGlobalConfig, makeProjectConfig} from '@jest/test-utils';
import JSDomEnvironment from '../';

describe('JSDomEnvironment', () => {
it('should configure setTimeout/setInterval to use the browser api', () => {
const env = new JSDomEnvironment(makeProjectConfig());
const env = new JSDomEnvironment({
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig(),
});

env.fakeTimers!.useFakeTimers();

Expand All @@ -23,19 +26,23 @@ describe('JSDomEnvironment', () => {
});

it('has modern fake timers implementation', () => {
const env = new JSDomEnvironment(makeProjectConfig());
const env = new JSDomEnvironment({
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig(),
});

expect(env.fakeTimersModern).toBeDefined();
});

it('should respect userAgent option', () => {
const env = new JSDomEnvironment(
makeProjectConfig({
const env = new JSDomEnvironment({
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig({
testEnvironmentOptions: {
userAgent: 'foo',
},
}),
);
});

expect(env.dom.window.navigator.userAgent).toEqual('foo');
});
Expand All @@ -53,7 +60,10 @@ describe('JSDomEnvironment', () => {
* will be called, so please make sure the global.document is still available at this point.
*/
it('should not set the global.document to null too early', () => {
const env = new JSDomEnvironment(makeProjectConfig());
const env = new JSDomEnvironment({
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig(),
});

const originalCloseFn = env.global.close.bind(env.global);
env.global.close = () => {
Expand Down
29 changes: 17 additions & 12 deletions packages/jest-environment-jsdom/src/index.ts
Expand Up @@ -7,9 +7,13 @@

import type {Context} from 'vm';
import {JSDOM, ResourceLoader, VirtualConsole} from 'jsdom';
import type {EnvironmentContext, JestEnvironment} from '@jest/environment';
import type {
EnvironmentContext,
JestEnvironment,
JestEnvironmentConfig,
} from '@jest/environment';
import {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
import type {Config, Global} from '@jest/types';
import type {Global} from '@jest/types';
import {ModuleMocker} from 'jest-mock';
import {installCommonGlobals} from 'jest-util';

Expand All @@ -30,25 +34,26 @@ export default class JSDOMEnvironment implements JestEnvironment<number> {
private errorEventListener: ((event: Event & {error: Error}) => void) | null;
moduleMocker: ModuleMocker | null;

constructor(config: Config.ProjectConfig, options?: EnvironmentContext) {
constructor(config: JestEnvironmentConfig, options?: EnvironmentContext) {
const {projectConfig} = config;
this.dom = new JSDOM(
typeof config.testEnvironmentOptions.html === 'string'
? config.testEnvironmentOptions.html
typeof projectConfig.testEnvironmentOptions.html === 'string'
? projectConfig.testEnvironmentOptions.html
: '<!DOCTYPE html>',
{
pretendToBeVisual: true,
resources:
typeof config.testEnvironmentOptions.userAgent === 'string'
typeof projectConfig.testEnvironmentOptions.userAgent === 'string'
? new ResourceLoader({
userAgent: config.testEnvironmentOptions.userAgent,
userAgent: projectConfig.testEnvironmentOptions.userAgent,
})
: undefined,
runScripts: 'dangerously',
url: config.testURL,
url: projectConfig.testURL,
virtualConsole: new VirtualConsole().sendTo(
options?.console || console,
),
...config.testEnvironmentOptions,
...projectConfig.testEnvironmentOptions,
},
);
const global = (this.global = this.dom.window.document
Expand All @@ -64,7 +69,7 @@ export default class JSDOMEnvironment implements JestEnvironment<number> {
// Node's error-message stack size is limited at 10, but it's pretty useful
// to see more than that when a test fails.
this.global.Error.stackTraceLimit = 100;
installCommonGlobals(global as any, config.globals);
installCommonGlobals(global as any, projectConfig.globals);

// TODO: remove this ASAP, but it currently causes tests to run really slow
global.Buffer = Buffer;
Expand Down Expand Up @@ -107,14 +112,14 @@ export default class JSDOMEnvironment implements JestEnvironment<number> {
};

this.fakeTimers = new LegacyFakeTimers({
config,
config: projectConfig,
global: global as unknown as typeof globalThis,
moduleMocker: this.moduleMocker,
timerConfig,
});

this.fakeTimersModern = new ModernFakeTimers({
config,
config: projectConfig,
global: global as unknown as typeof globalThis,
});
}
Expand Down
Expand Up @@ -5,31 +5,44 @@
* LICENSE file in the root directory of this source tree.
*/

import {makeProjectConfig} from '@jest/test-utils';
import {makeGlobalConfig, makeProjectConfig} from '@jest/test-utils';
import NodeEnvironment from '../';

describe('NodeEnvironment', () => {
it('uses a copy of the process object', () => {
const env1 = new NodeEnvironment(makeProjectConfig());
const env2 = new NodeEnvironment(makeProjectConfig());
const testEnvConfig = {
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig(),
};
const env1 = new NodeEnvironment(testEnvConfig);
const env2 = new NodeEnvironment(testEnvConfig);

expect(env1.global.process).not.toBe(env2.global.process);
});

it('exposes process.on', () => {
const env1 = new NodeEnvironment(makeProjectConfig());
const env1 = new NodeEnvironment({
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig(),
});

expect(env1.global.process.on).not.toBe(null);
});

it('exposes global.global', () => {
const env1 = new NodeEnvironment(makeProjectConfig());
const env1 = new NodeEnvironment({
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig(),
});

expect(env1.global.global).toBe(env1.global);
});

it('should configure setTimeout/setInterval to use the node api', () => {
const env1 = new NodeEnvironment(makeProjectConfig());
const env1 = new NodeEnvironment({
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig(),
});

env1.fakeTimers!.useFakeTimers();

Expand All @@ -44,7 +57,10 @@ describe('NodeEnvironment', () => {
});

it('has modern fake timers implementation', () => {
const env = new NodeEnvironment(makeProjectConfig());
const env = new NodeEnvironment({
globalConfig: makeGlobalConfig(),
projectConfig: makeProjectConfig(),
});

expect(env.fakeTimersModern).toBeDefined();
});
Expand Down
18 changes: 11 additions & 7 deletions packages/jest-environment-node/src/index.ts
Expand Up @@ -6,9 +6,9 @@
*/

import {Context, createContext, runInContext} from 'vm';
import type {JestEnvironment} from '@jest/environment';
import type {JestEnvironment, JestEnvironmentConfig} from '@jest/environment';
import {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
import type {Config, Global} from '@jest/types';
import type {Global} from '@jest/types';
import {ModuleMocker} from 'jest-mock';
import {installCommonGlobals} from 'jest-util';

Expand All @@ -25,11 +25,12 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
global: Global.Global;
moduleMocker: ModuleMocker | null;

constructor(config: Config.ProjectConfig) {
constructor(config: JestEnvironmentConfig) {
const {projectConfig} = config;
this.context = createContext();
const global = (this.global = runInContext(
'this',
Object.assign(this.context, config.testEnvironmentOptions),
Object.assign(this.context, projectConfig.testEnvironmentOptions),
));
global.global = global;
global.clearInterval = clearInterval;
Expand Down Expand Up @@ -81,7 +82,7 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
global.atob = atob;
global.btoa = btoa;
}
installCommonGlobals(global, config.globals);
installCommonGlobals(global, projectConfig.globals);

this.moduleMocker = new ModuleMocker(global);

Expand All @@ -104,13 +105,16 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
};

this.fakeTimers = new LegacyFakeTimers({
config,
config: projectConfig,
global,
moduleMocker: this.moduleMocker,
timerConfig,
});

this.fakeTimersModern = new ModernFakeTimers({config, global});
this.fakeTimersModern = new ModernFakeTimers({
config: projectConfig,
global,
});
}

async setup(): Promise<void> {}
Expand Down
7 changes: 6 additions & 1 deletion packages/jest-environment/src/index.ts
Expand Up @@ -28,8 +28,13 @@ export type ModuleWrapper = (
...extraGlobals: Array<Global.Global[keyof Global.Global]>
) => unknown;

export interface JestEnvironmentConfig {
projectConfig: Config.ProjectConfig;
globalConfig: Config.GlobalConfig;
}

export declare class JestEnvironment<Timer = unknown> {
constructor(config: Config.ProjectConfig, context?: EnvironmentContext);
constructor(config: JestEnvironmentConfig, context?: EnvironmentContext);
global: Global.Global;
fakeTimers: LegacyFakeTimers<Timer> | null;
fakeTimersModern: ModernFakeTimers | null;
Expand Down
21 changes: 13 additions & 8 deletions packages/jest-repl/src/cli/runtime-cli.ts
Expand Up @@ -63,22 +63,27 @@ export async function run(
const options = await readConfig(argv, root);
const globalConfig = options.globalConfig;
// Always disable automocking in scripts.
const config: Config.ProjectConfig = {
const projectConfig: Config.ProjectConfig = {
...options.projectConfig,
automock: false,
};

try {
const hasteMap = await Runtime.createContext(config, {
const hasteMap = await Runtime.createContext(projectConfig, {
maxWorkers: Math.max(cpus().length - 1, 1),
watchman: globalConfig.watchman,
});

const transformer = await createScriptTransformer(config);
const transformer = await createScriptTransformer(projectConfig);
const Environment: typeof JestEnvironment =
await transformer.requireAndTranspileModule(config.testEnvironment);
await transformer.requireAndTranspileModule(
projectConfig.testEnvironment,
);

const environment = new Environment(config);
const environment = new Environment({
globalConfig,
projectConfig,
});
setGlobal(
environment.global as unknown as typeof globalThis,
'console',
Expand All @@ -87,7 +92,7 @@ export async function run(
setGlobal(
environment.global as unknown as typeof globalThis,
'jestProjectConfig',
config,
projectConfig,
);
setGlobal(
environment.global as unknown as typeof globalThis,
Expand All @@ -96,7 +101,7 @@ export async function run(
);

const runtime = new Runtime(
config,
projectConfig,
environment,
hasteMap.resolver,
transformer,
Expand All @@ -112,7 +117,7 @@ export async function run(
filePath,
);

for (const path of config.setupFiles) {
for (const path of projectConfig.setupFiles) {
const esm = runtime.unstable_shouldLoadAsEsm(path);

if (esm) {
Expand Down

0 comments on commit 7e5a402

Please sign in to comment.