ECMAScript Modules (ESM) Testing using Jest
When testing my ECMAScript Modules (ESM) using Jest, I encountered an error stating SyntaxError: Cannot use import statement outside a module
.
It is because your testing target uses other modules using import
keyword.
This error can be resolved with the Jest experimental support.
Jest ships with experimental support for ECMAScript Modules (ESM).
Using an example ESM package, this post will show you how to resolve based on the official documentation.
Initializing Project
Creating ESM Package
Run the following command to create an example ESM package.
mkdir jest-esm && cd jest-esm
npm init # Run with all options default
Install the following packages.
npm i -D typescript jest @types/jest ts-node ts-jest
The generated package.json
looks like this.
{
"name": "jest-esm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
Add "type": "module"
to the package.json
.
@@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
+ "type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
Configuring TypeScript
Run the following command to create a tsconfig.json
.
npx tsc --init
Modify the tsconfig.json
as follows:
@@ -14 +14 @@
- "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
@@ -28 +28 @@
- "module": "commonjs", /* Specify what module code is generated. */
+ "module": "es6", /* Specify what module code is generated. */
@@ -30 +30 @@
- // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
+ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
Configuring Jest
Run the following command to generate a basic configuration file (jest.config.ts
).
npm init jest@latest
The following questions will help Jest to create a suitable configuration for your project
✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … yes
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … no
✔ Which provider should be used to instrument code for coverage? › v8
✔ Automatically clear mock calls, instances, contexts and results before every test? … no
The table below describes the specified options.
Option | Value |
---|---|
use Jest when running “test” script in “package.json” | yes |
use Typescript for the configuration file | yes |
test environment | jsdom (browser-like) |
add coverage reports | no |
provider for coverage | v8 |
Automatically clear mock calls, instances, contexts and results | no |
Configuring ts-jest
Set ts-jest
to preset
in jest.config.ts
, which is described in the official documentation.
Instead, add the line: preset: “ts-jest” to the jest.config.js file afterwards.
@@ -102,7 +102,7 @@
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
- // preset: undefined,
+ preset: 'ts-jest',
// Run tests from one or more projects
// projects: undefined,
Supporting ESM on Jest
As of February 2024, the Jest ESM support feature is experimental. For more information, please refer to the official documentation.
Jest ships with experimental support for ECMAScript Modules (ESM).
The implementation may have bugs and lack features. For the latest status check out the issue and the label on the issue tracker.
Also note that the APIs Jest uses to implement ESM support are still considered experimental by Node (as of version 18.8.0).
Modify the package.json
as follows:
@@ -5,7 +5,7 @@
"main": "index.js",
"type": "module",
"scripts": {
- "test": "jest"
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"author": "",
"license": "ISC",
Then, add extensionsToTreatAsEsm
to the jest.config.ts
.
@@ -194,6 +194,8 @@
// Whether to use watchman for file crawling
// watchman: true,
+
+ extensionsToTreatAsEsm: ['.ts'],
};
export default config;
Supporting ESM on ts-jest
Based on the official documentation, update the jest.config.ts
as follows:
@@ -90,7 +90,9 @@
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
- // moduleNameMapper: {},
+ moduleNameMapper: {
+ '^(\\.{1,2}/.*)\\.js$': '$1',
+ },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
@@ -175,7 +177,14 @@
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
- // transform: undefined,
+ transform: {
+ '^.+\\.tsx?$': [
+ 'ts-jest',
+ {
+ useESM: true,
+ },
+ ],
+ },
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
Creating ESM
Installing hast-util-from-html
As a third-party ESM, install hast-util-from-html
.
npm i hast-util-from-html
@@ -15,5 +15,8 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
+ },
+ "dependencies": {
+ "hast-util-from-html": "^2.0.1"
}
}
Check the node_modules/hast-util-from-html/index.js
.
The module is exported by the keyword export
.
/**
* @typedef {import('hast-util-from-parse5')} DoNotTouchItRegistersData
*
* @typedef {import('./lib/index.js').ErrorCode} ErrorCode
* @typedef {import('./lib/index.js').ErrorSeverity} ErrorSeverity
* @typedef {import('./lib/index.js').OnError} OnError
* @typedef {import('./lib/index.js').Options} Options
*/
export {fromHtml} from './lib/index.js'
ESM
Create the index.ts
with the following content.
It imports fromHtml
from hast-util-from-html
.
import { fromHtml } from 'hast-util-from-html';
export default function JestEsm(): void {
const root = fromHtml(
'<span><a href="https://github.com">GitHub</a></span>',
{ fragment: true },
);
console.info(root);
}
As a test code, add the index.spec.ts
with the following content.
import JestEsm from './index';
test('case1', () => {
JestEsm();
});
Testing ESM
Run the following command to test the module.
npm run test
> jest-esm@1.0.0 test
> jest
● Validation Error:
Test environment jest-environment-jsdom cannot be found. Make sure the testEnvironment configuration option points to an existing node module.
Configuration Documentation:
https://jestjs.io/docs/configuration
As of Jest 28 "jest-environment-jsdom" is no longer shipped by default, make sure to install it separately.
Based on the output information, please install jest-environment-jsdom
.
npm i -D jest-environment-jsdom
@@ -12,6 +12,7 @@
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
Run the test again and you will see the test done successfully.
npm run test
> jest-esm@1.0.0 test
> node --experimental-vm-modules node_modules/jest/bin/jest.js
console.info
{
type: 'root',
children: [
{
type: 'element',
tagName: 'span',
properties: {},
children: [Array],
position: [Object]
}
],
data: { quirksMode: false },
position: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 53, offset: 52 }
}
}
at JestEsm (index.ts:8:11)
(node:47304) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
PASS ./index.spec.ts
✓ case1 (20 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1 s
Ran all test suites.