DEV Community

Cover image for Behavior Driven Development (BDD) using Playwright
Swikriti Tripathi for JankariTech

Posted on • Updated on

Behavior Driven Development (BDD) using Playwright

Playwright is an open-source NodeJS framework for browser automation. It is developed by Microsoft and the development team has members that were involved in developing Puppeteer for Google.

One of the main features of Playwright is that it can automate Chromium, Webkit, and Firefox browsers with a single API. Along with being cross-browser, it is cross-platform and cross-language, supporting the major OS like Windows, Linux, Mac and languages like TypeScript, JavaScript, Python, .NET, Java. Playwright also comes with tools like codgen - which lets you generate automatic code by recording your actions, you can find out more about Playwright on their official website.

For this blog, we will be implementing BDD in Playwright. I have a small to-do web app and I'm going to be setting up Playwright in the same. If you want to follow through you can fork and clone the project from here. If you have your web application you can set up Playwright there as well. Let's get started!

Note: the whole setup is done in Ubuntu 20.04.3 LTS, so some setup steps might differ depending on your platform

Prerequisites

  • Node.js version 12 or above. If you don't already have node installed in your system you can use this blog as a guide

Note Only Ubuntu 18.04 and Ubuntu 20.04 are officially supported.

Installation

Run from your project's root directory

   npm i -D @playwright/test
   npm i -D playwright 
   npx playwright install
Enter fullscreen mode Exit fullscreen mode

Playwright doesn't come with the built-in support for BDD so we are going to use the help of another tool Cucumber

   npm i -D @cucumber/cucumber@7.3.1 @cucumber/pretty-formatter
Enter fullscreen mode Exit fullscreen mode

After this, devDependencies in your package.json should look something like this

// package.json file

"devDependencies": {
    "@cucumber/cucumber": "^7.3.1",
    "@cucumber/pretty-formatter": "^1.0.0-alpha.1",
    "@playwright/test": "^1.18.0",
    "playwright": "^1.18.1"
  }
Enter fullscreen mode Exit fullscreen mode

Configuration

We are going to use Cucumber to run our tests so we need to have a configuration file for it. At the root level of your project create a file cucumber.conf.js

First of all, we are going to require the following:

// cucumber.conf.js file

const { Before, BeforeAll, AfterAll, After, setDefaultTimeout } = require("@cucumber/cucumber");
// you can choose other browsers like webkit or firefox according to your requirement
const { chromium } = require("playwright");
Enter fullscreen mode Exit fullscreen mode

Set default timeout to some reasonable amount of time

// cucumber.conf.js file

// in milliseconds
setDefaultTimeout(60000)
Enter fullscreen mode Exit fullscreen mode

Add the following code snippet to your file

// cucumber.conf.js file

// launch the browser
BeforeAll(async function () {
   global.browser = await chromium.launch({
       headless: false,
       slowMo: 1000,
   });

});

// close the browser
AfterAll(async function () {

   await global.browser.close();
});
Enter fullscreen mode Exit fullscreen mode

In the above snippet of code, we launch a chrome browser where our tests will be automated. You can launch a different one as per your requirement, just make sure you import the correct browser. We run the browser in the headed mode which can be done by setting headless:false, this means that when the test is running we can see it being automated in the browser. You can set it to true if you don't want to see the test running but where is the fun in that? Another option is slowMo which slows down Playwright operations by the specified amount of milliseconds and can be helpful to watch the test run. There are various options that can be set while launching the browser, you can go through all of them here. After we've finished our operations we will close the browser. This configuration is for before/after all of the tests are run. Now we need to configure what happens when each test scenario is run. For this look at the snippet below:

// cucumber.conf.js file

// Create a new browser context and page per scenario
Before(async function () {
   global.context = await global.browser.newContext();
   global.page = await global.context.newPage();
});

// Cleanup after each scenario
After(async function () {
   await global.page.close();
   await global.context.close();
});

Enter fullscreen mode Exit fullscreen mode

After we've launched our browser we need to create a new browser context. Playwright allows creating incognito browser contexts with browser.newContext([options]) method. Each browser context has its page that provides methods to interact with a single tab in a browser. We can create a page with context.newPage() method. Similar to launching a browser we can set a lot of options while creating a browser context as well like screenshots, record video, geolocation, and more, you can go through all of them here. After we've finished with our operations we close the page and context.

Voila, we're done with the configuration part. The whole cucumber.conf.js file looks like this :

// cucumber.conf.js file

const { Before, BeforeAll, AfterAll, After, setDefaultTimeout } = require("@cucumber/cucumber");
const { chromium } = require("playwright");

setDefaultTimeout(60000)

// launch the browser
BeforeAll(async function () {
   global.browser = await chromium.launch({
       headless: false,
       slowMo: 1000,
   });

});

// close the browser
AfterAll(async function () {

   await global.browser.close();
});

// Create a new browser context and page per scenario
Before(async function () {
   global.context = await global.browser.newContext();
   global.page = await global.context.newPage();
});

