ScreenshotNeo

BlogHow-to

How to Use Cucumber With Playwright

Combine Gherkin scenarios with Playwright browser automation in Cucumber.js. Set up scenario state, hooks, runnable code, and reliable cleanup.

By the ScreenshotNeo team4 October 202611 min read

Use Cucumber.js to run Gherkin scenarios and match each step to JavaScript or TypeScript support code; use Playwright from that support code to control the browser. Cucumber does not automate browsers itself, and this integration is assembled in your project rather than enabled as a Playwright Test setting. A practical starting pattern is one browser process per Cucumber worker, with a fresh browser context and page for each scenario.

This guide shows a runnable JavaScript setup, explains scenario state and hooks, and covers tags, parallel execution, failures, and runner choice. The examples use the current package names and avoid pinning versions; check the official installation pages for requirements compatible with your Node.js runtime.

1. Understand how Cucumber and Playwright fit together

The execution path is:

.feature scenario
  → Cucumber.js matches a step
  → step definition calls Playwright
  → browser, context, and page perform the action
  → assertion reports success or failure to Cucumber

A feature file describes behavior in Gherkin. Step definitions connect that language to code. Playwright provides browser, context, and page APIs. Cucumber documents that it is not a browser automation tool, while documenting asynchronous step definitions and scenario state separately. This is a composition of tools, not a native mode inside Playwright Test. See the Cucumber browser automation guide and Cucumber.js step definitions.

2. Create a JavaScript project and install dependencies

Start in a new or existing Node.js project. Install Cucumber.js and Playwright, then install the browser binaries Playwright needs:

npm init -y
npm install --save-dev @cucumber/cucumber playwright
npx playwright install

The commands install the packages without hard-coded versions. Playwright installation and browser setup can vary by operating system and runtime; consult the Playwright installation guide and browser documentation if installation needs system dependencies.

Use this small project layout:

features/
  search.feature
  support/
    world.js
    hooks.js
  step_definitions/
    search.steps.js
cucumber.js
package.json

Add a Cucumber configuration file to specify where feature files and support code live. This example runs headless Chromium and supports an optional parallel worker count:

// cucumber.js
module.exports = {
  default: {
    paths: ['features/**/*.feature'],
    require: ['features/support/**/*.js', 'features/step_definitions/**/*.js'],
    format: ['progress'],
    parallel: Number(process.env.CUCUMBER_PARALLEL || 0),
    worldParameters: {
      baseURL: process.env.BASE_URL || 'https://example.com'
    }
  }
};

Add a script to package.json (merge it with any scripts already there):

{
  "scripts": {
    "test:e2e": "cucumber-js"
  }
}

Run with npm run test:e2e. Set BASE_URL to your application’s test environment as appropriate. The example uses CommonJS JavaScript files; if your project uses ESM or TypeScript, configure Cucumber’s support-code loading for that runtime rather than mixing module formats.

3. Write a feature in Gherkin

Keep feature steps at the behavior level. Avoid encoding browser mechanics such as CSS selectors into every sentence; put those details in step definitions or page helpers.

# features/search.feature
Feature: Search

  Scenario: A visitor searches for a term
    Given I open the search page
    When I search for "Playwright"
    Then the results heading contains "Playwright"

4. Share Playwright state through Cucumber World

Cucumber-JS creates an isolated World for each scenario. Store scenario-owned values such as the browser context and page on that World. Use a regular function for hooks or steps that access this; arrow functions capture their surrounding this and do not receive the scenario World as their function context. See Cucumber state.

// features/support/world.js
const { setWorldConstructor, World } = require('@cucumber/cucumber');

class BrowserWorld extends World {
  constructor(options) {
    super(options);
    this.baseURL = options.parameters.baseURL;
    this.browser = undefined;
    this.context = undefined;
    this.page = undefined;
  }
}

setWorldConstructor(BrowserWorld);

Use hooks to create a page before each scenario and close its context afterward. This example creates a browser process per worker lazily and closes it when that worker exits. When running without parallel workers, there is one worker. Hooks and World are per-worker/per-scenario mechanisms; this lifecycle is an implementation pattern, not a requirement imposed by either tool.

// features/support/hooks.js
const { Before, After, BeforeAll, AfterAll, setDefaultTimeout } = require('@cucumber/cucumber');
const { chromium } = require('playwright');

setDefaultTimeout(30_000);
let browser;

BeforeAll(async function () {
  browser = await chromium.launch({ headless: true });
});

Before(async function () {
  this.browser = browser;
  this.context = await browser.newContext();
  this.page = await this.context.newPage();
});

