On this page14 sections

Playwright + TypeScript + Cucumber BDD

A BDD automation project combining Playwright's browser control with Cucumber's Gherkin scenario definitions for readable, stakeholder-friendly E2E test suites.

IntermediateBDDE2EUI automationAcceptance testingbdd
Setup: ~25 minLanguage: TypeScriptFramework: PlaywrightPackage manager: npmBest for: Teams that want BDD-style test documentation aligned with user stories

Overview

This project shows how to integrate Playwright's browser automation with Cucumber.js's Gherkin feature files to produce a BDD test suite. The key architectural point: Cucumber.js — not Playwright Test — is the test runner. A custom World class (support/world.ts) initialises Playwright's Browser, BrowserContext, and Page objects, making them available to every step definition. Gherkin feature files describe behaviour in plain English (Given/When/Then); TypeScript step definitions map those phrases to Playwright actions via Page Object classes. The result is a suite where product managers can read the test scenarios, developers can write step definitions, and CI publishes an HTML report that shows which behaviours passed or failed without requiring a local environment.

Project goals

  • Demonstrate how to wire @cucumber/cucumber as the test runner with Playwright providing the browser — without using Playwright Test
  • Implement a custom Cucumber World class that manages Browser, BrowserContext, and Page lifecycle per scenario
  • Write Gherkin feature files that describe user-visible behaviour declaratively, avoiding UI implementation details
  • Map Gherkin steps to Playwright actions through Page Object classes, keeping step definitions readable and thin
  • Capture a screenshot on scenario failure inside a Cucumber After hook and embed it in the HTML report
  • Run the suite in CI on GitHub Actions and publish an HTML test report as a downloadable artifact
  • Use Cucumber tags (@smoke, @regression) to run subsets of the suite without modifying configuration

Architecture

Cucumber World wrapping Playwright

Cucumber.js drives the test lifecycle. A custom World class (set via setWorldConstructor) holds the Playwright Browser, BrowserContext, and Page instances. Cucumber BeforeAll/AfterAll hooks in support/hooks.ts launch and close the browser around the full run; Before/After hooks create and destroy a fresh BrowserContext per scenario, ensuring test isolation. Step definitions import page object classes and call their action methods — Playwright API calls never appear directly in step definitions.

features/Gherkin .feature files; describe behaviour in Given/When/Then; one file per user story or flow
features/step-definitions/TypeScript step definition files; bind Gherkin phrases to page object method calls
support/world.tsCustom Cucumber World; holds browser/context/page; set via setWorldConstructor
support/hooks.tsCucumber BeforeAll (launch browser), Before (new context+page per scenario), After (screenshot on failure), AfterAll (close browser)
pages/Page Object classes; encapsulate Playwright locators and actions; imported by step definitions
utils/Shared helpers: env config loader, test data generators, optional API client for precondition setup
cucumber.jsCucumber runner config: feature file globs, step definition paths, TypeScript transpiler, format/reporter settings

Prerequisites

  • Node.js 18 or later
  • npm 9 or later
  • Git
  • A GitHub account (for CI pipeline)
  • Familiarity with TypeScript basics (classes, async/await, interfaces)
  • Basic understanding of Gherkin syntax (Given/When/Then) is helpful but not required

Folder structure

