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.
Repository
View repository ↗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 flowfeatures/step-definitions/— TypeScript step definition files; bind Gherkin phrases to page object method callssupport/world.ts— Custom Cucumber World; holds browser/context/page; set via setWorldConstructorsupport/hooks.ts— Cucumber 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 definitionsutils/— Shared helpers: env config loader, test data generators, optional API client for precondition setupcucumber.js— Cucumber runner config: feature file globs, step definition paths, TypeScript transpiler, format/reporter settingsPrerequisites
- ✓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
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 valuesSetup & run
Installation
- 1.
Clone the repository: git clone <repo-url> && cd playwright-typescript-cucumber-bdd - 2.
Install dependencies: npm install - 3.
Install Playwright browsers: npx playwright install --with-deps chromium - 4.
Copy environment config: cp .env.example .env - 5.
Set BASE_URL in .env (e.g. https://www.saucedemo.com for a public practice site) - 6.
Validate step definitions are wired: npx cucumber-js --dry-run - 7.
Run the full suite: npx cucumber-js
Commands
Run all feature files
npx cucumber-jsRuns every .feature file matched by the globs in cucumber.js
Run a single feature file
npx cucumber-js features/login.featureUseful 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-runChecks that every Gherkin step has a matching step definition without executing any browser code
Generate HTML report
npm run reportPost-processes cucumber-report.json into a standalone HTML report using cucumber-html-reporter
Environment
| Variable | Description | Example | Required |
|---|---|---|---|
BASE_URL | Root URL of the application under test | https://www.saucedemo.com | Yes |
BROWSER | Playwright browser to launch: chromium, firefox, or webkit | chromium | No |
HEADLESS | Set to 'false' to run the browser in headed mode for local debugging | true | No |
TEST_USER_USERNAME | Username for the Sauce Demo standard user | standard_user | Yes |
TEST_USER_PASSWORD | Password for the Sauce Demo standard user | secret_sauce | Yes |
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