// Cleanup after each scenario
After(async function () {
   await global.page.close();
   await global.context.close();
});

Enter fullscreen mode Exit fullscreen mode

Writing Tests

Now some fun stuff, we start writing tests!

Our file structure will look like this

📦tests
┗ 📂acceptance
┃ ┣ 📂features
┃ ┃ ┗ 📜todo.feature
┃ ┗ 📂stepDefinitions
┃ ┃ ┗ 📜todoContext.js

Enter fullscreen mode Exit fullscreen mode

Following the above tree create file tests/acceptance/features/todo.feature. As we are using BDD, we are going to start by writing a feature file and we are going to be using Gherkin language to do so. If you don't know how to write a feature file or what Gherkin is you can take the help of the following blogs as it's out of the scope of this blog and won't be explained in detail.

Here's a basic syntax of what a feature file looks like

Feature: a short description of a software feature
As a user
I want to do this
So I can achieve that

Scenario: name of the scenario
Given [Preconditions or initial context of the system ]
When [Event or action]
Then [Expected output]
Enter fullscreen mode Exit fullscreen mode

Now assuming you've got some knowledge of feature files and how to write them we proceed further.

The application that I'm going to be testing is a todo app and the UI looks like this.

Homepage of the app

I want to test if the item I've added is displayed on the UI or not. And the feature file looks like this.

// todo.feature

Feature: todo
 As a user
 I want to add an item to the todo list
 So that I can organize tasks

 Scenario: Add item to the todo list
   Given a user has navigated to the homepage
   # the text inside the quote works as a variable that can be passed to a function
   When the user adds "test" to the todo list using the webUI
   Then card "test" should be displayed on the webUI
Enter fullscreen mode Exit fullscreen mode

Now we implement each step of the scenario using Playwright! Create a context file tests/acceptance/stepDefinitions/todoContext.js. We can get a boilerplate for each step in the scenario where we can provide our implementation. For that add the following script in your package.json file.

"test:e2e": "cucumber-js --require cucumber.conf.js --require tests/acceptance/stepDefinitions/**/*.js --format @cucumber/pretty-formatter"

Enter fullscreen mode Exit fullscreen mode

We will be using the test:e2e script for running the test. Now go to your terminal and run the script

npm run test:e2e tests/acceptance/features/todo.feature
Enter fullscreen mode Exit fullscreen mode

This will run your feature file. As the steps aren't implemented yet you will get something like this on your screen.

? Given a user has navigated to the homepage
      Undefined. Implement with the following snippet:

        Given('a user has navigated to the homepage', function () {
          // Write code here that turns the phrase above into concrete actions
          return 'pending';
        });

  ? When the user adds "test" to the todo list using the webUI
      Undefined. Implement with the following snippet:

        When('the user adds {string} to the todo list using the webUI', function (string) {
          // Write code here that turns the phrase above into concrete actions
          return 'pending';
        });

  ? Then card "test" should be displayed on the webUI
      Undefined. Implement with the following snippet:

        Then('card {string} should be displayed on the webUI', function (string) {
          // Write code here that turns the phrase above into concrete actions
          return 'pending';
        });

Enter fullscreen mode Exit fullscreen mode

You can now add the generated snippets into your context file and start implementing them.

Import following

// todoContext.js file

const {Given, When, Then} = require('@cucumber/cucumber')
// import expect for assertion
const { expect } = require("@playwright/test");
Enter fullscreen mode Exit fullscreen mode

Define your launch url and selectors for different UI elements as per need, these are project specific. Playwright supports CSS and Xpath selectors. You can find the detailed information about them here

// todoContext.js file

//launch url
const url = 'http://localhost:3000'

//define selectors
const homepageElement = '.borderTodo'
const todoInput = '.todo-input';
const todoButton = '.todo-button';
const todoItem = '.todo .todo-item ';
Enter fullscreen mode Exit fullscreen mode

Now we can implement the individual test steps, like so

// todoContext.js file

Given('a user has navigated to the homepage', async function () {
   // navigate to the app
   await page.goto(url)
   // locate the element in the webUI
   const locator = await page.locator(homepageElement)
   // assert that it's visible
   await expect(locator).toBeVisible()
})

When('the user adds {string} to the todo list using the webUI',async function (item) {
   // fill the item that was input from the feature file , to the inputText field in the UI
   await page.fill(todoInput , item)
   // click the button
   await page.click(todoButton)
})

Then('card {string} should be displayed on the webUI',async function (item) {
   // get text of the item that is visible in the UI
   const text = await page.innerText(todoItem)
   // assert that its name is similar to what we provided
   await expect(text).toBe(item)
})

Enter fullscreen mode Exit fullscreen mode

You can find different methods available to interact with UI elements like click, fill and so on in Playwright's official documentation, it's very nicely explained how the function works along with the example code.

We use the page that we created in the before hook to interact with various web elements. Playwright performs autowait and performs a range of actionability checks on elements and ensures that elements are ready to perform the expected operation. This is one of its plus points.