Project structure
Bash
cucumber.js                               # Cucumber runner config: feature file globs, step definition paths, TypeScript transpiler (ts-node), JSON formatter output path
tsconfig.json                             # TypeScript config; ts-node is used by cucumber.js to transpile step definitions at runtime — no separate build step needed
.env.example                              # Template for environment variables — copy to .env before running locally
features/                                 # All Gherkin .feature files — one file per user story or workflow
features/login.feature                    # Scenarios for login: successful login, failed login with wrong credentials, locked-out account
features/checkout.feature                 # BDD scenarios for the add-to-cart and checkout flow, including empty-cart validation
features/step-definitions/                # TypeScript step definition files — one per feature domain
features/step-definitions/login.steps.ts  # Given/When/Then bindings for login scenarios; delegates to LoginPage methods
features/step-definitions/checkout.steps.ts  # Step definitions for cart and checkout scenarios; uses this.page from the World
support/                                  # Cucumber support files loaded before tests; controls browser lifecycle and World construction
support/world.ts                          # Custom World class: holds browser/context/page; exposes helper methods; registered with setWorldConstructor
support/hooks.ts                          # BeforeAll launches browser; Before creates a fresh BrowserContext+Page per scenario; After screenshots on failure and attaches via this.attach(); AfterAll closes browser
pages/                                    # Page Object classes used by step definitions to keep Playwright calls out of Gherkin bindings
pages/LoginPage.ts                        # Playwright locators and action methods for the login page (fillUsername, fillPassword, submit, getErrorMessage)
pages/CheckoutPage.ts                     # Locators and actions for the product listing, cart, and checkout pages
utils/env.ts                              # Typed environment variable loader: reads .env, validates required keys on startup, throws on missing values

Setup & run

