2025 Jest/Puppeteer Helper Import: The Ultimate Guide
Tired of boilerplate? Master the 2025 way to import Jest & Puppeteer helpers. This guide simplifies your E2E testing setup for cleaner, faster, and more maintainable code.
Alex Miller
Senior Full-Stack Engineer specializing in test automation, CI/CD, and modern JavaScript frameworks.
2025 Jest/Puppeteer Helper Import: The Ultimate Guide
Does your Jest/Puppeteer test file look like a mile-long setup script before you even get to the first it
block? You spend ages writing boilerplate to launch a browser, open a new page, and handle cleanup, all before you can test a single button click. You’re not alone. For years, this has been a common frustration in end-to-end (E2E) testing.
But it’s 2025, and it’s time to leave that mess behind. There’s a cleaner, more modular, and infinitely more maintainable way to structure your tests. This guide will walk you through the ultimate approach to importing and managing Jest and Puppeteer helpers, transforming your testing workflow from a chore into a charm.
On This Page
The Problem with the “Old Way”
Let’s be honest. You’ve probably written code that looks something like this inside your test file:
// in some-feature.test.js
const puppeteer = require('puppeteer');
describe('Some Feature', () => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
jest.setTimeout(30000); // Set a long timeout
});
beforeEach(async () => {
await page.goto('https://example.com/login');
});
afterAll(async () => {
await browser.close();
});
it('should do something', async () => {
// ... actual test logic
});
});
What’s wrong with this? For a single file, maybe nothing. But multiply this across ten, twenty, or a hundred test files. You’re facing:
- Massive Code Duplication: The same
beforeAll
andafterAll
blocks are copied everywhere. - Maintenance Headaches: Need to change the Puppeteer launch options (e.g., add
--no-sandbox
or run in headless mode)? You have to update every single test file. - Poor Readability: Test files are cluttered with setup logic, obscuring the actual tests they are supposed to run.
The 2025 Philosophy: Centralized & Reusable Helpers
The modern approach embraces the Don't Repeat Yourself (DRY) principle. We'll centralize two types of logic:
- Lifecycle Management: The code that launches and closes the browser and page.
- Utility Functions: Reusable actions you perform all the time, like logging in, taking a screenshot, or waiting for a specific element.
By creating dedicated helper modules, we can import just what we need, keeping our test files lean and focused on their specific purpose.
Step-by-Step: Building Your Helper Module
Let's build this from the ground up. It’s simpler than you think.
Step 1: The Project Structure
A clean structure is the foundation. Inside your tests directory (commonly __tests__
or tests
), create a `helpers` folder.
my-project/
├── __tests__/
│ ├── helpers/
│ │ ├── puppeteer.js // Handles browser/page lifecycle
│ │ └── utils.js // Contains common reusable functions
│ ├── login.test.js
│ └── dashboard.test.js
├── jest.config.js
└── package.json
Step 2: Create the Core Puppeteer Lifecycle Helper
This is the most important part. We'll create a file that manages the browser instance for us. This file will use Jest's beforeAll
and afterAll
hooks and export the `browser` and `page` objects.
Create __tests__/helpers/puppeteer.js
:
// __tests__/helpers/puppeteer.js
const puppeteer = require('puppeteer');
// We can set a single timeout for all tests.
// No more repeating this in every file!
jest.setTimeout(30000);
let browser;
let page;
// This runs once before all tests in a suite.
// It launches the browser and creates a new page.
beforeAll(async () => {
browser = await puppeteer.launch({
headless: 'new', // Use the new headless mode
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
page = await browser.newPage();
});
// This runs once after all tests in a suite.
// It closes the browser.
afterAll(async () => {
if (browser) {
await browser.close();
}
});
// Export the browser and page objects so they can be used in tests.
// We wrap them in a function to ensure they are initialized before being accessed.
module.exports = {
getBrowser: () => browser,
getPage: () => page,
};
Why wrap exports in functions? This prevents issues where a test file might try to access `browser` or `page` before beforeAll
has finished running. Calling `getPage()` ensures you get the initialized instance.
Step 3: Create Reusable Utility Functions
Now, let's create helpers for common actions. These functions will accept the `page` object as their first argument.
Create __tests__/helpers/utils.js
:
// __tests__/helpers/utils.js
/**
* A helper function to log in a user.
* @param {import('puppeteer').Page} page - The Puppeteer page object.
* @param {string} username - The username to enter.
* @param {string} password - The password to enter.
*/
async function login(page, username, password) {
await page.goto('https://yourapp.com/login');
await page.type('#username', username);
await page.type('#password', password);
await page.click('button[type="submit"]');
await page.waitForNavigation();
}
/**
* Clicks a selector and waits for navigation.
* @param {import('puppeteer').Page} page - The Puppeteer page object.
* @param {string} selector - The CSS selector to click.
*/
async function clickAndWait(page, selector) {
await Promise.all([
page.waitForNavigation(),
page.click(selector),
]);
}
/**
* Takes a screenshot with a standardized name.
* @param {import('puppeteer').Page} page - The Puppeteer page object.
* @param {string} name - The name for the screenshot file.
*/
async function takeScreenshot(page, name) {
await page.screenshot({ path: `screenshots/${name}-${Date.now()}.png` });
}
module.exports = { login, clickAndWait, takeScreenshot };
Look how clean that is! Each function has a single responsibility. Adding a new helper, like `getText(page, selector)`, is trivial.
Putting It All Together: The Clean Test File
Now for the magic. Let's rewrite our `login.test.js` to use these new helpers. The difference is night and day.
Your new __tests__/login.test.js
:
// __tests__/login.test.js
// Import our lifecycle helper. This single line handles all setup and teardown!
const { getPage } = require('./helpers/puppeteer');
// Import the specific utility functions we need.
const { login } = require('./helpers/utils');
describe('Login Flow', () => {
it('should allow a valid user to log in successfully', async () => {
// Get the initialized page object from our helper
const page = getPage();
// Use our clean, reusable login function
await login(page, 'testuser', 'strongpassword123');
// Assert the outcome
const heading = await page.$eval('h1', el => el.textContent);
expect(heading).toBe('Welcome, testuser!');
});
it('should show an error message with invalid credentials', async () => {
const page = getPage();
await login(page, 'invalid-user', 'wrong-password');
const errorMessage = await page.$eval('.error-message', el => el.textContent);
expect(errorMessage).toContain('Invalid username or password');
});
});
Compare this to the original. It’s beautiful! The test file is now 100% focused on what to test, not how to set it up. It's readable, declarative, and incredibly easy to maintain.
Advanced Tip: Explicit Imports vs. Global Setup
You might have heard of Jest's `globalSetup` and `globalTeardown` configurations. These allow you to run a script once before and after *all* your test suites.
- Pros of Global Setup: Can be faster, as the browser is launched only once for the entire test run.
- Cons of Global Setup: It hides dependencies. A test file might magically work without you knowing why. It also makes it harder to run a single test file in isolation and can lead to state bleeding between test files if not managed carefully.
My Recommendation for 2025: Stick with the explicit, per-suite import approach shown in this guide. The clarity, test isolation, and maintainability it provides far outweigh the minor performance gain of a global setup for most projects. It forces a clear dependency chain and makes your test suites self-contained and robust.
Conclusion: Test Smarter, Not Harder
By moving your Puppeteer lifecycle logic and common actions into dedicated helper modules, you achieve a level of organization that pays dividends immediately. Your tests become:
- Readable: They describe business logic, not setup procedures.
- Maintainable: Update a launch option or a login selector in one place.
- Scalable: Adding new tests and helpers is simple and clean.
Stop fighting boilerplate and start writing E2E tests that are a pleasure to work with. Adopt this helper import pattern and spend your time building reliable, high-quality applications.