This is the whole context file

// todoContext.js file

const {Given, When, Then} = require('@cucumber/cucumber')
// import expect for assertion
const { expect } = require("@playwright/test")

//launch url
const url = 'http://localhost:3000'

//define selectors
const homepageElement = '.borderTodo'
const todoInput = '.todo-input'
const todoButton = '.todo-button'
const todoItem = '.todo .todo-item '


Given('a user has navigated to the homepage', async function () {
   // navigate to the app
   await page.goto(url)
   // locate the element in the webUI
   const locator = page.locator(homepageElement)
   // assert that it's visible
   expect(locator).toBeVisible()
})

When('the user adds {string} to the todo list using the webUI',async function (item) {
   // fill the item that was input from the feature file , to the inputText field in the UI
   await page.fill(todoInput , item)
   // click the button
   await page.click(todoButton)
})

Then('card {string} should be displayed on the webUI',async function (item) {
   // get text of the item that is visible in the UI
   const text = await page.innerText(todoItem)
   // assert that its name is similar to what we provided
   expect(text).toBe(item)
})

Enter fullscreen mode Exit fullscreen mode

Run the test

First of all, you need to run your application, in my case

npm run start
Enter fullscreen mode Exit fullscreen mode

Now run the test and watch it in the browser

npm run test:e2e tests/acceptance/features/todo.feature
Enter fullscreen mode Exit fullscreen mode

You should get a log similar to this one.

Feature: todo # tests/acceptance/features/todo.feature:1

 As a user
 I want to add an item to the todo list
 So that I can organize tasks

 Scenario: Add item to the todo list # tests/acceptance/features/todo.feature:6
   Given a user has navigated to the homepage
   When the user adds "test" to the todo list using the webUI
   Then card "test" should be displayed on the webUI

1 scenario (1 passed)
3 steps (3 passed)
0m04.266s (executing steps: 0m04.010s)
Enter fullscreen mode Exit fullscreen mode

Hopefully, your test also passed like mine and you got to learn about a new library.
You can extend the feature file to add more scenarios or add multiple feature files, implement the Page Object Model as per your requirement and it should all work the same.

You can find the source code of this implementation here

Top comments (13)

Collapse
 
charliewhu profile image
Charlie Avery • Edited

Hi, I'm not sure where you're getting the page object from in your todoContext.js file. This doesn't run for me while having the same config, and I get ReferenceError: page is not defined, any ideas why?

Collapse
 
swikritit profile image
Swikriti Tripathi

Hi sorry for the late reply but page is created incucumber.conf.js file and is set global so that it can be used across all the other files

Before(async function () {
   global.context = await global.browser.newContext();
   global.page = await global.context.newPage();
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
parsh07_20 profile image
Prashant Patil

it's still not working

Image description

Image description

Collapse
 
jwsheen profile image
James W. Shin

After rename cucumber.config.js to cucumber.conf.js, everything ok.

Thread Thread
 
parsh07_20 profile image
Prashant Patil

When execute it i am getting,

Image description

but still i am getting undefined for steps even though those are defined

Image description

Image description

and the thread issue is still not working for me
FYR

Image description

Thread Thread
 
akwasin profile image
akwasin

About the import issue, there's a few possible solutions.
One could be to add "type": "module" to your package.json. Another cause could depend on if you're using typescript commonJS or esNext. take a look at those two possible fixes.

Collapse
 
ankitverma profile image
Ankit Verma

Thanks for the good article!

Collapse
 
shaney profile image
shane young

Hi, Thank you very much for the sharing. It is great, but when start to run, I always got this error:
Parse error in "tests\acceptance\features\todo.feature" (1:1): expected: #EOF, #Language, #TagLine, #FeatureLine, #Comment, #Empty, got '// todo.feature'

I am still learning. Could you please give me some help?
Thank

Shane

Collapse
 
onlyplanb profile image
CodeCase • Edited

I think you should remove the following

// todo.feature

from your featurefile because of your lintersettings?

Collapse
 
shaney profile image
shane young

Oh, right. Did forget that. :)
Thanks a lot.

Collapse
 
vitalets profile image
Vitaliy Potapov • Edited

Thanks for the article!
Inspired by it I've created playwright-bdd package that allows to run Cucumber tests with Playwright. The only difference is that it uses Playwright as a test runner and utilizes Cucumber as a library.

Collapse
 
richardcariven profile image
RichardCariven

github.com/vitalets/playwright-bdd there is now a dedicated PLaywright-BDD runner which allows you to write Cucumber tests using the playwright runner directly.

It really streamlines the code, means you can directly use the Playwright.config.ts file to store all your browser config, and automatically creates a new context for every test. You can also use Playwright Fixtures to streamline your test and your framework. I have been porting our Cucumber project across and don't think I will use the cucumber runner ever again.

Collapse
 
vinny06 profile image
arvinder06

Thanks for this. I am just wondering how we can shard these tests and run in multiple machines at the same time?