The reliable pattern is to give every Cucumber scenario its own World containing a Puppeteer browser, context, and page. Start them in a Before hook, use regular (non-arrow) step functions so Cucumber can bind this, and close the browser in After. This keeps cookies and page state from leaking between scenarios and works in headless Chrome on a developer machine or CI.
Use a per-scenario World for Puppeteer
Cucumber.js creates an isolated World for each scenario. Put the browser objects on that World rather than in module-level variables. A scenario then follows one predictable lifecycle:
Beforelaunches Chrome and creates a page (or a browser context plus page).Given,When, andThensteps usethis.page.Aftercloses the browser even when a step fails.
Do not use arrow functions for hooks or steps that need this; arrow functions capture the surrounding JavaScript this instead of Cucumber’s World.
Install Cucumber.js and Puppeteer
Let Puppeteer manage Chrome
For a new project, install the full puppeteer package:
#1 Best Overall
npm init -y
npm install --save-dev @cucumber/cucumber puppeteer
The package normally downloads a compatible Chrome for Testing binary during installation. Publisher estimates put that download at about 170 MB on macOS, 282 MB on Linux, and 280 MB on Windows; the actual size changes with the browser revision and platform. If your package manager blocks install scripts, install the browser explicitly:
npx puppeteer browsers install
Run that command in the build image or CI job that will execute the scenarios, not only on a developer laptop.
Use a system-managed browser
Choose puppeteer-core when your container or workstation already owns Chrome or Chromium:
npm install --save-dev @cucumber/cucumber puppeteer-core
Pass an explicit executable path when launching. You can also set PUPPETEER_EXECUTABLE_PATH and keep the value in your Puppeteer configuration or CI environment. The browser version still needs to match the Puppeteer release’s supported-browser mapping.
Create the project files
A small CommonJS project can use this layout:
features/
login.feature
steps/login.steps.js
support/world.js
support/hooks.js
cucumber.cjs
package.json
Define the World
// features/support/world.js
const { setWorldConstructor, World } = require('@cucumber/cucumber');
class CustomWorld extends World {
constructor(options) {
super(options);
this.browser = null;
this.context = null;
this.page = null;
}
}
setWorldConstructor(CustomWorld);
The constructor receives Cucumber’s options, including worldParameters. Keeping a context property makes it easy to create an isolated incognito-style context when a scenario needs more than one page.
Launch and close Chrome in hooks
// features/support/hooks.js
const { Before, After } = require('@cucumber/cucumber');
const puppeteer = require('puppeteer');
Before(async function () {
const settings = this.parameters || {};
this.browser = await puppeteer.launch({
headless: settings.headless ?? true,
executablePath: settings.executablePath || undefined,
args: settings.args || []
});
this.context = await this.browser.createBrowserContext();
this.page = await this.context.newPage();
if (settings.viewport) {
await this.page.setViewport(settings.viewport);
}
});
After(async function () {
if (this.browser) {
await this.browser.close();
this.browser = null;
this.context = null;
this.page = null;
}
});
puppeteer.launch({ headless: true }) is explicit, but headless mode is already Puppeteer’s default. Closing the browser, rather than only the page, also terminates renderer processes and prevents a CI worker from accumulating Chrome processes.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
Configure Cucumber and scenario parameters
// cucumber.cjs
module.exports = {
default: {
require: ['features/support/**/*.js', 'features/steps/**/*.js'],
format: ['progress'],
worldParameters: {
baseUrl: 'https://example.test',
viewport: { width: 1280, height: 900 },
headless: true
}
}
};
Cucumber searches the project root for configuration files including cucumber.json, cucumber.yaml, cucumber.yml, cucumber.js, cucumber.cjs, and cucumber.mjs. Use worldParameters for values such as the application URL, viewport, or a browser choice instead of hard-coding environment-specific data in every step.
Write a feature and steps
# features/login.feature
Feature: Login
Scenario: A valid user signs in
Given I open the login page
When I sign in with "alice" and "correct-horse"
Then I should see "Dashboard"
// features/steps/login.steps.js
const assert = require('node:assert/strict');
const { Given, When, Then } = require('@cucumber/cucumber');
Given('I open the login page', async function () {
await this.page.goto(`${this.parameters.baseUrl}/login`, {
waitUntil: 'networkidle2'
});
});
When('I sign in with {string} and {string}', async function (username, password) {
await this.page.locator('#username').fill(username);
await this.page.locator('#password').fill(password);
await this.page.locator('button[type="submit"]').click();
});
Then('I should see {string}', async function (text) {
await this.page.waitForFunction(
expected => document.body.innerText.includes(expected),
{},
text
);
const body = await this.page.evaluate(() => document.body.innerText);
assert.match(body, new RegExp(text.replace(/[.*+?^${}()|[]\]/g, '\$&')));
});
Replace selectors with the ones used by your application. Prefer stable IDs, roles, labels, or test attributes over deeply nested CSS selectors. A navigation assertion should wait for the application condition you care about, not an arbitrary sleep.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsRun the scenarios
npx cucumber-js
To see the browser locally, change headless to false in worldParameters. The same steps then run in a visible Chrome window, which is useful for inspecting selectors and authentication redirects. For a one-off run, keep the configuration stable and use an environment-specific config file or parameter rather than editing test code.
Headless modes and browser versions
Regular headless Chrome
Use headless: true (or omit the option) for normal automated runs. It uses the same Chrome code path intended for browser automation, so it is the safest default when headless output must resemble a user’s browser.
Headful debugging
Use headless: false on a machine with a display. In a Linux CI container, a display server is usually unavailable, so keep this mode for local diagnosis.
The shell mode
headless: 'shell' selects the separate chrome-headless-shell binary. Puppeteer’s guide describes it as potentially more performant, but it is not behaviorally identical to regular Chrome. Validate navigation, downloads, extensions, and rendering before adopting it for a compatibility-sensitive suite.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Rank #3
Match Puppeteer to Chrome
Puppeteer publishes a supported-browser table for each release. At the time covered here, Puppeteer 25.12.0 maps to Chrome for Testing 154.0.8037.57. That mapping is volatile: check the table for the exact version in your lockfile before pinning a system browser. A mismatch can produce launch failures, missing protocol commands, or subtle rendering differences.
Share state safely between steps
All steps in one scenario receive the same World instance, so values can be stored alongside the page:
When('I save the order number', async function () {
this.orderNumber = await this.page.locator('[data-order-number]').textContent();
});
Then('the order number is shown again', async function () {
const value = await this.page.locator('[data-order-number]').textContent();
if (value !== this.orderNumber) {
throw new Error(`Expected ${this.orderNumber}, received ${value}`);
}
});
Never put a page or browser in a module-level variable. Parallel scenarios would overwrite each other’s references, and sequential scenarios could inherit cookies or local storage. If a scenario needs multiple tabs, create them from this.context and keep the references on the World.
Use hooks for diagnostics and cleanup
Before and After are scenario-scoped. BeforeAll and AfterAll are for setup outside an individual scenario; in parallel execution they run once per worker by default. Use BeforeStep and AfterStep for step-level diagnostics, such as attaching a screenshot after a failure:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchconst { AfterStep } = require('@cucumber/cucumber');
AfterStep(async function ({ result }) {
if (result?.status === 'FAILED' && this.page) {
const image = await this.page.screenshot({ type: 'png' });
await this.attach(image, 'image/png');
}
});
Keep failure capture best-effort. If the page has already crashed, catch the screenshot error so it does not hide the original assertion failure.
CI and container troubleshooting
“Could not find Chrome” or a missing executable
This usually means the install script was skipped or the image never received a browser. Run npx puppeteer browsers install during the build, or enable the package’s install script. If you use puppeteer-core, provide a real executablePath or PUPPETEER_EXECUTABLE_PATH; that package does not download Chrome for you.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Sandbox errors in Linux
Chrome needs a usable sandbox and writable profile and cache directories. Prefer a correctly configured non-root container user. The --no-sandbox flag disables a security boundary and should be considered only when the content is trusted and the container cannot be fixed otherwise:
args: ['--no-sandbox', '--disable-setuid-sandbox']
Do not add those flags reflexively to production-facing tests.
Alpine Linux fails while Debian works
Chrome does not support Alpine out of the box. Install the required system dependencies, verify the Chromium version against Puppeteer’s supported-browser mapping, and run the complete image in your own CI environment. A Debian or Ubuntu base image is often simpler when you need Puppeteer’s downloaded Chrome.
Timeouts and flaky navigation
- Wait for a meaningful selector or application state instead of a fixed delay.
- Use an appropriate
waitUntilvalue;networkidle2can hang on applications with long-lived connections. - Give CI enough CPU, memory, and writable temporary space for Chrome’s profile.
- Capture the URL, console errors, and a screenshot in
AfterStepwhen a step fails. - Close every browser in
After, including scenarios that throw before a page is created.
Performance, reliability, and parallel execution
Launching one browser per scenario maximizes isolation but costs startup time. If the suite is large, run scenarios in parallel workers and let each worker own its browsers; do not share a mutable page across workers. A worker-scoped browser can reduce startup overhead, but then each scenario must receive a fresh browser context and all state-reset guarantees become your responsibility. The per-scenario browser pattern is the safer baseline.
Reuse a browser context only when the scenarios intentionally share authentication. Otherwise, a fresh context prevents cookies, local storage, permissions, and service-worker state from contaminating later cases. Keep waits event-driven, avoid unnecessary screenshots on passing steps, and pin dependency versions in the lockfile so browser revisions do not change unexpectedly.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.ES modules instead of CommonJS
If your project uses "type": "module", use import and a cucumber.mjs configuration. The lifecycle is unchanged:
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Best Value
// features/support/hooks.js
import { Before, After } from '@cucumber/cucumber';
import puppeteer from 'puppeteer';
Before(async function () {
this.browser = await puppeteer.launch({ headless: true });
this.page = await this.browser.newPage();
});
After(async function () {
await this.browser?.close();
});
Do not mix module systems accidentally: a CommonJS require in an ESM file (or the reverse) can fail before Cucumber discovers any steps.
Or skip the browser setup
If your goal is a clean image or PDF rather than browser assertions, ScreenshotNeo provides a single HTTP request. Its API accepts the URL and returns PNG, JPEG, WebP, or PDF; the documentation is at https://screenshotneo.com/docs/.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Every feature is included on every plan: 1,000 screenshots per month are free with no card, and paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
FAQ
Can one Cucumber scenario use more than one page?
Yes. Create additional pages from this.context, store them on the World, and close the browser in After. The context keeps their cookies and permissions isolated from other scenarios.
Recommended Free Tools
Should I use BeforeAll for a single browser?
Only when you deliberately accept shared state and understand worker behavior in parallel runs. For independent acceptance tests, scenario-scoped Before and After hooks are less error-prone.
Why does a test pass headful but fail headless?
Check viewport size, timing assumptions, missing fonts or system libraries, and browser-version alignment. Capture the failing URL, console output, and a screenshot, then reproduce with headless: false locally.
Frequently Asked Questions
Can I run Puppeteer with Cucumber.js in TypeScript?
Yes, provided your Cucumber loader transpiles the step and support files before execution. The World, regular function syntax, and Before/After lifecycle remain the same.
How do I supply credentials or custom headers?
Set them through Puppeteer before navigation, for example with page.setExtraHTTPHeaders or page.authenticate, and keep secrets in CI environment variables rather than cucumber.cjs.
Free tools Windows power users keep installed
One-click scans. No signup required.
Is chrome-headless-shell a drop-in replacement for Chrome?
No. Puppeteer documents shell mode as potentially faster but not behaviorally identical to regular Chrome, so verify the specific APIs and rendering your scenarios exercise.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

