diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f621f66b734..1d6c19c7c57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - `[jest-core]` Run failed tests interactively the same way we do with snapshots ([#10858](https://github.com/facebook/jest/pull/10858)) - `[jest-core]` more `TestSequencer` methods can be async ([#10980](https://github.com/facebook/jest/pull/10980)) - `[jest-core]` Add support for `testSequencer` written in ESM ([#11207](https://github.com/facebook/jest/pull/11207)) +- `[jest-core]` Add support for `globalSetup` and `globalTeardown` written in ESM ([#11267](https://github.com/facebook/jest/pull/11267)) - `[jest-environment-node]` Add AbortController to globals ([#11182](https://github.com/facebook/jest/pull/11182)) - `[@jest/fake-timers]` Update to `@sinonjs/fake-timers` to v7 ([#11198](https://github.com/facebook/jest/pull/11198)) - `[jest-haste-map]` Handle injected scm clocks ([#10966](https://github.com/facebook/jest/pull/10966)) diff --git a/e2e/__tests__/globalSetup.test.ts b/e2e/__tests__/globalSetup.test.ts index 04e08bd4fa0d..bb104b6a4e4d 100644 --- a/e2e/__tests__/globalSetup.test.ts +++ b/e2e/__tests__/globalSetup.test.ts @@ -8,6 +8,7 @@ import {tmpdir} from 'os'; import * as path from 'path'; import * as fs from 'graceful-fs'; +import {onNodeVersions} from '@jest/test-utils'; import { cleanup, createEmptyPackage, @@ -192,3 +193,13 @@ test('properly handle rejections', () => { expect(stderr).toContain('Error: Jest: Got error running globalSetup'); expect(stderr).toContain('reason: undefined'); }); + +onNodeVersions('^12.17.0 || >=13.2.0', () => { + test('globalSetup works with ESM modules', () => { + const {exitCode} = runJest('global-setup-esm', [`--no-cache`], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); + + expect(exitCode).toBe(0); + }); +}); diff --git a/e2e/__tests__/globalTeardown.test.ts b/e2e/__tests__/globalTeardown.test.ts index 200faff130f4..5f9a9ef78813 100644 --- a/e2e/__tests__/globalTeardown.test.ts +++ b/e2e/__tests__/globalTeardown.test.ts @@ -8,6 +8,7 @@ import {tmpdir} from 'os'; import * as path from 'path'; import * as fs from 'graceful-fs'; +import {onNodeVersions} from '@jest/test-utils'; import {createDirectory} from 'jest-util'; import {cleanup, runYarnInstall} from '../Utils'; import runJest, {json as runWithJson} from '../runJest'; @@ -131,3 +132,13 @@ test('globalTeardown throws with named export', () => { `globalTeardown file must export a function at ${teardownPath}`, ); }); + +onNodeVersions('^12.17.0 || >=13.2.0', () => { + test('globalTeardown works with ESM modules', () => { + const {exitCode} = runJest('global-teardown-esm', [`--no-cache`], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); + + expect(exitCode).toBe(0); + }); +}); diff --git a/e2e/global-setup-esm/__tests__/test.js b/e2e/global-setup-esm/__tests__/test.js new file mode 100644 index 000000000000..2a9dc9e0c339 --- /dev/null +++ b/e2e/global-setup-esm/__tests__/test.js @@ -0,0 +1,20 @@ +/** + * 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 os from 'os'; +import path from 'path'; +import fs from 'graceful-fs'; +import greeting from '../'; + +const DIR = path.join(os.tmpdir(), 'jest-global-setup-esm'); + +test('should exist setup file', () => { + const files = fs.readdirSync(DIR); + expect(files).toHaveLength(1); + const setup = fs.readFileSync(path.join(DIR, files[0]), 'utf8'); + expect(setup).toBe('setup'); +}); diff --git a/e2e/global-setup-esm/index.js b/e2e/global-setup-esm/index.js new file mode 100644 index 000000000000..6780e1013e20 --- /dev/null +++ b/e2e/global-setup-esm/index.js @@ -0,0 +1,8 @@ +/** + * 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. + */ + +export default 'hello!'; diff --git a/e2e/global-setup-esm/package.json b/e2e/global-setup-esm/package.json new file mode 100644 index 000000000000..5771bc560de0 --- /dev/null +++ b/e2e/global-setup-esm/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "jest": { + "testEnvironment": "node", + "globalSetup": "/setup.js", + "transform": {} + } +} diff --git a/e2e/global-setup-esm/setup.js b/e2e/global-setup-esm/setup.js new file mode 100644 index 000000000000..144cca53cf80 --- /dev/null +++ b/e2e/global-setup-esm/setup.js @@ -0,0 +1,24 @@ +/** + * 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 crypto from 'crypto'; +import os from 'os'; +import path from 'path'; +import fs from 'graceful-fs'; +import jestUtil from 'jest-util'; + +const {createDirectory} = jestUtil; + +const DIR = path.join(os.tmpdir(), 'jest-global-setup-esm'); + +export default function () { + return new Promise(resolve => { + createDirectory(DIR); + const fileId = crypto.randomBytes(20).toString('hex'); + fs.writeFileSync(path.join(DIR, fileId), 'setup'); + resolve(); + }); +} diff --git a/e2e/global-teardown-esm/__tests__/test.js b/e2e/global-teardown-esm/__tests__/test.js new file mode 100644 index 000000000000..6c5dfb67b485 --- /dev/null +++ b/e2e/global-teardown-esm/__tests__/test.js @@ -0,0 +1,17 @@ +/** + * 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 os from 'os'; +import path from 'path'; +import fs from 'graceful-fs'; +import greeting from '../'; + +const DIR = path.join(os.tmpdir(), 'jest-global-teardown-esm'); + +test('should not exist teardown file', () => { + expect(fs.existsSync(DIR)).toBe(false); +}); diff --git a/e2e/global-teardown-esm/index.js b/e2e/global-teardown-esm/index.js new file mode 100644 index 000000000000..6780e1013e20 --- /dev/null +++ b/e2e/global-teardown-esm/index.js @@ -0,0 +1,8 @@ +/** + * 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. + */ + +export default 'hello!'; diff --git a/e2e/global-teardown-esm/package.json b/e2e/global-teardown-esm/package.json new file mode 100644 index 000000000000..835bbfd6931e --- /dev/null +++ b/e2e/global-teardown-esm/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "jest": { + "testEnvironment": "node", + "globalTeardown": "/teardown.js", + "transform": {} + } +} diff --git a/e2e/global-teardown-esm/teardown.js b/e2e/global-teardown-esm/teardown.js new file mode 100644 index 000000000000..314c4bdf6c44 --- /dev/null +++ b/e2e/global-teardown-esm/teardown.js @@ -0,0 +1,24 @@ +/** + * 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 crypto from 'crypto'; +import os from 'os'; +import path from 'path'; +import fs from 'graceful-fs'; +import jestUtil from 'jest-util'; + +const {createDirectory} = jestUtil; + +const DIR = path.join(os.tmpdir(), 'jest-global-teardown-esm'); + +export default function () { + return new Promise(resolve => { + createDirectory(DIR); + const fileId = crypto.randomBytes(20).toString('hex'); + fs.writeFileSync(path.join(DIR, fileId), 'teardown'); + resolve(); + }); +} diff --git a/packages/jest-core/src/runGlobalHook.ts b/packages/jest-core/src/runGlobalHook.ts index 900749b68b9d..10e817e4829c 100644 --- a/packages/jest-core/src/runGlobalHook.ts +++ b/packages/jest-core/src/runGlobalHook.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import {pathToFileURL} from 'url'; import * as util from 'util'; import pEachSeries = require('p-each-series'); import {createScriptTransformer} from '@jest/transform'; @@ -60,18 +61,41 @@ export default async ({ await globalModule(globalConfig); }); } catch (error) { - if (util.types.isNativeError(error)) { - error.message = `Jest: Got error running ${moduleName} - ${modulePath}, reason: ${error.message}`; + if (error && error.code === 'ERR_REQUIRE_ESM') { + const configUrl = pathToFileURL(modulePath); - throw error; - } + // node `import()` supports URL, but TypeScript doesn't know that + const importedConfig = await import(configUrl.href); + + if (!importedConfig.default) { + throw new Error( + `Jest: Failed to load ESM transformer at ${modulePath} - did you use a default export?`, + ); + } + + const globalModule = importedConfig.default; + + if (typeof globalModule !== 'function') { + throw new TypeError( + `${moduleName} file must export a function at ${modulePath}`, + ); + } + + await globalModule(globalConfig); + } else { + if (util.types.isNativeError(error)) { + error.message = `Jest: Got error running ${moduleName} - ${modulePath}, reason: ${error.message}`; + + throw error; + } - throw new Error( - `Jest: Got error running ${moduleName} - ${modulePath}, reason: ${prettyFormat( - error, - {maxDepth: 3}, - )}`, - ); + throw new Error( + `Jest: Got error running ${moduleName} - ${modulePath}, reason: ${prettyFormat( + error, + {maxDepth: 3}, + )}`, + ); + } } }); }