Installation

  1. 1.Clone the repository: git clone <repo-url> && cd playwright-typescript-cucumber-bdd
  2. 2.Install dependencies: npm install
  3. 3.Install Playwright browsers: npx playwright install --with-deps chromium
  4. 4.Copy environment config: cp .env.example .env
  5. 5.Set BASE_URL in .env (e.g. https://www.saucedemo.com for a public practice site)
  6. 6.Validate step definitions are wired: npx cucumber-js --dry-run
  7. 7.Run the full suite: npx cucumber-js

Commands

Run all feature files

npx cucumber-js

Runs every .feature file matched by the globs in cucumber.js

Run a single feature file

npx cucumber-js features/login.feature

Useful during development to focus on one feature without running the full suite

Run scenarios with a specific tag

npx cucumber-js --tags "@smoke"

Runs only scenarios tagged with @smoke — combine tags with 'and', 'or', 'not' expressions

Dry run (validate step definitions)

npx cucumber-js --dry-run

Checks that every Gherkin step has a matching step definition without executing any browser code

Generate HTML report

npm run report

Post-processes cucumber-report.json into a standalone HTML report using cucumber-html-reporter

Environment

VariableDescriptionExampleRequired
BASE_URLRoot URL of the application under testhttps://www.saucedemo.comYes
BROWSERPlaywright browser to launch: chromium, firefox, or webkitchromiumNo
HEADLESSSet to 'false' to run the browser in headed mode for local debuggingtrueNo
TEST_USER_USERNAMEUsername for the Sauce Demo standard userstandard_userYes
TEST_USER_PASSWORDPassword for the Sauce Demo standard usersecret_sauceYes

Test data strategy

  • Each scenario receives a fresh BrowserContext and Page via the Cucumber Before hook — state from one scenario never leaks into another
  • Test credentials are read from environment variables in utils/env.ts; never hardcoded in step definitions or feature files
  • For scenarios that require pre-existing data (e.g. a cart with items), the World exposes an optional API helper that seeds state via HTTP before the Gherkin Given step opens the browser
  • Scenario Outline with Examples tables is used for data-driven scenarios (e.g. login with multiple invalid credential combinations) — no code duplication across data rows
  • Sensitive values (passwords, API keys) are stored in .env (gitignored locally) and injected as GitHub Actions secrets in CI

Reporting

  • Cucumber JSON formatter writes cucumber-report.json after each run — this is the raw data source for all report post-processing
  • npm run report invokes cucumber-html-reporter to convert cucumber-report.json into a standalone HTML report with scenario status, duration, and step breakdown
  • The Cucumber After hook captures a Playwright screenshot on any failing scenario and embeds it into the report via this.attach(screenshot, 'image/png')
  • GitHub Actions uploads the HTML report directory as a downloadable artifact on every run, including failures, so the report is accessible without a local environment

CI/CD

  • A .github/workflows/cucumber.yml workflow triggers on push and pull_request to the main branch
  • The workflow uses actions/setup-node@v4 pinned to Node 20 with npm dependency caching
  • Playwright's Chromium browser is installed with npx playwright install --with-deps chromium — only Chromium in CI for speed
  • The full suite runs with npx cucumber-js; npm run report generates the HTML report post-run
  • The report directory is uploaded with actions/upload-artifact@v4 so failures are debuggable from the GitHub Actions summary tab
  • BASE_URL and TEST_USER_PASSWORD are stored as GitHub Actions repository secrets and injected at runtime

Common issues

"No step definition found for ..." error at runtime

Cause: The Gherkin step text does not exactly match any step definition string or regex — extra spaces, punctuation, or capitalisation differences cause a mismatch

Fix: Run npx cucumber-js --dry-run to list unmatched steps; check the step definition for exact string or regex alignment; use backtick template strings in step definitions to avoid regex escaping issues

this.page is undefined inside a step definition

Cause: The step definition file is not using the World correctly — either the World was not registered with setWorldConstructor, or the step definition is not an arrow function (arrow functions lose the 'this' binding)

Fix: Ensure support/world.ts calls setWorldConstructor(CustomWorld) and that step definition callbacks are declared as regular function() expressions, not arrow functions

Browser state leaks between scenarios (one scenario's login persists into the next)

Cause: The Cucumber Before hook is reusing the same BrowserContext across scenarios rather than creating a new one

Fix: In support/hooks.ts, call browser.newContext() and context.newPage() inside the Before hook (not BeforeAll), and close the context in the After hook — each scenario gets an isolated context

TypeScript compilation errors when running cucumber-js

Cause: cucumber.js is not configured to transpile TypeScript — Cucumber loads .ts files directly but needs ts-node or tsx in the requireModule (or loader) config

Fix: In cucumber.js, add requireModule: ['ts-node/register'] (CommonJS) or loader: ['ts-node/esm'] (ESM); ensure tsconfig.json is compatible with ts-node's defaults

Screenshots are not embedded in the HTML report after failures

Cause: this.attach() is being called with the wrong MIME type, or the Cucumber After hook is not receiving the scenario object needed to check failure status

Fix: Declare the After hook as After(async function(scenario) { ... }) and call this.attach(buffer, 'image/png') only when scenario.result?.status === Status.FAILED

Best practices

  • Write Gherkin steps declaratively ('When the user logs in with valid credentials') not imperatively ('When the user clicks the username field and types admin@example.com') — declarative steps survive UI refactors
  • Keep each scenario focused on one behaviour — avoid Scenario Outlines that test unrelated paths in the same examples table
  • Put all Playwright locator and interaction code inside Page Object classes — step definitions should read like English driving page objects, with no raw page.$() calls
  • Use Cucumber tags (@smoke, @regression, @wip) to partition the suite — run only @smoke on PR for fast feedback and the full @regression suite on merge
  • Validate that all step definitions are wired with npx cucumber-js --dry-run before pushing — a dry-run failure in CI is less expensive than a runtime mismatch discovered in a 10-minute run
  • Use Scenario Outline with Examples tables for data-driven scenarios rather than duplicating similar feature blocks
  • Store the Cucumber JSON report in a predictable path (e.g. reports/cucumber-report.json) so CI artifact upload paths are stable across runs

Next steps

  • Add an API step library (e.g. 'Given a user account exists with role {string}') that calls the backend API to set up preconditions without UI interaction — keeps scenarios faster and more stable
  • Integrate Allure Reporter as an alternative to cucumber-html-reporter for richer trend dashboards, history, and per-step attachment viewing
  • Extend the suite to run in parallel using Cucumber's --parallel flag combined with isolated BrowserContext creation in the World — verify that the Before/After hooks are parallel-safe
  • Add accessibility assertions using axe-core in a shared After hook so every scenario also validates WCAG compliance without duplicating axe setup