After(async function (scenario) {
  if (scenario.result?.status === 'FAILED' && this.page) {
    // Optional diagnostic artifact; keep the path unique for parallel runs.
    const safeName = (scenario.pickle.name || 'scenario').replace(/[^a-z0-9_-]+/gi, '-');
    await this.page.screenshot({ path: `artifacts/${safeName}-${Date.now()}.png`, fullPage: true }).catch(() => {});
  }
  if (this.context) await this.context.close();
});

AfterAll(async function () {
  if (browser) await browser.close();
});

If you use the optional failure screenshot, create the artifacts directory before running, or adapt the code to create it. In CI, ensure the artifact path is retained by your CI system if you need to inspect it. The catch prevents a diagnostic screenshot failure from masking the scenario result.

5. Define asynchronous steps and run the scenario

Cucumber waits for a returned promise or an async function to complete. Await Playwright actions and assertions so that errors reach Cucumber and fail the scenario. This example uses Playwright’s locator assertion API from expect in @playwright/test; if you prefer not to add that package, the following steps instead use a Node assertion on locator text.

// features/step_definitions/search.steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const assert = require('node:assert/strict');

Given('I open the search page', async function () {
  await this.page.goto(this.baseURL, { waitUntil: 'domcontentloaded' });
});

When('I search for {string}', async function (term) {
  await this.page.getByRole('searchbox').fill(term);
  await this.page.getByRole('searchbox').press('Enter');
});

Then('the results heading contains {string}', async function (expected) {
  const heading = this.page.getByRole('heading', { name: /results/i });
  await heading.waitFor({ state: 'visible' });
  const text = await heading.textContent();
  assert.ok(text?.includes(expected), `Expected heading to include ${JSON.stringify(expected)}, got ${JSON.stringify(text)}`);
});

The locators and page behavior above are illustrative: replace them with selectors and accessible roles that exist in your application. The code uses only the playwright package and Node’s built-in assertion module. Run the example after setting an application URL whose page actually provides a search box and a matching results heading.

6. How do Cucumber step definitions use Playwright?

A step definition receives captured values from the Gherkin expression and can read the page from its World. Cucumber Expressions such as {string} are convenient for quoted strings; regular expressions are also supported. Keep each step small and move repeated workflows into helper functions or page objects.

For example, a helper can hold the page-specific interaction:

async function searchFor(page, term) {
  await page.getByRole('searchbox').fill(term);
  await page.getByRole('searchbox').press('Enter');
}

When('I search for {string}', async function (term) {
  await searchFor(this.page, term);
});

Do not define the step with an arrow function if it needs this.page. If the step does not use World, an arrow function is possible, but consistent regular functions can make the convention clearer.

7. Choose the right browser, context, and page lifecycle

Resource Typical lifetime What it isolates
Browser One per worker, reused across scenarios Browser process and its startup cost
Context One per scenario Cookies, local storage, permissions, and session state
Page One or more per scenario A tab within a context

A fresh context per scenario helps prevent state leaking through cookies or storage. Close it even on failures; the After hook is the natural cleanup point. A browser per scenario is simpler but starts more processes and can increase runtime and resource use. Reusing one context across scenarios saves setup but risks order-dependent tests and should be a deliberate choice.

Hooks can be selected by tags, and Cucumber runs Before hooks in definition order and After hooks in reverse order. This allows targeted setup while preserving cleanup. See Cucumber.js hooks.

// Only scenarios tagged @authenticated receive this setup.
const { Before } = require('@cucumber/cucumber');

Before({ tags: '@authenticated' }, async function () {
  await this.page.goto(`${this.baseURL}/login`);
  // Perform the app's login flow, or establish state through a supported test fixture.
});

8. Run a scenario and diagnose failures

Run all configured features with npm run test:e2e. To narrow a run, use Cucumber’s supported command-line options for your installed release, such as filtering by tags or selecting a feature path. Keep the configuration and CLI syntax aligned with the installed Cucumber.js version.

For useful diagnostics, capture a screenshot or page URL on failure, retain browser console messages when investigating client errors, and consider enabling Playwright tracing around scenarios that are hard to reproduce. Keep artifact names unique across parallel workers, and avoid storing secrets in traces or screenshots. Cucumber and Playwright are separate tools, so wire artifact collection into your hooks or helper code explicitly.

9. Parallel execution and cross-browser coverage

Cucumber.js parallel mode runs scenarios in workers. BeforeAll and AfterAll run once per worker by default, so module-level browser variables in the example belong to that worker rather than to every scenario globally. Do not assume a single global browser lifecycle across the whole run. Shared test servers, accounts, queues, and data need their own concurrency strategy. See the hooks documentation; GitHub’s main branch can include newer behavior than an installed release, so verify version-specific features before relying on them.

For cross-browser coverage, parameterize browser launch or define separate Cucumber profiles/CI jobs for Chromium, Firefox, and WebKit. Playwright projects can group browser and environment settings, but they do not automatically connect Cucumber scenarios to Playwright Test projects. Review Playwright projects if you are deciding how to manage that matrix.

