Masterclass Foundations What is Cucumber?
Hands-on · 3 hours · TypeScript

Add Cucumber BDD on top of your Playwright framework

Plain-English .feature files describe the behaviour; the steps delegate straight to the Page Objects you already have. No automation logic is duplicated — Cucumber is a readable front end over TTACart.

@cucumber/cucumber ts-node tsconfig-paths @playwright/test 13 scenarios · 42 steps

Concept

What is Cucumber — its three components.

Cucumber runs Behaviour-Driven Development: you write specifications in a structured plain language called Gherkin, and Cucumber executes them by matching each line to a step definition. Three moving parts:

1

Feature file

The Gherkin spec — Feature, Scenario, Given/When/Then. Readable by anyone on the team.

2

Step definitions

Each Gherkin line maps to a TypeScript function that drives an existing Page Object. The glue to your framework.

3

Support code

The World (per-scenario state + Page Objects) and hooks that launch and tear down the browser.

Given / When / Then are aliases. They behave identically — the words tell a story: Given a context, When an action, Then an outcome. And / But continue the previous keyword.

Setup

How it's wired into the framework.

Everything lives under src/cucumber/; nothing else changes.

src/cucumber/ ├── tsconfig.json # CommonJS override for ts-node ├── support/ │ ├── world.ts # CustomWorld: page + Page Objects per scenario │ └── hooks.ts # Chromium lifecycle + failure screenshots ├── level-00-installation/ # wiring smoke (1 scenario) ├── level-01-basic/ # Feature/Background/Given-When-Then (3 scenarios) └── level-02-data-driven/ # Scenario Outline + Data Table + JSON (9 scenarios)

The three glue pieces

A

cucumber.js

One profile per level. Sets TS_NODE_PROJECT to the CommonJS tsconfig before hooks load; registers ts-node + tsconfig-paths.

B

tsconfig.json

Root tsconfig is Node16, which ts-node can't require. This override flips to CommonJS and re-declares the path aliases.

C

support/

world.ts holds the page + POMs per scenario; hooks.ts launches/tears down Chromium and screenshots failures.

cucumber.js
// sets the CommonJS tsconfig before the requireModule hooks load
process.env.TS_NODE_PROJECT = process.env.TS_NODE_PROJECT || 'src/cucumber/tsconfig.json';

const common = {
  requireModule: ['ts-node/register', 'tsconfig-paths/register'],
  format: ['progress-bar', 'html:reports/cucumber/report.html', 'summary'],
};

module.exports = {
  level1: { ...common,
    require: ['src/cucumber/support/**/*.ts', 'src/cucumber/level-01-basic/steps/**/*.ts'],
    paths:   ['src/cucumber/level-01-basic/features/**/*.feature'] },
  // level2 also loads level1 steps — login steps are reused
};
support/world.ts
export class CustomWorld extends World {
  page!: Page;
  loginPage!: LoginPage;
  cartPage!: CartPage;        // …all the existing Page Objects

  initPages(): void {       // called from the Before hook
    this.loginPage = new LoginPage(this.page);
    this.cartPage  = new CartPage(this.page);
  }
}
setWorldConstructor(CustomWorld);
support/hooks.ts
BeforeAll(async () => { browser = await chromium.launch({ headless: !process.env.HEADED }); });

Before(async function (this: CustomWorld) {
  this.context = await browser.newContext({ baseURL: BASE_URL });
  this.page = await this.context.newPage();
  this.initPages();
});

After(async function (this: CustomWorld, { result }) {
  if (result?.status === Status.FAILED) this.attach(await this.page.screenshot(), 'image/png');
  await this.page?.close();
});

Level 0

Installation & wiring smoke.1 scenario

Install the three packages, then prove the whole toolchain runs end to end.

terminal
npm install -D @cucumber/cucumber ts-node tsconfig-paths
npx playwright install chromium
npm run cucumber:level0
smoke.feature
@level0 @smoke
Feature: Cucumber + Playwright wiring (Level 0)

  Scenario: The TTACart login page loads
    Given I open the TTACart login page
    Then the page title should contain "TTACart"
smoke.steps.ts
Given('I open the TTACart login page', async function (this: CustomWorld) {
  await this.page.goto(LoginPage.PATH);
});

Then('the page title should contain {string}', async function (this: CustomWorld, expected: string) {
  await expect(this.page).toHaveTitle(new RegExp(expected, 'i'));
});
✔ 1 scenario (1 passed) · 2 steps (2 passed)

Level 1

Basic scenarios.3 scenarios

Feature + Background + Given/When/Then. A Background runs before every scenario — here it opens the login page. The steps call straight into LoginPage and InventoryPage.

login.feature
@level1 @login
Feature: TTACart Login (Level 1 — basic scenarios)

  Background:
    Given I am on the TTACart login page

  @smoke @P0
  Scenario: A standard user can log in
    When I log in as "standard_user" with password "tta_secret"
    Then I should land on the products page

  @negative
  Scenario: A locked-out user is refused
    When I log in as "locked_out_user" with password "tta_secret"
    Then I should see a login error containing "locked out"
login.steps.ts
When('I log in as {string} with password {string}',
  async function (this: CustomWorld, username: string, password: string) {
    await this.loginPage.loginAs(username, password);   // ← reuses the POM
  });

Then('I should land on the products page', async function (this: CustomWorld) {
  await this.inventoryPage.assertLoaded();
});
✔ 3 scenarios (3 passed) · 9 steps (9 passed)
Negative copy. "locked out" and "do not match" must match the real TTACart error strings — the step asserts [data-test="error"] contains them.

Level 2

Data-driven techniques.9 scenarios

The three ways Cucumber feeds data into one scenario body:

Scenario Outline

