Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

  1. Before launches Chrome and creates a page (or a browser context plus page).
  2. Given, When, and Then steps use this.page.
  3. After closes 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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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
Sale
HTML and CSS: Design and Build Websites
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Run 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const { 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
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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 waitUntil value; networkidle2 can 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 AfterStep when 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.Support on Ko-Fi

ES modules instead of CommonJS

If your project uses "type": "module", use import and a cucumber.mjs configuration. The lifecycle is unchanged:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
// 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.