ECMAScript Modules (ESM) Testing using Jest

ECMAScript Modules (ESM) Testing using Jest

Takahiro Iwasa
(岩佐 孝浩)
Takahiro Iwasa (岩佐 孝浩)
7 min read
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.

OptionValue
use Jest when running “test” script in “package.json”yes
use Typescript for the configuration fileyes
test environmentjsdom (browser-like)
add coverage reportsno
provider for coveragev8
Automatically clear mock calls, instances, contexts and resultsno

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

> [email protected] 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

> [email protected] 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.
Takahiro Iwasa
(岩佐 孝浩)

Takahiro Iwasa (岩佐 孝浩)

Software Developer at iret, Inc.
Architecting and developing cloud native applications mainly with AWS. Japan AWS Top Engineers 2020-2023