Playwright + BDD: Building an End-to-End Test Automation Framework from Scratch
A practical guide to building a maintainable E2E test suite with Playwright, TypeScript and Gherkin.
Why this article? As a QA Engineer, I often hear that test automation is "just clicking buttons with code." In this post I walk through a production-ready approach: a BDD-driven E2E framework that is readable by devs/POs/QAs, easy to maintain, and green in CI — built with Playwright, TypeScript and playwright-bdd (Gherkin in Portuguese, the Belgium of the project's origin, but the pattern applies to any language).
This is a write-up of a portfolio project — the full, runnable source is on GitHub.
The stack
| Tool | Why |
|---|---|
| Playwright (1.6x) | Reliable cross-browser automation (Chromium + Firefox here) |
| TypeScript (strict) | Type safety across page objects and steps |
| playwright-bdd (9.x) | Writes scenarios in Gherkin and executes them with the Playwright runner |
| Page Objects (POM) | Centralized selectors, no hardcoding in tests |
| Allure Report | Rich report with steps, screenshots and history |
| GitHub Actions | CI that runs the suite and publishes the report on GitHub Pages |
Project structure
├── features/ # BDD scenarios (Gherkin)
│ ├── login.feature
│ ├── inventory.feature
│ └── checkout.feature
├── steps/ # Step definitions (bridge Gherkin to code)
├── fixtures/ # Extended fixtures + injected Page Objects
├── src/pages/ # Page Objects
├── support/ # Environment config (.env)
└── playwright.config.ts # Playwright + playwright-bdd config
1. BDD-first: write the feature, then the code
The beauty of BDD is the conversation. Scenarios are written in plain language, so Product Owners, Devs and QAs share the same understanding:
Feature: Login
Scenario: Login with valid credentials
Given I am on the login page
When I fill in the valid credentials
Then I am taken to the products page
2. Bind Gherkin to code with step definitions
Each Given/When/Then maps to a step definition that uses fixtures to inject Page Objects:
Given('I am on the login page', async ({ loginPage }) => {
await loginPage.open();
});
When('I fill in the valid credentials', async ({ loginPage }) => {
await loginPage.login(STANDARD_USER, PASSWORD);
});
Then('I am taken to the products page', async ({ inventoryPage }) => {
await inventoryPage.expectVisible();
});
3. Page Objects: keep selectors in one place
A Page Object encapsulates the page's elements and actions, so the framework stays maintainable when the UI changes:
export class LoginPage {
constructor(readonly page: Page) {
this.username = page.locator('#user-name');
this.password = page.locator('#password');
this.submit = page.locator('#login-button');
}
async login(user: string, pass: string) {
await this.page.goto('/');
await this.username.fill(user);
await this.password.fill(pass);
await this.submit.click();
}
}
4. Pluggable fixtures make steps concise
Using test.extend() + createBdd(), the Page Objects are injected into every step — no globals, no setup duplication.
5. Report & CI
- Allure gives a rich, visual report; the CI publishes it on GitHub Pages (a live, navigable link in your README).
-
Robustness is a feature:
workers: 1+ navigation retries tame flaky network access to public demo apps — the same lesson applies to single-session systems.
Takeaways
- BDD is a collaboration tool, not just syntax.
- Page Objects + typed fixtures = the difference between a script that breaks and a framework that scales.
- Green CI with published reports is what makes a portfolio credible.
Want the full code? Check out the open-source project linked on my GitHub, where all 7 scenarios run green in CI with an Allure report deployed to GitHub Pages.
Jessica Sales — QA Engineer, focused on end-to-end automation, API testing and applying AI to the QA lifecycle.
Top comments (0)