Logo
  • Home
  • Services
  • Blog
  • Company
Contact Us

Playwright × BDD: Cucumber.js vs Playwright-bdd

Home > Blog > Playwright × BDD: Cucumber.js vs Playwright-bdd

Author: Tomotaka ASAGI

Published: Jan 17, 2026

image

Introduction

When I started using Playwright with TypeScript, I chose Cucumber.js as my BDD framework.

The reason was simple. I had spent years using Selenium + Cucumber in Java, so I just replaced Selenium with Playwright. It was the same pattern I mentioned in Part 2—sticking with a familiar setup.

But as I worked on projects, I noticed something important.

Cucumber.js and Playwright-bdd use different test runners.

This difference in test runners affects which features are available. As with Parts 1 and 2, understanding this difference is key to making the right choice.

The Cucumber.js + Playwright Architecture

Let's first clarify the architecture when combining Cucumber.js with Playwright.

Cucumber.js (test runner)
    ↓
Playwright (browser automation)

In this setup, the test runner is Cucumber.js. Playwright is only used as a library for browser automation.

This puts you in the same situation as using "Playwright with Java" from Part 2.

Feature Differences by Test Runner

When using Cucumber.js as your test runner, Playwright Test-specific features are not available. This is not a limitation of Cucumber.js itself, but a structural difference due to using a different test runner.

Feature
Playwright Test
Cucumber.js + Playwright
toHaveScreenshot()
✅
❌
toMatchSnapshot()
✅
❌
Fixtures
✅
❌
HTML Reporter
✅
❌ (can use Cucumber reporters)
Trace Viewer integration
✅
❌
Sharding
✅
❌
UI Mode
✅
❌

This information is current as of January 2025. Future updates to Cucumber.js may add support for some of these features.

In my projects, I often wanted to use VRT (toHaveScreenshot()). With the Cucumber.js setup, implementing VRT required separate tools like OpenCV, and managing screenshots needed additional consideration.

Understanding Your Options

Let's organize the available options.

Option 1: Cucumber.js + Playwright (Traditional Setup)

Pros:

  • Leverage existing Cucumber assets
  • Same setup as Java era, low learning curve
  • Access to Cucumber ecosystem (reporting tools, multi-language support, etc.)
  • Long track record with abundant resources and community support
  • Easy to share knowledge with Cucumber projects in other languages (Java, Ruby, etc.)

Cons:

  • Cannot use Playwright Test-specific features
  • VRT requires separate implementation

Option 2: Playwright Test (Without BDD)

Pros:

  • Full access to Playwright Test features
  • VRT, Reporter, Fixtures available out of the box

Cons:

  • Cannot use Gherkin syntax
  • Cannot write tests as a shared language with business stakeholders

Option 3: Playwright-bdd

Pros:

  • Can use Gherkin syntax (maintain BDD concepts)
  • Full access to Playwright Test features
  • VRT (toHaveScreenshot()) is available

Cons:

  • Migration cost from Cucumber.js
  • Need to learn Playwright-bdd specifics
  • Shorter track record compared to Cucumber
  • Cannot use Cucumber-specific ecosystem (certain reporting tools, etc.)

What is Playwright-bdd?

Playwright-bdd is a library that enables BDD (Gherkin syntax) on top of Playwright Test.

Playwright Test (test runner)
    ↓
Playwright-bdd (Gherkin support)
    ↓
Playwright (browser automation)

The key point is that the test runner is Playwright Test. This allows you to write tests in Gherkin while having access to all Playwright Test features.

Basic Usage

Installation

npm init playwright@latest
npm install -D playwright-bdd

Feature File

# features/login.feature
Feature: Login functionality

  Scenario: Successful login
    Given I open the login page
    When I login with valid credentials
    Then the dashboard is displayed

Page Objects and Fixtures

In real projects, Page Object Model (POM) is commonly used for maintainability. With Playwright-bdd, you can inject page objects using Fixtures.

// fixtures.ts
import { test as base } from 'playwright-bdd';
import { LoginPage } from './pages/LoginPage';

export const test = base.extend<{ loginPage: LoginPage }>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

Step Definitions

In step definitions, you use the page objects injected via Fixtures.

Step definitions become simpler, and page operations are consolidated in page objects. When UI changes occur, you only need to modify the page objects.

VRT Works Too

With Playwright-bdd, Playwright Test features work as expected.

Then('the layout is correct', async ({ page }) => {
  await expect(page).toHaveScreenshot('dashboard.png');
});

VRT, which requires separate implementation in the Cucumber.js setup, is available as a standard feature with Playwright-bdd.

Cucumber.js vs Playwright-bdd Comparison

Aspect
Cucumber.js + Playwright
Playwright-bdd
Gherkin syntax
✅
✅
Test runner
Cucumber.js
Playwright Test
VRT (toHaveScreenshot)
Separate implementation
Built-in
HTML Reporter
Cucumber reporters
Playwright Test built-in
Fixtures
Via World object
Built-in
Fixtures + POM
Via World object
Built-in (great fit)
Trace Viewer
Requires separate setup
Built-in
Existing Cucumber assets
Easy to reuse
Migration required
Ecosystem track record
Long
Relatively new

This comparison is based on information as of January 2025. Both projects are actively developed, and features may be added or changed in the future.

Which Should You Choose?

As with Parts 1 and 2, the answer is: it depends on what you need.

Choose Cucumber.js when:

  • You have extensive existing Cucumber assets (feature files, step definitions)
  • You want to leverage the Cucumber ecosystem (specific reporting tools, etc.)
  • You want to share knowledge with Cucumber projects in other languages
  • Your team is familiar with Cucumber

Choose Playwright-bdd when:

  • You want to maintain BDD concepts while using Playwright Test features
  • You want to leverage VRT (toHaveScreenshot()) as a built-in feature
  • It's a new project, or you can accept the migration cost
  • You want to adopt a POM structure utilizing Fixtures

Conclusion

Both Cucumber.js and Playwright-bdd are frameworks that enable BDD with Playwright.

The main difference lies in the test runner. Cucumber.js uses Cucumber as its test runner, while Playwright-bdd uses Playwright Test. This difference affects the available features and ecosystem.

  • Cucumber.js: Long track record, rich ecosystem, leverage existing assets
  • Playwright-bdd: Integration with Playwright Test features (VRT, Fixtures, etc.)

The consistent message throughout this series has been the importance of clarifying "what do you need?" before making a choice.

  • Part 1: Selenium vs Playwright
  • Part 2: Java vs TypeScript
  • Part 3: Cucumber.js vs Playwright-bdd

Every choice involves trade-offs. Understand what your project needs and make the optimal choice.

This article is Part 3 of the Playwright series.

  • Part 1: From Selenium to Playwright
  • Part 2: TypeScript vs Java - Feature Differences by Language
  • Part 3: BDD Framework Comparison - Cucumber.js vs Playwright-bdd (this article)

Home

About Us

Services

Blog

Contact Us

Privacy Policy

Cookie

©ARRANGILITY SDN. BHD.

// pages/LoginPage.ts
import { Page } from '@playwright/test';

export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.page.getByLabel('Email').fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Login' }).click();
  }
}
// steps/login.steps.ts
import { createBdd } from 'playwright-bdd';
import { test } from '../fixtures';

const { Given, When, Then } = createBdd(test);

Given('I open the login page', async ({ loginPage }) => {
  await loginPage.goto();
});

When('I login with valid credentials', async ({ loginPage }) => {
  await loginPage.login('test@example.com', 'password123');
});

Then('the dashboard is displayed', async ({ page }) => {
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});