Modern E2E Testing with Cucumber, Playwright & Typescript

Shaun English
5 min readJan 24, 2024

--

In modern software development, end-to-end testing has become an essential part of the quality assurance process. However, setting up and executing E2E tests can be a time-consuming and complex task, even more so when migrating away from deprecated libraries such as Protractor.

In this article, we will explore how to streamline E2E testing using a combination of Playwright, Cucumber, and TypeScript.

When using cucumber-js with @playwright/test, the responsibilities of each library are as follows:

cucumber-js

This library will be used when defining the feature files and step definitions using Gherkin syntax. The feature files define the scenarios that will be tested, and the step definitions provide the code that implements the steps in those scenarios.

Cucumber parses your Gherkin feature files, executes the steps, and provides reporting. It is a self-contained tool that combines both the framework and the test runner functionalities.

@playwright/test

This library is responsible for browser automation. @playwright/test provides an api for browser automation to perform user interactions.

When these two libraries are used together, cucumber-js defines the scenarios and steps that will be tested, and @playwright/test launches and interacts with the browser to perform the actions specified in the step definitions. This allows you to write tests in a natural language format using Gherkin syntax, while utilising the power of Playwright for modern browser automation and testing.

Getting Started

Useful Visual Studio Code Extensions:

  • Cucumber (Gherkin) Full Support
    This plugin provides enhanced syntax highlighting, autocomplete, and formatting for Gherkin files, making it easier for developers to create and maintain BDD-style tests. The plugin also supports quick navigation between steps, outlining of feature files, and the ability to run tests directly from the editor.

Installation

Clone the project locally to your machine then run the following commands to install dependencies, then install browsers for Playwright :

npm i
npm run e2e:install

Config

Cucumber-js Config

const fs = require('fs');
const path = require('path');
const resultsDir = path.join(__dirname, 'e2e/results');

/**
* check if e2e/results dir exists, if not create
*/
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir);
}

/**
* Cucumber.js Config
* Cucumber profiles for different browser / devices
*/

// Node parameters set via command line or CI
const common = {
format: [
`json:./e2e/results/cucumber-report.json`,
'@cucumber/pretty-formatter',
'html:./e2e/results/cucumber-report.html',
],
requireModule: ['ts-node/register'],
require: [path.join(__dirname, 'e2e/**/*.ts')],
paths: [path.join(__dirname, 'e2e/features/')],
publishQuiet: true,
timeout: 30000,
worldParameters: {
headless: false,
appUrl: process.env.APP_URL || 'http://localhost:4200',
mockApiUrl: process.env.APP_URL || 'http://apimock:3000',
},
};

const local = {
...common,
};

const ci = {
...common,
parallel: 5, // run in parallel in pipeline to speed up test execution
worldParameters: {
...common.worldParameters,
headless: true,
},
};

const debug = {
...common,
mode: 'generate', // cucumber option for generating reference screenshots
paths: [], // set to empty as path defined in launch.json feature debug
};

module.exports = {
default: local,
debug: debug,
ci: ci,
};

Playwright Setup

Custom World

This example use only Chromium for running automation, it will need to be adapted if you wish to test against other browsers.

import { After, Before, BeforeAll, setDefaultTimeout, setWorldConstructor, World } from '@cucumber/cucumber';
import { defineConfig, Browser, BrowserContext, chromium, Page } from '@playwright/test';
import { HomePage } from './page_objects/homepage.po';

/** World.
* @class
* Test World is instantiated on each scenario and shares state between step definitions, this can be a reference
* to the browser object, page objects or any custom code - best practice is to create your page objects here
*/
export class TestWorld extends World {
browser!: Browser;
context!: BrowserContext;
page!: Page;
homePage!: HomePage;

/**
* @param {IWorldOptions=} opts
*/
constructor(opts: any) {
super(opts);
}

async init() {
this.browser = await chromium.launch({ headless: this.parameters.headless });
this.context = await this.browser.newContext();
this.page = await this.context.newPage();
this.homePage = new HomePage(this, this.page);

return this.page.goto(this.parameters.appUrl);
}

async destroy() {
await this.page.close();
await this.context.close();
await this.browser.close();
}
}

setWorldConstructor(TestWorld);
setDefaultTimeout(60000);

BeforeAll(() => {
defineConfig({
use: {
screenshot: 'on',
},
snapshotPathTemplate: 'e2e/results/screenshots/{testFilePath}/{arg}{ext}',
});
});

Before(async function (this: TestWorld) {
await this.init();
});

After(async function (this: TestWorld) {
await this.destroy();
});

Page Objects

import { expect } from '@playwright/test';
import { TestWorld } from 'e2e/world';

export class HomePage {
constructor(private world: TestWorld) {}

async navigateTo(): Promise<void> {
await this.world.page.goto(`${this.world.parameters.appUrl}`);
await expect(this.world.page).toHaveURL(this.world.parameters.appUrl);
}

async hasRunningText(): Promise<boolean> {
return this.world.page.isVisible('//span[text()="cucumber-playwright-starter app is running!"]');
}
}

VS Code Debug Config

A nice to have is the ability to debug all tests, individual feature files, or tagged scenarios, this launch.json enables this. Use args to pass specific commands to the cli.

{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "E2E Tests",
"type": "node",
"request": "launch",
"console": "integratedTerminal",
"program": "${workspaceFolder}/node_modules/@cucumber/cucumber/bin/cucumber-js",
"args": [
/* "--tags",
"@your-tags" */
]
},
{
"name": "E2E Current Feature",
"type": "node",
"request": "launch",
"console": "integratedTerminal",
"program": "${workspaceFolder}/node_modules/@cucumber/cucumber/bin/cucumber-js",
"args": [
"-p", // cucumberjs --profile
"debug", // use cucumberjs debug profile in cucumber.js config
"${relativeFile}" // run tests for current file (must be .feature)
]
}
]
}

BDD Testing

Features

Feature: Homepage Feature Testing

Example feature file

@test
Scenario: Loads homepage
Given I am on the homepage
Then I see a banner

Steps

import { Given, Then } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { TestWorld } from 'e2e/world';

Given('I am on the homepage', async function (this: TestWorld) {
await this.homePage.navigateTo();
expect(await this.page.title()).toEqual('CucumberPlaywrightStarter');
});

Then('I see a banner', async function (this: TestWorld) {
const el: boolean = await this.homePage.hasRunningText();
expect(el).toBeTruthy();
});

Reporting

Out of the box, Cucumber comes with reporting features, config fro this can be found in the cucumber config file. In addition to this, I have also installed the cucumber-html-reporter library to produce a report which is more visually appealing.

To produce this report run the following command after all the tests have executed:

npm run e2e:report-publish

CI / CD

When running your integration tests are part of a CI pipeline you can use the following npm script:

npm run e2e:ci

This will start the web server for the application, once this is running it will then run the e2e:test-ci script which will run all the cucumber tests.

Alternatively, you can run the following, which will do all the above, but also generate the cucumber-html-reporter formatted report and the end:

npm run e2e:ci-and-report

Summary

In conclusion, E2E testing is a crucial aspect of software development that ensures applications are functioning as intended. Cucumber, Playwright, and TypeScript provide developers with a modern and powerful set of tools for creating reliable, maintainable E2E tests.

With the ability to automate testing across multiple browsers and platforms, developers can save time and ensure that their applications are working consistently.

--

--