Skip to content

Commit

Permalink
feat(jest-environment-{node,jsdom}): allow specifying `customExportCo…
Browse files Browse the repository at this point in the history
…nditions`
  • Loading branch information
jeysal committed Apr 29, 2022
1 parent e0bc54d commit d7a6ba6
Show file tree
Hide file tree
Showing 10 changed files with 83 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### Features

- `[jest-environment-node, jest-environment-jsdom]` Allow specifying `customExportConditions` ([#12774](https://github.com/facebook/jest/pull/12774))

### Fixes

### Chore & Maintenance
Expand Down
6 changes: 5 additions & 1 deletion docs/Configuration.md
Expand Up @@ -1362,7 +1362,11 @@ beforeAll(() => {

Default: `{}`

Test environment options that will be passed to the `testEnvironment`. The relevant options depend on the environment. For example, you can override options given to [`jsdom`](https://github.com/jsdom/jsdom) such as `{html: "<html lang="zh-cmn-Hant"></html>", url: 'https://jestjs.io/', userAgent: "Agent/007"}`.
Test environment options that will be passed to the `testEnvironment`. The relevant options depend on the environment.

For example, in `jest-environment-jsdom`, you can override options given to [`jsdom`](https://github.com/jsdom/jsdom) such as `{html: "<html lang="zh-cmn-Hant"></html>", url: 'https://jestjs.io/', userAgent: "Agent/007"}`.

Both `jest-environment-jsdom` and `jest-environment-node` allow specifying `customExportConditions`, which allow you to control which versions of a library are loaded from `exports` in `package.json`. `jest-environment-jsdom` defaults to `['browser']`. `jest-environment-node` defaults to `['node', 'node-addons']`.

These options can also be passed in a docblock, similar to `testEnvironment`. Note that it must be parseable by `JSON.parse`. Example:

Expand Down
2 changes: 1 addition & 1 deletion docs/UpgradingToJest28.md
Expand Up @@ -173,7 +173,7 @@ npm install --save-dev jest-jasmine2

Jest now includes full support for [package `exports`](https://nodejs.org/api/packages.html#exports), which might mean that files you import are not resolved correctly.

Additionally, Jest now supplies more conditions. `jest-environment-node` has `node` and `node-addons`, while `jest-environment-jsdom` has `browser`. As a result, you might e.g. get browser code which assumes ESM, when Jest provides `['require', 'browser']`. You can either report a bug to the library (or Jest, the implementation is new and might have bugs!), override the conditions Jest passes (via a custom test environment and overriding `exportConditions()`), using a custom resolver or `moduleMapper`. Lots of options, and you'll need to pick the correct one for your project.
Additionally, Jest now supplies more conditions. `jest-environment-node` has `node` and `node-addons`, while `jest-environment-jsdom` has `browser`. As a result, you might e.g. get browser code which assumes ESM, when Jest provides `['require', 'browser']`. You can either report a bug to the library (or Jest, the implementation is new and might have bugs!), override the conditions Jest passes (by passing the `customExportConditions` option to the test environment), or use a custom resolver or `moduleMapper`. Lots of options, and you'll need to pick the correct one for your project.

Known examples of packages that fails in Jest 28 are [`uuid`](https://npmjs.com/package/uuid) and [`nanoid`](https://npmjs.com/package/nanoid) when using the `jest-environment-jsdom` environment. For an analysis, and a potential workaround, see [this comment](https://github.com/microsoft/accessibility-insights-web/pull/5421#issuecomment-1109168149).

Expand Down
1 change: 1 addition & 0 deletions e2e/resolve-conditions/.gitignore
@@ -0,0 +1 @@
!node_modules
@@ -0,0 +1,15 @@
/**
* 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.
*
* @jest-environment jest-environment-jsdom
* @jest-environment-options {"customExportConditions": ["special"]}
*/

import {fn} from 'fake-dual-dep';

test('returns correct message', () => {
expect(fn()).toEqual('hello from special');
});
@@ -0,0 +1,15 @@
/**
* 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.
*
* @jest-environment jest-environment-node
* @jest-environment-options {"customExportConditions": ["special"]}
*/

import {fn} from 'fake-dual-dep';

test('returns correct message', () => {
expect(fn()).toEqual('hello from special');
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions e2e/resolve-conditions/node_modules/fake-dual-dep/special.mjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion packages/jest-environment-jsdom/src/index.ts
Expand Up @@ -33,6 +33,7 @@ export default class JSDOMEnvironment implements JestEnvironment<number> {
global: Win;
private errorEventListener: ((event: Event & {error: Error}) => void) | null;
moduleMocker: ModuleMocker | null;
customExportConditions = ['browser'];

constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {
const {projectConfig} = config;
Expand Down Expand Up @@ -109,6 +110,20 @@ export default class JSDOMEnvironment implements JestEnvironment<number> {
return originalRemoveListener.apply(this, args);
};

if ('customExportConditions' in projectConfig.testEnvironmentOptions) {
const {customExportConditions} = projectConfig.testEnvironmentOptions;
if (
Array.isArray(customExportConditions) &&
customExportConditions.every(item => typeof item === 'string')
) {
this.customExportConditions = customExportConditions;
} else {
throw new Error(
'Custom export conditions specified but they are not an array of strings',
);
}
}

this.moduleMocker = new ModuleMocker(global as any);

this.fakeTimers = new LegacyFakeTimers({
Expand Down Expand Up @@ -158,7 +173,7 @@ export default class JSDOMEnvironment implements JestEnvironment<number> {
}

exportConditions(): Array<string> {
return ['browser'];
return this.customExportConditions;
}

getVmContext(): Context | null {
Expand Down
17 changes: 16 additions & 1 deletion packages/jest-environment-node/src/index.ts
Expand Up @@ -59,6 +59,7 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
fakeTimersModern: ModernFakeTimers | null;
global: Global.Global;
moduleMocker: ModuleMocker | null;
customExportConditions = ['node', 'node-addons'];

// while `context` is unused, it should always be passed
constructor(config: JestEnvironmentConfig, _context: EnvironmentContext) {
Expand Down Expand Up @@ -111,6 +112,20 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {

installCommonGlobals(global, projectConfig.globals);

if ('customExportConditions' in projectConfig.testEnvironmentOptions) {
const {customExportConditions} = projectConfig.testEnvironmentOptions;
if (
Array.isArray(customExportConditions) &&
customExportConditions.every(item => typeof item === 'string')
) {
this.customExportConditions = customExportConditions;
} else {
throw new Error(
'Custom export conditions specified but they are not an array of strings',
);
}
}

this.moduleMocker = new ModuleMocker(global);

const timerIdToRef = (id: number) => ({
Expand Down Expand Up @@ -157,7 +172,7 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
}

exportConditions(): Array<string> {
return ['node', 'node-addons'];
return this.customExportConditions;
}

getVmContext(): Context | null {
Expand Down

0 comments on commit d7a6ba6

Please sign in to comment.