Increase parallelism gradually. More workers can shorten elapsed time but also consume more CPU and memory, put more load on the application, and expose collisions in shared test data. Give each worker independent accounts or data where needed, and make cleanup safe to run after partial failures.

10. Cucumber.js or Playwright Test?

Choose Cucumber.js when executable Gherkin and a BDD workflow shared with product, QA, or other stakeholders are important enough to justify maintaining step definitions, hooks, and integration support code. Choose Playwright Test when you want Playwright’s own Node.js runner and integrated test workflow. Playwright recommends its own runner for Node.js; Cucumber remains a separate option for teams that need Gherkin. See Playwright’s language documentation and the Cucumber browser automation guide.

Decision point Cucumber.js with Playwright Playwright Test
Scenario format Gherkin feature files and matched steps Test code in Playwright’s runner
Browser integration You write support code to create and clean browser resources Use Playwright’s runner and its documented test workflow
Best fit Teams that need behavior scenarios as a shared artifact Teams that prefer Playwright’s Node.js test runner
Parallel strategy Plan worker ownership and shared state in Cucumber hooks Use Playwright Test’s own configuration and projects

11. Troubleshooting common problems

Symptom Likely cause Fix
No step definition matches a step The expression, quoting, or support-code discovery path does not match the feature. Check the wording and captured parameter type; confirm require or support loading includes the step file.
this.page is undefined The setup hook did not run, the page was not assigned to World, or an arrow function was used to access World. Verify hook registration and assignment; use a regular function for hooks and steps that use this.
Step times out while the page keeps loading The navigation waits for a lifecycle event that a long-running page or application never reaches, or the default step timeout is too short. Choose the navigation condition appropriate to the application, wait for a specific locator, and set a justified Cucumber timeout.
Browser executable is missing The Playwright package is installed but its browser binary is not. Run npx playwright install; in Linux CI, follow Playwright’s browser and system-dependency instructions.
Tests pass alone but fail in parallel Scenarios share accounts, records, ports, files, or mutable server state. Use worker-specific data or isolated fixtures, unique artifact paths, and concurrency-safe cleanup.
One scenario affects the next Context, page, cookies, or application data are reused or not cleaned up. Create a fresh context per scenario and close it in After; reset server-side data too.
Assertion error is hidden or the scenario passes too early A promise was not returned or awaited. Make the step async and await every browser operation and assertion.
Module or loader syntax error CommonJS and ESM/TypeScript configuration are mixed. Choose one module format and configure Cucumber support-code loading to match the project.
Failure screenshot hook throws The output directory is missing, the page has already closed, or the browser is disconnected. Create the directory and guard diagnostic capture; ensure cleanup still runs.

12. Performance, reliability, and cost considerations

  • Reuse the browser process, isolate the context. A per-worker browser with a per-scenario context usually balances startup work with clean session state.
  • Wait for application conditions. Prefer waiting for a meaningful locator or state over arbitrary sleeps. Use a fixed delay only for a known reason, since it makes runs slower and can still be too short.
  • Limit parallelism to available capacity. Browser workers use CPU and memory, and can increase pressure on test environments and shared services.
  • Control external dependencies. Network variability, third-party pages, and rate limits can make browser scenarios flaky; use stable test environments and test-owned fixtures where possible.
  • Keep artifacts targeted. Screenshots and traces help diagnose failures but use storage and may expose page data. Capture them when useful and handle them according to your project’s data practices.
  • Budget for your own infrastructure. Cucumber.js and Playwright are packages; execution cost comes from the machines, CI minutes, browser storage, and application environment you choose. The research sources establish no universal runtime or cost benchmark.

13. Or skip the browser setup

If the task is to capture a webpage image rather than test an interactive workflow, ScreenshotNeo provides a screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for options and response details.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
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(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.

14. FAQ

How do I integrate Cucumber with Playwright?

Install Cucumber.js and Playwright, configure Cucumber to load your feature and support files, create browser resources in hooks, and call Playwright from asynchronous step definitions.

How do I share a Playwright page between Cucumber steps?

Assign the page to the scenario’s World in a Before hook, then access it as this.page in regular-function steps. Close its context in an After hook.

Can I use TypeScript?

Yes. The same division of responsibilities applies, but configure Cucumber’s support-code loader and module format to match your TypeScript project and installed Cucumber.js version.

Does using Cucumber make Playwright Test run the feature files?

No. Cucumber.js and Playwright Test are distinct runners. In this setup, Cucumber runs scenarios and your support code invokes Playwright.

Does this pattern test mobile applications?

It automates browser contexts and pages. Playwright can emulate device characteristics for browser testing, but that is distinct from automating a native mobile application.