Fix Jest/Puppeteer Import Failures: 5 Proven Steps 2025
Struggling with Jest and Puppeteer import errors? Learn 5 proven steps to fix `SyntaxError` and module conflicts in 2025. A dev's guide to ESM vs. CJS.
Daniel Petroff
Senior Frontend Engineer specializing in test automation, build tooling, and modern JavaScript.
That Sinking Feeling: When `import puppeteer` Crashes Your Tests
You’ve been there. You’re ready to write some powerful end-to-end tests. You’ve installed Jest, you’ve installed Puppeteer, and you write that first beautiful line: import puppeteer from 'puppeteer';
. You run your test with confidence, only to be greeted by a wall of angry red text. Maybe it’s a SyntaxError: Cannot use import statement outside a module
, or perhaps a cryptic message about unexpected tokens. Your momentum grinds to a halt, and you find yourself spiraling down a rabbit hole of GitHub issues and outdated Stack Overflow answers.
This is one of the most common friction points for developers combining Jest and Puppeteer in 2025. The core of the problem isn’t a bug in either library; it’s a fundamental mismatch in how they handle JavaScript modules. As the JavaScript ecosystem has decisively moved towards ES Modules (ESM), legacy configurations built around CommonJS (CJS) are showing their age. Since version 22, Puppeteer itself is now an ESM-only package, meaning it no longer supports the old require('puppeteer')
syntax out of the box in many setups.
Don't worry, this is completely fixable. In this guide, we'll walk you through five proven, up-to-date steps to resolve these import failures for good. We’ll demystify the ESM vs. CJS conflict and give you the exact configurations you need to get back to what you do best: writing great tests.
Table of Contents
Step 1: Diagnose the Root Cause: ESM vs. CommonJS
Before you copy-paste any code, it’s crucial to understand why this error happens. The entire issue boils down to the difference between two module systems in Node.js:
- CommonJS (CJS): The traditional Node.js system. It uses
require()
to import modules andmodule.exports
to export them. It's synchronous. - ES Modules (ESM): The official standard for JavaScript, used in browsers and modern Node.js. It uses
import ... from ...
andexport
statements. It's asynchronous.
By default, Jest has historically operated in a CommonJS-like environment. When it sees an import
statement, it often doesn't know what to do unless it's been specifically configured to handle it. The conflict intensifies because Puppeteer v22+ is ESM-only. You cannot require()
it anymore in a standard CJS file.
Quick Comparison: CJS vs. ESM
Feature | CommonJS (CJS) | ES Modules (ESM) |
---|---|---|
Import Syntax | const myModule = require('my-module'); | import myModule from 'my-module'; |
Export Syntax | module.exports = { ... }; | export default { ... }; or export const ...; |
Loading | Synchronous | Asynchronous |
Node.js Default | For .js files, unless "type": "module" is in package.json | For .mjs files, or .js files if "type": "module" is set |
Puppeteer v22+ | Not directly supported | Required |
The error message you see is your biggest clue:
SyntaxError: Cannot use import statement outside a module
: You are trying to useimport
in a file that Node.js and Jest are treating as CommonJS.ReferenceError: require is not defined
: You are trying to userequire()
in a file that is being treated as an ES Module (e.g., an.mjs
file or after setting"type": "module"
).
With this understanding, let's move on to the fix.
Step 2: The Modern Fix: Embrace ES Modules in Your Project
The most robust, future-proof solution is to configure your entire test environment to work with ES Modules natively. This involves telling Node.js, Jest, and Babel (a JavaScript compiler) that you're all-in on ESM.
Part A: Set Your Project's Module Type
The first step is to signal to Node.js that your project's .js
files should be treated as ES Modules. You do this with one simple line in your package.json
:
// package.json
{
"name": "my-puppeteer-project",
"version": "1.0.0",
"type": "module",
// ... other properties
}
By adding "type": "module"
, you've flipped the default. Now, you must use import
/export
syntax in all your .js
files.
Part B: Configure Jest and Babel to Transpile Code
Even with "type": "module"
, Jest needs help transforming this modern syntax into something it can execute. This is where Babel comes in. First, install the necessary development dependencies:
npm install --save-dev jest babel-jest @babel/core @babel/preset-env
Next, create a Babel configuration file, babel.config.js
, in your project root:
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
],
};
Finally, configure Jest to use Babel. Create a jest.config.js
file:
// jest.config.js
export default {
// A preset that is recommended for Node.js projects
preset: 'ts-jest/presets/default-esm', // or another ESM-compatible preset
// Use Babel to transform files
transform: {
'^.+\\.m?js$': 'babel-jest',
},
// Indicates that the module system is ES Modules
moduleFileExtensions: ['js', 'mjs'],
testEnvironment: 'node',
// This is crucial for Puppeteer's ESM build
// It tells Jest to not transform Puppeteer's own code
transformIgnorePatterns: [
'/node_modules/(?!(puppeteer|another-es-module))',
],
};
This setup tells Jest: "Hey, we're using ES Modules. When you encounter a .js
or .mjs
file, use Babel to transform it according to our babel.config.js
, but please ignore the node_modules
directory *except* for Puppeteer, which needs its ESM syntax preserved."
Step 3: The Streamlined Path: Use the `jest-puppeteer` Preset
If the manual configuration feels a bit heavy, the jest-puppeteer
library offers a fantastic, streamlined alternative. It handles a lot of the boilerplate setup for you, including managing the browser instance.
First, install the preset:
npm install --save-dev jest-puppeteer puppeteer
Next, update your jest.config.js
to use the preset. This replaces much of the manual setup:
// jest.config.js
export default {
preset: 'jest-puppeteer',
// You might still need the transform section if you use ESM syntax in your own test files
transform: {
'^.+\\.m?js$': 'babel-jest',
},
// Ensure you've set up babel.config.js as shown in Step 2!
};
The real magic of jest-puppeteer
is that it automatically provides global browser
and page
objects in your test files, so you don't even need to import Puppeteer yourself!
Your test file can look as simple as this:
// my-test.spec.js
describe('Google Homepage', () => {
beforeAll(async () => {
await page.goto('https://google.com');
});
it('should be titled "Google"', async () => {
await expect(page.title()).resolves.toMatch('Google');
});
});
Notice there's no import puppeteer from 'puppeteer'
. The preset handles the setup and teardown of the browser, making your tests cleaner and solving the import problem by abstracting it away.
Step 4: The Workaround: Dynamic `import()` for Mixed Projects
What if you're stuck in a large, legacy CommonJS project and can't just switch everything to ESM? In this case, you can use a dynamic import()
. This is an `async` function-like expression that loads an ES Module on demand.
This approach allows you to keep your project as CommonJS (no "type": "module"
) but still pull in the ESM-only Puppeteer library.
Here’s how you’d structure your test file:
// my-test.spec.js (in a CommonJS project)
describe('My Puppeteer Test', () => {
let browser;
let page;
beforeAll(async () => {
// Dynamically import Puppeteer
const puppeteer = (await import('puppeteer')).default;
browser = await puppeteer.launch();
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
it('should load a page successfully', async () => {
await page.goto('https://example.com');
const heading = await page.$eval('h1', el => el.textContent);
expect(heading).toBe('Example Domain');
});
});
This is a powerful workaround because it isolates the ESM dependency. However, it's more verbose and should be considered a bridge solution while planning a full migration to ESM.
Step 5: The Sanity Check: Verify Your Environment and Dependencies
If you've tried the steps above and are still facing issues, it's time for a sanity check. These simple steps can solve a surprising number of problems.
- Check Your Node.js Version: Native ESM support has evolved. Ensure you are using an active LTS version of Node.js (v18.x, v20.x, or newer is highly recommended for 2025). Run
node -v
to check. - Clear Your Caches: Corrupted dependencies or a messy `node_modules` folder can cause phantom issues. The classic "nuke and pave" approach often works wonders:
# Remove all installed packages and the lock file
rm -rf node_modules package-lock.json
# Reinstall everything from scratch
npm install
jest-puppeteer
, make sure you aren't also trying to set up a browser instance manually in a global setup file. Let the preset do its job.Conclusion: Embrace the Modules
Navigating the world of JavaScript modules can feel complex, but the conflict between Jest and Puppeteer is a classic symptom of the ecosystem's transition to a modern standard. The core takeaway is this: align your tools. By telling your project, Jest, and Babel to speak the same language (ESM), you eliminate the source of the conflict.
To recap:
- For new projects, start with Step 2: set
"type": "module"
and configure Jest/Babel for a robust, modern setup. - For the easiest setup, use Step 3: the `jest-puppeteer` preset abstracts away the complexity.
- For legacy projects, use Step 4: the dynamic `import()` is your escape hatch.
By following these steps, you can turn frustrating configuration errors into a solid, reliable testing foundation. Happy testing!