From eb33c365d5b3e4b246b2de96b6631ef2ce44cd3a Mon Sep 17 00:00:00 2001 From: ahnpnl Date: Mon, 13 Oct 2025 22:29:52 +0200 Subject: [PATCH] docs: update `testEnvironment` and `testEnvironmentOptions` doc --- docs/Configuration.md | 168 ++++++---- docs/TestEnvironment.md | 293 +++++++++++++++++ website/sidebars.json | 1 + .../version-29.7/Configuration.md | 184 ++++++----- .../version-29.7/TestEnvironment.md | 243 ++++++++++++++ .../version-30.0/Configuration.md | 184 ++++++----- .../version-30.0/TestEnvironment.md | 296 ++++++++++++++++++ .../version-29.7-sidebars.json | 1 + .../version-30.0-sidebars.json | 1 + 9 files changed, 1163 insertions(+), 208 deletions(-) create mode 100644 docs/TestEnvironment.md create mode 100644 website/versioned_docs/version-29.7/TestEnvironment.md create mode 100644 website/versioned_docs/version-30.0/TestEnvironment.md diff --git a/docs/Configuration.md b/docs/Configuration.md index 2e10910b5f44..ebe83380c626 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1889,15 +1889,17 @@ More about serializers API can be found [here](https://github.com/jestjs/jest/tr ::: -### `testEnvironment` \[string] +### `testEnvironment` \[node | jsdom | string] -Default: `"node"` +Default: `node` The test environment that will be used for testing. The default environment in Jest is a Node.js environment. If you are building a web app, you can use a browser-like environment through [`jsdom`](https://github.com/jsdom/jsdom) instead. By adding a `@jest-environment` docblock at the top of the file, you can specify another environment to be used for all tests in that file: -```js +- With built-in environments: + +```js tab title="my-test.spec.js" /** * @jest-environment jsdom */ @@ -1908,91 +1910,108 @@ 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. The constructor is passed [`globalConfig`](https://github.com/jestjs/jest/blob/v29.2.1/packages/jest-types/src/Config.ts#L358-L422) and [`projectConfig`](https://github.com/jestjs/jest/blob/v29.2.1/packages/jest-types/src/Config.ts#L424-L481) as its first argument, and [`testEnvironmentContext`](https://github.com/jestjs/jest/blob/491e7cb0f2daa8263caccc72d48bdce7ba759b11/packages/jest-environment/src/index.ts#L13) as its second. - -The class may optionally expose an asynchronous `handleTestEvent` method to bind to events fired by [`jest-circus`](https://github.com/jestjs/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/jestjs/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. +```ts tab title="my-test.spec.ts" +/** + * @jest-environment jsdom + */ -Any docblock pragmas in test files will be passed to the environment constructor and can be used for per-test configuration. If the pragma does not have a value, it will be present in the object with its value set to an empty string. If the pragma is not present, it will not be present in the object. +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -To use this class as your custom environment, refer to it by its full path within the project. For example, if your class is stored in `my-custom-environment.js` in some subfolder of your project, then the annotation might look like this: +- With custom environment: -```js +```js tab title="my-test.spec.js" /** - * @jest-environment ./src/test/my-custom-environment + * @jest-environment ./my-custom-environment.js */ -``` -:::info - -TestEnvironment is sandboxed. Each test suite will trigger setup/teardown in their own TestEnvironment. +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -::: +```ts tab title="my-test.spec.ts" +/** + * @jest-environment ./my-custom-environment.ts + */ -Example: +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -```js -// my-custom-environment -const NodeEnvironment = require('jest-environment-node').TestEnvironment; +You can also define custom environment. When non-builtin environment is used, Jest will try to load either the file path or the package name defined as value for `testEnvironment`. That file or the package should export an object with the shape of `JestEnvironment`: -class CustomEnvironment extends NodeEnvironment { +```js tab title="environment.js" +/** + * @implements {import('@jest/environment').JestEnvironment} + */ +class CustomEnvironment { constructor(config, context) { - super(config, context); - console.log(config.globalConfig); - console.log(config.projectConfig); - this.testPath = context.testPath; - this.docblockPragmas = context.docblockPragmas; - } - - async setup() { - await super.setup(); - await someSetupTasks(this.testPath); - this.global.someGlobalObject = createGlobalObject(); - - // Will trigger if docblock contains @my-custom-pragma my-pragma-value - if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { - // ... - } - } - - async teardown() { - this.global.someGlobalObject = destroyGlobalObject(); - await someTeardownTasks(); - await super.teardown(); + const {projectConfig} = config; + // Implement the constructor } + // Example of a method getVmContext() { - return super.getVmContext(); - } - - async handleTestEvent(event, state) { - if (event.name === 'test_start') { - // ... - } + return null; } + // Implement the required methods here } module.exports = CustomEnvironment; ``` -```js -// my-test-suite -/** - * @jest-environment ./my-custom-environment - */ -let someGlobalObject; +```ts tab title="environment.ts" +import type { + EnvironmentContext, + JestEnvironment, + JestEnvironmentConfig, +} from '@jest/environment'; -beforeAll(() => { - someGlobalObject = globalThis.someGlobalObject; -}); +export default class CustomEnvironment implements JestEnvironment { + constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { + const {projectConfig} = config; + // Implement the constructor + } + + // Example of a method + getVmContext() { + return null; + } + // Implement the required methods here +} ``` +Jest also exposes `builtinEnvironments` through `jest-environment-node` and `jest-environment-jsdom` packages, in case you just want to extend it. You can read more about extending environments in [our guide](TestEnvironment.md). + ### `testEnvironmentOptions` \[Object] Default: `{}` -Test environment options that will be passed to the `testEnvironment`. The relevant options depend on the environment. +Test environment options that will be passed to the `testEnvironment`. The relevant options depend on the environment being used. + +#### Node Environment Options + +When using the `node` environment, you can configure various options that are passed to `runInContext`. These options include: -For example, you can override options passed to [`jsdom`](https://github.com/jsdom/jsdom): +- **`globalsCleanup`** (**'on'** | **'soft'** | **'off'**): Controls cleanup of global variables between tests. Default: `'soft'`. +- All the options listed in the [vm.runInContext](https://nodejs.org/api/vm.html#scriptrunincontextcontextifiedobject-options) documentation + +#### JSDOM Environment Options + +When using the `jsdom` environment, you can configure various options that are passed to [jsdom](https://github.com/jsdom/jsdom). These options include: + +- **`url`** - The URL of the page (affects `window.location` and relative URLs). Default: `"http://localhost"` +- **`userAgent`** - The user agent string. Default: a generic user agent +- All the options listed in the [jsdom](https://github.com/jsdom/jsdom) + +For example, you can override options passed to `jsdom`: ```js tab title="jest.config.js" const {defineConfig} = require('jest'); @@ -2000,7 +2019,7 @@ const {defineConfig} = require('jest'); module.exports = defineConfig({ testEnvironment: 'jsdom', testEnvironmentOptions: { - html: '', + html: '', url: 'https://jestjs.io/', userAgent: 'Agent/007', }, @@ -2013,14 +2032,23 @@ import {defineConfig} from 'jest'; export default defineConfig({ testEnvironment: 'jsdom', testEnvironmentOptions: { - html: '', + 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']`. +#### Custom Export Conditions + +The `testEnvironmentOptions` allow specifying `customExportConditions`, which control which versions of a library are loaded from `exports` in `package.json`. + +The built-in environments have the following defaults: + +- `jest-environment-jsdom` defaults to `['browser']` +- `jest-environment-node` defaults to `['node', 'node-addons']` + +For example, you can override `customExportConditions` passed to `jsdom`: ```js tab title="jest.config.js" const {defineConfig} = require('jest'); @@ -2057,6 +2085,20 @@ test('use jsdom and set the URL in this test file', () => { }); ``` +You can combine multiple options: + +```js +/** + * @jest-environment jsdom + * @jest-environment-options {"url": "https://example.com/", "userAgent": "Custom Agent"} + */ + +test('use custom URL and user agent', () => { + expect(window.location.href).toBe('https://example.com/'); + expect(window.navigator.userAgent).toContain('Custom Agent'); +}); +``` + ### `testFailureExitCode` \[number] Default: `1` diff --git a/docs/TestEnvironment.md b/docs/TestEnvironment.md new file mode 100644 index 000000000000..c9ba2696c128 --- /dev/null +++ b/docs/TestEnvironment.md @@ -0,0 +1,293 @@ +--- +id: test-environment +title: Test Environment +--- + +Jest provides environment option to run code inside a specific environment. You can modify how environment behaves with `testEnvironmentOptions` option. + +By default, you can use these environments: + +- `node` is default environment +- `jsdom` emulates browser environment by providing Browser API, uses [`jsdom`](https://github.com/jsdom/jsdom) package + +## Environments for Specific Files + +When setting `testEnvironment` option in your config, it will apply to all the test files in your project. To have more fine-grained control, you can use control comments to specify environment for specific files. Control comments are comments that start with `@jest-environment` and are followed by the environment name: + +- With built-in environments: + +```js tab title="my-test.spec.js" +/** + * @jest-environment jsdom + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +```ts tab title="my-test.spec.ts" +/** + * @jest-environment jsdom + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +- With custom environment: + +```js tab title="my-test.spec.js" +/** + * @jest-environment ./my-custom-environment.js + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +```ts tab title="my-test.spec.ts" +/** + * @jest-environment ./my-custom-environment.ts + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +### Extending built-in Environments + +Jest allows you to extend the built-in environments, such as `NodeEnvironment` or `JSDOMEnvironment`, to create your own custom environment. This is useful when you want to add additional functionality or modify the behavior of the existing environments. + +Here is an example of how to extend the `NodeEnvironment`: + +```js tab title="custom-node-environment.js" +// An example of a custom Node environment + +const NodeEnvironment = require('jest-environment-node'); + +/** + * @implements {import('jest-environment-node').NodeEnvironment} + */ +class CustomNodeEnvironment extends NodeEnvironment { + constructor(config, context) { + super(config, context); + console.log(config.globalConfig); + console.log(config.projectConfig); + this.testPath = context.testPath; + this.docblockPragmas = context.docblockPragmas; + } + + async setup() { + await super.setup(); + await someSetupTasks(this.testPath); + this.global.someGlobalObject = createGlobalObject(); + + // Will trigger if docblock contains @my-custom-pragma my-pragma-value + if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { + // ... + } + } + + async teardown() { + this.global.someGlobalObject = destroyGlobalObject(); + await someTeardownTasks(); + await super.teardown(); + } + + getVmContext() { + return super.getVmContext(); + } + + async handleTestEvent(event, state) { + if (event.name === 'test_start') { + // ... + } + } +} + +module.exports = CustomEnvironment; +``` + +```ts tab title="custom-node-environment.ts" +// An example of a custom Node environment + +import NodeEnvironment from 'jest-environment-node'; + +export default class CustomNodeEnvironment extends NodeEnvironment { + constructor(config, context) { + super(config, context); + console.log(config.globalConfig); + console.log(config.projectConfig); + this.testPath = context.testPath; + this.docblockPragmas = context.docblockPragmas; + } + + async setup() { + await super.setup(); + await someSetupTasks(this.testPath); + this.global.someGlobalObject = createGlobalObject(); + + // Will trigger if docblock contains @my-custom-pragma my-pragma-value + if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { + // ... + } + } + + async teardown() { + this.global.someGlobalObject = destroyGlobalObject(); + await someTeardownTasks(); + await super.teardown(); + } + + getVmContext() { + return super.getVmContext(); + } + + async handleTestEvent(event, state) { + if (event.name === 'test_start') { + // ... + } + } +} +``` + +and declare in your Jest config + +```js tab title="jest.config.js" +const {defineConfig} = require('jest'); + +module.exports = defineConfig({ + testEnvironment: './custom-node-environment.js', +}); +``` + +```ts tab title="jest.config.ts" +import {defineConfig} from 'jest'; + +export default defineConfig({ + testEnvironment: './custom-node-environment.ts', +}); +``` + +:::tip + +Jest also provides `@jest/environment-jsdom-abstract` package to make it easier for you to compose your own custom test environment based on `jsdom` or use your own `jsdom` installed version. + +```js tab title="custom-jsdom-environment.js" +const JSDOMEnvironment = require('@jest/environment-jsdom-abstract'); +const jsdom = require('jsdom'); + +class CustomJSDOMEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context, jsdom); + } + + // Override methods to customize behavior +} + +module.exports = CustomJSDOMEnvironment; +``` + +```ts tab title="custom-jsdom-environment.ts" +import JSDOMEnvironment from '@jest/environment-jsdom-abstract'; +import jsdom from 'jsdom'; + +export default class CustomJSDOMEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context, jsdom); + } + + // Override methods to customize behavior +} +``` + +and declare in your Jest config + +```js tab title="jest.config.js" +const {defineConfig} = require('jest'); + +module.exports = defineConfig({ + testEnvironment: './custom-jsdom-environment.js', +}); +``` + +```ts tab title="jest.config.ts" +import {defineConfig} from 'jest'; + +export default defineConfig({ + testEnvironment: './custom-jsdom-environment.ts', +}); +``` + +::: + +### Custom Environment + +You can create your own package to extend Jest environment. To do so, create package with a name, or specify a path to a valid JS/TS file. That package should export an object with the shape of Environment: + +:::tip + +It's a best practice to name your custom environment with `jest-environment-` prefix, so that it is clearly identifiable as a Jest environment. + +::: + +```js tab title="environment.js" +/** + * @implements {import('@jest/environment').JestEnvironment} + */ +class CustomEnvironment { + // Implement the required methods here + + // Example of a method + getVmContext() { + return null; + } +} + +module.exports = CustomEnvironment; +``` + +```ts tab title="environment.ts" +import type {JestEnvironment} from '@jest/environment'; + +export default class CustomEnvironment implements JestEnvironment { + // Implement the required methods here + + // Example of a method + getVmContext() { + return null; + } +} +``` + +and declare in your Jest config + +```js tab title="jest.config.js" +const {defineConfig} = require('jest'); + +module.exports = defineConfig({ + testEnvironment: './environment.js', +}); +``` + +```ts tab title="jest.config.ts" +import {defineConfig} from 'jest'; + +export default defineConfig({ + testEnvironment: './environment.ts', +}); +``` + +## See Also + +- [Configuration - testEnvironment](Configuration.md#testenvironment-string) +- [Configuration - testEnvironmentOptions](Configuration.md#testenvironmentoptions-object) +- [JSDOM Documentation](https://github.com/jsdom/jsdom) diff --git a/website/sidebars.json b/website/sidebars.json index 3c39b1cc845e..eaecb7bfbc9d 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -12,6 +12,7 @@ ], "Guides": [ "snapshot-testing", + "test-environment", "tutorial-async", "timer-mocks", "manual-mocks", diff --git a/website/versioned_docs/version-29.7/Configuration.md b/website/versioned_docs/version-29.7/Configuration.md index 607fb51ccf46..0595115926b2 100644 --- a/website/versioned_docs/version-29.7/Configuration.md +++ b/website/versioned_docs/version-29.7/Configuration.md @@ -1918,15 +1918,17 @@ More about serializers API can be found [here](https://github.com/jestjs/jest/tr ::: -### `testEnvironment` \[string] +### `testEnvironment` \[node | jsdom | string] -Default: `"node"` +Default: `node` The test environment that will be used for testing. The default environment in Jest is a Node.js environment. If you are building a web app, you can use a browser-like environment through [`jsdom`](https://github.com/jsdom/jsdom) instead. By adding a `@jest-environment` docblock at the top of the file, you can specify another environment to be used for all tests in that file: -```js +- With built-in environments: + +```js tab title="my-test.spec.js" /** * @jest-environment jsdom */ @@ -1937,113 +1939,128 @@ 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. The constructor is passed [`globalConfig`](https://github.com/jestjs/jest/blob/v29.2.1/packages/jest-types/src/Config.ts#L358-L422) and [`projectConfig`](https://github.com/jestjs/jest/blob/v29.2.1/packages/jest-types/src/Config.ts#L424-L481) as its first argument, and [`testEnvironmentContext`](https://github.com/jestjs/jest/blob/491e7cb0f2daa8263caccc72d48bdce7ba759b11/packages/jest-environment/src/index.ts#L13) as its second. - -The class may optionally expose an asynchronous `handleTestEvent` method to bind to events fired by [`jest-circus`](https://github.com/jestjs/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/jestjs/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. +```ts tab title="my-test.spec.ts" +/** + * @jest-environment jsdom + */ -Any docblock pragmas in test files will be passed to the environment constructor and can be used for per-test configuration. If the pragma does not have a value, it will be present in the object with its value set to an empty string. If the pragma is not present, it will not be present in the object. +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -To use this class as your custom environment, refer to it by its full path within the project. For example, if your class is stored in `my-custom-environment.js` in some subfolder of your project, then the annotation might look like this: +- With custom environment: -```js +```js tab title="my-test.spec.js" /** - * @jest-environment ./src/test/my-custom-environment + * @jest-environment ./my-custom-environment.js */ -``` - -:::info -TestEnvironment is sandboxed. Each test suite will trigger setup/teardown in their own TestEnvironment. +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -::: +```ts tab title="my-test.spec.ts" +/** + * @jest-environment ./my-custom-environment.ts + */ -Example: +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -```js -// my-custom-environment -const NodeEnvironment = require('jest-environment-node').TestEnvironment; +You can also define custom environment. When non-builtin environment is used, Jest will try to load either the file path or the package name defined as value for `testEnvironment`. That file or the package should export an object with the shape of `JestEnvironment`: -class CustomEnvironment extends NodeEnvironment { +```js tab title="environment.js" +/** + * @implements {import('@jest/environment').JestEnvironment} + */ +class CustomEnvironment { constructor(config, context) { - super(config, context); - console.log(config.globalConfig); - console.log(config.projectConfig); - this.testPath = context.testPath; - this.docblockPragmas = context.docblockPragmas; - } - - async setup() { - await super.setup(); - await someSetupTasks(this.testPath); - this.global.someGlobalObject = createGlobalObject(); - - // Will trigger if docblock contains @my-custom-pragma my-pragma-value - if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { - // ... - } - } - - async teardown() { - this.global.someGlobalObject = destroyGlobalObject(); - await someTeardownTasks(); - await super.teardown(); + const {projectConfig} = config; + // Implement the constructor } + // Example of a method getVmContext() { - return super.getVmContext(); - } - - async handleTestEvent(event, state) { - if (event.name === 'test_start') { - // ... - } + return null; } + // Implement the required methods here } module.exports = CustomEnvironment; ``` -```js -// my-test-suite -/** - * @jest-environment ./my-custom-environment - */ -let someGlobalObject; +```ts tab title="environment.ts" +import type { + EnvironmentContext, + JestEnvironment, + JestEnvironmentConfig, +} from '@jest/environment'; -beforeAll(() => { - someGlobalObject = globalThis.someGlobalObject; -}); +export default class CustomEnvironment implements JestEnvironment { + constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { + const {projectConfig} = config; + // Implement the constructor + } + + // Example of a method + getVmContext() { + return null; + } + // Implement the required methods here +} ``` +Jest also exposes `builtinEnvironments` through `jest-environment-node` and `jest-environment-jsdom` packages, in case you just want to extend it. You can read more about extending environments in [our guide](./TestEnvironment.md). + ### `testEnvironmentOptions` \[Object] Default: `{}` -Test environment options that will be passed to the `testEnvironment`. The relevant options depend on the environment. +Test environment options that will be passed to the `testEnvironment`. The relevant options depend on the environment being used. -For example, you can override options passed to [`jsdom`](https://github.com/jsdom/jsdom): +#### Node Environment Options -```js tab +When using the `node` environment, you can configure various options that are passed to `runInContext`. These options include: + +- **`globalsCleanupMode`** (**'on'** | **'soft'** | **'off'**): Controls cleanup of global variables between tests. Default: `'soft'`. +- All the options listed in the [vm.runInContext](https://nodejs.org/api/vm.html#scriptrunincontextcontextifiedobject-options) documentation + +#### JSDOM Environment Options + +When using the `jsdom` environment, you can configure various options that are passed to [jsdom](https://github.com/jsdom/jsdom). These options include: + +- **`url`** - The URL of the page (affects `window.location` and relative URLs). Default: `"http://localhost"` +- **`userAgent`** - The user agent string. Default: a generic user agent +- All the options listed in the [jsdom](https://github.com/jsdom/jsdom) + +For example, you can override options passed to `jsdom`: + +```js tab title="jest.config.js" /** @type {import('jest').Config} */ -const config = { +module.exports = { testEnvironment: 'jsdom', testEnvironmentOptions: { - html: '', + html: '', url: 'https://jestjs.io/', userAgent: 'Agent/007', }, }; - -module.exports = config; ``` -```ts tab +```ts tab title="jest.config.ts" import type {Config} from 'jest'; const config: Config = { testEnvironment: 'jsdom', testEnvironmentOptions: { - html: '', + html: '', url: 'https://jestjs.io/', userAgent: 'Agent/007', }, @@ -2052,21 +2069,28 @@ const config: Config = { export default config; ``` -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']`. +#### Custom Export Conditions -```js tab +The `testEnvironmentOptions` allow specifying `customExportConditions`, which control which versions of a library are loaded from `exports` in `package.json`. + +The built-in environments have the following defaults: + +- `jest-environment-jsdom` defaults to `['browser']` +- `jest-environment-node` defaults to `['node', 'node-addons']` + +For example, you can override `customExportConditions` passed to `jsdom`: + +```js tab title="jest.config.js" /** @type {import('jest').Config} */ -const config = { +module.exports = { testEnvironment: 'jsdom', testEnvironmentOptions: { customExportConditions: ['react-native'], }, }; - -module.exports = config; ``` -```ts tab +```ts tab title="jest.config.ts" import type {Config} from 'jest'; const config: Config = { @@ -2092,6 +2116,20 @@ test('use jsdom and set the URL in this test file', () => { }); ``` +You can combine multiple options: + +```js +/** + * @jest-environment jsdom + * @jest-environment-options {"url": "https://example.com/", "userAgent": "Custom Agent"} + */ + +test('use custom URL and user agent', () => { + expect(window.location.href).toBe('https://example.com/'); + expect(window.navigator.userAgent).toContain('Custom Agent'); +}); +``` + ### `testFailureExitCode` \[number] Default: `1` diff --git a/website/versioned_docs/version-29.7/TestEnvironment.md b/website/versioned_docs/version-29.7/TestEnvironment.md new file mode 100644 index 000000000000..1b0a610160fc --- /dev/null +++ b/website/versioned_docs/version-29.7/TestEnvironment.md @@ -0,0 +1,243 @@ +--- +id: test-environment +title: Test Environment +--- + +Jest provides environment option to run code inside a specific environment. You can modify how environment behaves with `testEnvironmentOptions` option. + +By default, you can use these environments: + +- `node` is default environment +- `jsdom` emulates browser environment by providing Browser API, uses [`jsdom`](https://github.com/jsdom/jsdom) package + +## Environments for Specific Files + +When setting `testEnvironment` option in your config, it will apply to all the test files in your project. To have more fine-grained control, you can use control comments to specify environment for specific files. Control comments are comments that start with `@jest-environment` and are followed by the environment name: + +- With built-in environments: + +```js tab title="my-test.spec.js" +/** + * @jest-environment jsdom + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +```ts tab title="my-test.spec.ts" +/** + * @jest-environment jsdom + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +- With custom environment: + +```js tab title="my-test.spec.js" +/** + * @jest-environment ./my-custom-environment.js + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +```ts tab title="my-test.spec.ts" +/** + * @jest-environment ./my-custom-environment.ts + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +### Extending built-in Environments + +Jest allows you to extend the built-in environments, such as `NodeEnvironment` or `JSDOMEnvironment`, to create your own custom environment. This is useful when you want to add additional functionality or modify the behavior of the existing environments. + +Here is an example of how to extend the `NodeEnvironment`: + +```js tab title="custom-node-environment.js" +// An example of a custom Node environment + +const NodeEnvironment = require('jest-environment-node'); + +/** + * @implements {import('jest-environment-node').NodeEnvironment} + */ +class CustomNodeEnvironment extends NodeEnvironment { + constructor(config, context) { + super(config, context); + console.log(config.globalConfig); + console.log(config.projectConfig); + this.testPath = context.testPath; + this.docblockPragmas = context.docblockPragmas; + } + + async setup() { + await super.setup(); + await someSetupTasks(this.testPath); + this.global.someGlobalObject = createGlobalObject(); + + // Will trigger if docblock contains @my-custom-pragma my-pragma-value + if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { + // ... + } + } + + async teardown() { + this.global.someGlobalObject = destroyGlobalObject(); + await someTeardownTasks(); + await super.teardown(); + } + + getVmContext() { + return super.getVmContext(); + } + + async handleTestEvent(event, state) { + if (event.name === 'test_start') { + // ... + } + } +} + +module.exports = CustomEnvironment; +``` + +```ts tab title="custom-node-environment.ts" +// An example of a custom Node environment + +import NodeEnvironment from 'jest-environment-node'; + +export default class CustomNodeEnvironment extends NodeEnvironment { + constructor(config, context) { + super(config, context); + console.log(config.globalConfig); + console.log(config.projectConfig); + this.testPath = context.testPath; + this.docblockPragmas = context.docblockPragmas; + } + + async setup() { + await super.setup(); + await someSetupTasks(this.testPath); + this.global.someGlobalObject = createGlobalObject(); + + // Will trigger if docblock contains @my-custom-pragma my-pragma-value + if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { + // ... + } + } + + async teardown() { + this.global.someGlobalObject = destroyGlobalObject(); + await someTeardownTasks(); + await super.teardown(); + } + + getVmContext() { + return super.getVmContext(); + } + + async handleTestEvent(event, state) { + if (event.name === 'test_start') { + // ... + } + } +} +``` + +and declare in your Jest config + +```js tab title="jest.config.js" +/** @type {import('jest').Config} */ +module.exports = { + testEnvironment: './custom-node-environment.js', +}; +``` + +```ts tab title="jest.config.ts" +import type {Config} from 'jest'; + +const config: Config = { + testEnvironment: './custom-node-environment.ts', +}; + +export default config; +``` + +### Custom Environment + +You can create your own package to extend Jest environment. To do so, create package with a name, or specify a path to a valid JS/TS file. That package should export an object with the shape of Environment: + +:::tip + +It's a best practice to name your custom environment with `jest-environment-` prefix, so that it is clearly identifiable as a Jest environment. + +::: + +```js tab title="environment.js" +/** + * @implements {import('@jest/environment').JestEnvironment} + */ +class CustomEnvironment { + // Implement the required methods here + + // Example of a method + getVmContext() { + return null; + } +} + +module.exports = CustomEnvironment; +``` + +```ts tab title="environment.ts" +import type {JestEnvironment} from '@jest/environment'; + +export default class CustomEnvironment implements JestEnvironment { + // Implement the required methods here + + // Example of a method + getVmContext() { + return null; + } +} +``` + +and declare in your Jest config + +```js tab title="jest.config.js" +/** @type {import('jest').Config} */ +module.exports = { + testEnvironment: './environment.js', +}; +``` + +```ts tab title="jest.config.ts" +import type {Config} from 'jest'; + +const config: Config = { + testEnvironment: './environment.ts', +}; + +export default config; +``` + +## See Also + +- [Configuration - testEnvironment](Configuration.md#testenvironment-string) +- [Configuration - testEnvironmentOptions](Configuration.md#testenvironmentoptions-object) +- [JSDOM Documentation](https://github.com/jsdom/jsdom) diff --git a/website/versioned_docs/version-30.0/Configuration.md b/website/versioned_docs/version-30.0/Configuration.md index d40723aeaf5e..3b5ede50d354 100644 --- a/website/versioned_docs/version-30.0/Configuration.md +++ b/website/versioned_docs/version-30.0/Configuration.md @@ -1942,15 +1942,17 @@ More about serializers API can be found [here](https://github.com/jestjs/jest/tr ::: -### `testEnvironment` \[string] +### `testEnvironment` \[node | jsdom | string] -Default: `"node"` +Default: `node` The test environment that will be used for testing. The default environment in Jest is a Node.js environment. If you are building a web app, you can use a browser-like environment through [`jsdom`](https://github.com/jsdom/jsdom) instead. By adding a `@jest-environment` docblock at the top of the file, you can specify another environment to be used for all tests in that file: -```js +- With built-in environments: + +```js tab title="my-test.spec.js" /** * @jest-environment jsdom */ @@ -1961,113 +1963,130 @@ 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. The constructor is passed [`globalConfig`](https://github.com/jestjs/jest/blob/v29.2.1/packages/jest-types/src/Config.ts#L358-L422) and [`projectConfig`](https://github.com/jestjs/jest/blob/v29.2.1/packages/jest-types/src/Config.ts#L424-L481) as its first argument, and [`testEnvironmentContext`](https://github.com/jestjs/jest/blob/491e7cb0f2daa8263caccc72d48bdce7ba759b11/packages/jest-environment/src/index.ts#L13) as its second. - -The class may optionally expose an asynchronous `handleTestEvent` method to bind to events fired by [`jest-circus`](https://github.com/jestjs/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/jestjs/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. +```ts tab title="my-test.spec.ts" +/** + * @jest-environment jsdom + */ -Any docblock pragmas in test files will be passed to the environment constructor and can be used for per-test configuration. If the pragma does not have a value, it will be present in the object with its value set to an empty string. If the pragma is not present, it will not be present in the object. +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -To use this class as your custom environment, refer to it by its full path within the project. For example, if your class is stored in `my-custom-environment.js` in some subfolder of your project, then the annotation might look like this: +- With custom environment: -```js +```js tab title="my-test.spec.js" /** - * @jest-environment ./src/test/my-custom-environment + * @jest-environment ./my-custom-environment.js */ -``` - -:::info -TestEnvironment is sandboxed. Each test suite will trigger setup/teardown in their own TestEnvironment. +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -::: +```ts tab title="my-test.spec.ts" +/** + * @jest-environment ./my-custom-environment.ts + */ -Example: +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` -```js -// my-custom-environment -const NodeEnvironment = require('jest-environment-node').TestEnvironment; +You can also define custom environment. When non-builtin environment is used, Jest will try to load either the file path or the package name defined as value for `testEnvironment`. That file or the package should export an object with the shape of `JestEnvironment`: -class CustomEnvironment extends NodeEnvironment { +```js tab title="environment.js" +/** + * @implements {import('@jest/environment').JestEnvironment} + */ +class CustomEnvironment { constructor(config, context) { - super(config, context); - console.log(config.globalConfig); - console.log(config.projectConfig); - this.testPath = context.testPath; - this.docblockPragmas = context.docblockPragmas; - } - - async setup() { - await super.setup(); - await someSetupTasks(this.testPath); - this.global.someGlobalObject = createGlobalObject(); - - // Will trigger if docblock contains @my-custom-pragma my-pragma-value - if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { - // ... - } - } - - async teardown() { - this.global.someGlobalObject = destroyGlobalObject(); - await someTeardownTasks(); - await super.teardown(); + const {projectConfig} = config; + // Implement the constructor } + // Example of a method getVmContext() { - return super.getVmContext(); + return null; } - async handleTestEvent(event, state) { - if (event.name === 'test_start') { - // ... - } - } + // Implement the required methods here } module.exports = CustomEnvironment; ``` -```js -// my-test-suite -/** - * @jest-environment ./my-custom-environment - */ -let someGlobalObject; +```ts tab title="environment.ts" +import type { + EnvironmentContext, + JestEnvironment, + JestEnvironmentConfig, +} from '@jest/environment'; -beforeAll(() => { - someGlobalObject = globalThis.someGlobalObject; -}); +export default class CustomEnvironment implements JestEnvironment { + constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { + const {projectConfig} = config; + // Implement the constructor + } + + // Example of a method + getVmContext() { + return null; + } + + // Implement the required methods here +} ``` +Jest also exposes `builtinEnvironments` through `jest-environment-node` and `jest-environment-jsdom` packages, in case you just want to extend it. You can read more about extending environments in [our guide](TestEnvironment.md). + ### `testEnvironmentOptions` \[Object] Default: `{}` -Test environment options that will be passed to the `testEnvironment`. The relevant options depend on the environment. +Test environment options that will be passed to the `testEnvironment`. The relevant options depend on the environment being used. -For example, you can override options passed to [`jsdom`](https://github.com/jsdom/jsdom): +#### Node Environment Options -```js tab +When using the `node` environment, you can configure various options that are passed to `runInContext`. These options include: + +- **`globalsCleanup`** (**'on'** | **'soft'** | **'off'**): Controls cleanup of global variables between tests. Default: `'soft'`. +- All the options listed in the [vm.runInContext](https://nodejs.org/api/vm.html#scriptrunincontextcontextifiedobject-options) documentation + +#### JSDOM Environment Options + +When using the `jsdom` environment, you can configure various options that are passed to [jsdom](https://github.com/jsdom/jsdom). These options include: + +- **`url`** - The URL of the page (affects `window.location` and relative URLs). Default: `"http://localhost"` +- **`userAgent`** - The user agent string. Default: a generic user agent +- All the options listed in the [jsdom](https://github.com/jsdom/jsdom) + +For example, you can override options passed to `jsdom`: + +```js tab title="jest.config.js" /** @type {import('jest').Config} */ -const config = { +module.exports = { testEnvironment: 'jsdom', testEnvironmentOptions: { - html: '', + html: '', url: 'https://jestjs.io/', userAgent: 'Agent/007', }, }; - -module.exports = config; ``` -```ts tab +```ts tab title="jest.config.ts" import type {Config} from 'jest'; const config: Config = { testEnvironment: 'jsdom', testEnvironmentOptions: { - html: '', + html: '', url: 'https://jestjs.io/', userAgent: 'Agent/007', }, @@ -2076,21 +2095,28 @@ const config: Config = { export default config; ``` -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']`. +#### Custom Export Conditions -```js tab +The `testEnvironmentOptions` allow specifying `customExportConditions`, which control which versions of a library are loaded from `exports` in `package.json`. + +The built-in environments have the following defaults: + +- `jest-environment-jsdom` defaults to `['browser']` +- `jest-environment-node` defaults to `['node', 'node-addons']` + +For example, you can override `customExportConditions` passed to `jsdom`: + +```js tab title="jest.config.js" /** @type {import('jest').Config} */ -const config = { +module.exports = { testEnvironment: 'jsdom', testEnvironmentOptions: { customExportConditions: ['react-native'], }, }; - -module.exports = config; ``` -```ts tab +```ts tab title="jest.config.ts" import type {Config} from 'jest'; const config: Config = { @@ -2116,6 +2142,20 @@ test('use jsdom and set the URL in this test file', () => { }); ``` +You can combine multiple options: + +```js +/** + * @jest-environment jsdom + * @jest-environment-options {"url": "https://example.com/", "userAgent": "Custom Agent"} + */ + +test('use custom URL and user agent', () => { + expect(window.location.href).toBe('https://example.com/'); + expect(window.navigator.userAgent).toContain('Custom Agent'); +}); +``` + ### `testFailureExitCode` \[number] Default: `1` diff --git a/website/versioned_docs/version-30.0/TestEnvironment.md b/website/versioned_docs/version-30.0/TestEnvironment.md new file mode 100644 index 000000000000..9b9ad02d29c6 --- /dev/null +++ b/website/versioned_docs/version-30.0/TestEnvironment.md @@ -0,0 +1,296 @@ +--- +id: test-environment +title: Test Environment +--- + +Jest provides environment option to run code inside a specific environment. You can modify how environment behaves with `testEnvironmentOptions` option. + +By default, you can use these environments: + +- `node` is default environment +- `jsdom` emulates browser environment by providing Browser API, uses [`jsdom`](https://github.com/jsdom/jsdom) package + +## Environments for Specific Files + +When setting `testEnvironment` option in your config, it will apply to all the test files in your project. To have more fine-grained control, you can use control comments to specify environment for specific files. Control comments are comments that start with `@jest-environment` and are followed by the environment name: + +- With built-in environments: + +```js tab title="my-test.spec.js" +/** + * @jest-environment jsdom + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +```ts tab title="my-test.spec.ts" +/** + * @jest-environment jsdom + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +- With custom environment: + +```js tab title="my-test.spec.js" +/** + * @jest-environment ./my-custom-environment.js + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +```ts tab title="my-test.spec.ts" +/** + * @jest-environment ./my-custom-environment.ts + */ + +test('use jsdom in this test file', () => { + const element = document.createElement('div'); + expect(element).not.toBeNull(); +}); +``` + +### Extending built-in Environments + +Jest allows you to extend the built-in environments, such as `NodeEnvironment` or `JSDOMEnvironment`, to create your own custom environment. This is useful when you want to add additional functionality or modify the behavior of the existing environments. + +Here is an example of how to extend the `NodeEnvironment`: + +```js tab title="custom-node-environment.js" +// An example of a custom Node environment + +const NodeEnvironment = require('jest-environment-node'); + +/** + * @implements {import('jest-environment-node').NodeEnvironment} + */ +class CustomNodeEnvironment extends NodeEnvironment { + constructor(config, context) { + super(config, context); + console.log(config.globalConfig); + console.log(config.projectConfig); + this.testPath = context.testPath; + this.docblockPragmas = context.docblockPragmas; + } + + async setup() { + await super.setup(); + await someSetupTasks(this.testPath); + this.global.someGlobalObject = createGlobalObject(); + + // Will trigger if docblock contains @my-custom-pragma my-pragma-value + if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { + // ... + } + } + + async teardown() { + this.global.someGlobalObject = destroyGlobalObject(); + await someTeardownTasks(); + await super.teardown(); + } + + getVmContext() { + return super.getVmContext(); + } + + async handleTestEvent(event, state) { + if (event.name === 'test_start') { + // ... + } + } +} + +module.exports = CustomEnvironment; +``` + +```ts tab title="custom-node-environment.ts" +// An example of a custom Node environment + +import NodeEnvironment from 'jest-environment-node'; + +export default class CustomNodeEnvironment extends NodeEnvironment { + constructor(config, context) { + super(config, context); + console.log(config.globalConfig); + console.log(config.projectConfig); + this.testPath = context.testPath; + this.docblockPragmas = context.docblockPragmas; + } + + async setup() { + await super.setup(); + await someSetupTasks(this.testPath); + this.global.someGlobalObject = createGlobalObject(); + + // Will trigger if docblock contains @my-custom-pragma my-pragma-value + if (this.docblockPragmas['my-custom-pragma'] === 'my-pragma-value') { + // ... + } + } + + async teardown() { + this.global.someGlobalObject = destroyGlobalObject(); + await someTeardownTasks(); + await super.teardown(); + } + + getVmContext() { + return super.getVmContext(); + } + + async handleTestEvent(event, state) { + if (event.name === 'test_start') { + // ... + } + } +} +``` + +and declare in your Jest config + +```js tab title="jest.config.js" +/** @type {import('jest').Config} */ +module.exports = { + testEnvironment: './custom-node-environment.js', +}; +``` + +```ts tab title="jest.config.ts" +import type {Config} from 'jest'; + +const config: Config = { + testEnvironment: './custom-node-environment.ts', +}; + +export default config; +``` + +:::tip + +Jest also provides `@jest/environment-jsdom-abstract` package to make it easier for you to compose your own custom test environment based on `jsdom` or use your own `jsdom` installed version. + +```js tab title="custom-jsdom-environment.js" +const JSDOMEnvironment = require('@jest/environment-jsdom-abstract'); +const jsdom = require('jsdom'); + +class CustomJSDOMEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context, jsdom); + } + + // Override methods to customize behavior +} + +module.exports = CustomJSDOMEnvironment; +``` + +```ts tab title="custom-jsdom-environment.ts" +import JSDOMEnvironment from '@jest/environment-jsdom-abstract'; +import jsdom from 'jsdom'; + +export default class CustomJSDOMEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context, jsdom); + } + + // Override methods to customize behavior +} +``` + +and declare in your Jest config + +```js tab title="jest.config.js" +/** @type {import('jest').Config} */ +module.exports = { + testEnvironment: './custom-jsdom-environment.js', +}; +``` + +```ts tab title="jest.config.ts" +import type {Config} from 'jest'; + +const config: Config = { + testEnvironment: './custom-jsdom-environment.ts', +}; + +export default config; +``` + +::: + +### Custom Environment + +You can create your own package to extend Jest environment. To do so, create package with a name, or specify a path to a valid JS/TS file. That package should export an object with the shape of Environment: + +:::tip + +It's a best practice to name your custom environment with `jest-environment-` prefix, so that it is clearly identifiable as a Jest environment. + +::: + +```js tab title="environment.js" +/** + * @implements {import('@jest/environment').JestEnvironment} + */ +class CustomEnvironment { + // Implement the required methods here + + // Example of a method + getVmContext() { + return null; + } +} + +module.exports = CustomEnvironment; +``` + +```ts tab title="environment.ts" +import type {JestEnvironment} from '@jest/environment'; + +export default class CustomEnvironment implements JestEnvironment { + // Implement the required methods here + + // Example of a method + getVmContext() { + return null; + } +} +``` + +and declare in your Jest config + +```js tab title="jest.config.js" +/** @type {import('jest').Config} */ +module.exports = { + testEnvironment: './environment.js', +}; +``` + +```ts tab title="jest.config.ts" +import type {Config} from 'jest'; + +const config: Config = { + testEnvironment: './environment.ts', +}; + +export default config; +``` + +## See Also + +- [Configuration - testEnvironment](Configuration.md#testenvironment-string) +- [Configuration - testEnvironmentOptions](Configuration.md#testenvironmentoptions-object) +- [JSDOM Documentation](https://github.com/jsdom/jsdom) diff --git a/website/versioned_sidebars/version-29.7-sidebars.json b/website/versioned_sidebars/version-29.7-sidebars.json index 72f8506cbd5d..cc7c3b3e879d 100644 --- a/website/versioned_sidebars/version-29.7-sidebars.json +++ b/website/versioned_sidebars/version-29.7-sidebars.json @@ -12,6 +12,7 @@ ], "Guides": [ "snapshot-testing", + "test-environment", "tutorial-async", "timer-mocks", "manual-mocks", diff --git a/website/versioned_sidebars/version-30.0-sidebars.json b/website/versioned_sidebars/version-30.0-sidebars.json index db604420e92f..873e5effd76c 100644 --- a/website/versioned_sidebars/version-30.0-sidebars.json +++ b/website/versioned_sidebars/version-30.0-sidebars.json @@ -12,6 +12,7 @@ ], "Guides": [ "snapshot-testing", + "test-environment", "tutorial-async", "timer-mocks", "manual-mocks",