One scenario, many Examples rows. Each row re-runs the body with its placeholders filled in.

Data Table

A table passed into a single step — add N products in one When via table.hashes().

{}

External JSON

Personas read from data/customers.json; a full checkout runs per persona.

Scenario Outline

login-outline.feature
  Scenario Outline: <username> logging in ends on the <outcome>
    When I log in as "<username>" with password "<password>"
    Then I should see the "<outcome>"

    Examples: valid users
      | username                | password   | outcome  |
      | standard_user           | tta_secret | products |
      | problem_user            | tta_secret | products |
      | performance_glitch_user | tta_secret | products |

Data Table

cart-datatable.feature + steps
    When I add the following products to the cart:
      | productId                    |
      | tta-practice-backpack        |
      | tta-bike-light               |
      | test-allthethings-tshirt-red |

// step — one When consumes the whole table
When('I add the following products to the cart:', async function (this: CustomWorld, table: DataTable) {
  for (const { productId } of table.hashes()) await this.inventoryPage.addToCart(productId);
});

External JSON

checkout.steps.ts
const customers = JSON.parse(readFileSync(join(__dirname, '../data/customers.json'), 'utf-8'));

When('I check out as the {string} customer', async function (this: CustomWorld, persona: string) {
  const customer = customers[persona];
  await this.cartPage.checkout();
  await this.checkoutStepOnePage.fillGuest(customer);    // ← reuses the POM
  await this.checkoutStepOnePage.continue();
  await this.checkoutStepTwoPage.finish();
});
✔ 9 scenarios (9 passed) · 31 steps (31 passed)

Print-friendly reference · pin this above your desk

Cucumber × Playwright · full cheat sheet.

Every Gherkin keyword, npm script, data technique, and config knob for this BDD layer — on one screen. Use ⌘P to print; the grid collapses to A4.

🥒 Gherkin keywords
Structure
FeatureTop-level grouping
BackgroundRuns before each Scenario
ScenarioOne concrete example
RuleGroup scenarios by rule
Steps
GivenA starting context
WhenAn action
ThenAn expected outcome
AndBut
▦ Data techniques
Parameterise
Scenario OutlineTemplated body
Examples:Rows feed the outline
<name>Placeholder token
In a step
DataTableTable arg to one step
table.hashes()Rows as objects
{string}{int}
External
readFileSyncLoad JSON personas
⌨ npm scripts
Run a level
cucumber:level01 · 2 steps
cucumber:level13 · 9 steps
cucumber:level29 · 31 steps
cucumberAll levels
cucumber:headedWatch browser
Filter / env
--tags "@negative"
--profile level2
BASE_URL=…HEADED=1
⚙ Wiring files
Config
cucumber.jsProfiles per level
tsconfig.jsonCommonJS override
Support
world.tsPage + POMs
hooks.tsBrowser lifecycle
Hooks
BeforeAllLaunch Chromium
BeforeNew context + POMs
AfterScreenshot on fail

Reference

Commands in full.

CommandWhat it runs
npm run cucumber:level0Level 0 — 1 scenario / 2 steps
npm run cucumber:level1Level 1 — 3 scenarios / 9 steps
npm run cucumber:level2Level 2 — 9 scenarios / 31 steps (loads Level 1 steps too)
npm run cucumberEvery level (default profile)
npm run cucumber:headedHEADED=1 — watch the browser
npx cucumber-js --profile level2 --tags "@datatable"Filter by tag inside a profile
BASE_URL=… npm run cucumberPoint at a different environment
Report: an HTML report renders to reports/cucumber/report.html (gitignored) after every run.

Workshop

A 3-hour agenda.

TimeTopic
0:00–0:20What is BDD? The three components; Given/When/Then.
0:20–0:45Level 0 — install, cucumber.js, the CommonJS tsconfig, first green run.
0:45–1:30Level 1 — Feature/Background, step definitions, the World, reusing Page Objects.
1:30–1:45Break.
1:45–2:40Level 2 — Scenario Outline, Data Table, external JSON; hooks + failure screenshots.
2:40–3:00Tag filtering, the HTML report, wrap-up + Q&A.

Context

How we automated TTACart.

Cucumber sits at the top of a layered Playwright framework. Each layer has one job; the BDD steps only ever talk to the Page Object layer:

LayerLives inResponsibility
Gherkin featuressrc/cucumber/**/featuresDescribe behaviour in plain English.
Step definitionssrc/cucumber/**/stepsMap each line to a Page Object call.
Page Objectssrc/pagesEncapsulate selectors + actions per screen.
Element utilitiessrc/utils/UtilElementLocatorLogged, timeout-aware locator wrappers.
Logger / reportersrc/utilsWinston logs + the custom TTA HTML report.

The custom TTA report

TTA custom report — stats dashboard, environment bar, and filterable test table

Overview — stats dashboard, environment bar, and the filterable test table.

TTA custom report — expanded end-to-end checkout flow with per-step screenshots and video

End-to-end flow — each step shows its log line and a screenshot, then the gallery and run video.

Help

Troubleshooting.

SymptomCause / fix
Cannot use import statement outside a moduleTS_NODE_PROJECT isn't pointing at src/cucumber/tsconfig.json — run via the npm scripts.
Cannot find module '@pages/…'tsconfig-paths/register missing, or aliases not mirrored in the Cucumber tsconfig.
Undefined step (yellow)The Gherkin text doesn't match any step definition; check the profile loads that steps file.
Negative-login assertion failsTTACart error copy changed — update the "locked out" / "do not match" fragments.
Timeouts on a slow networkBump setDefaultTimeout(...) in hooks.ts, or run HEADED=1 to watch.
Type error only in npm run typecheckThe root tsconfig (Node16) checks the same files; transpileOnly hides it at runtime — fix the type.