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
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
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"
}
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");
Set default timeout to some reasonable amount of time
// cucumber.conf.js file
// in milliseconds
setDefaultTimeout(60000)
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();
});
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();
});
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();
});
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
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]
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.
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
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"
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
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';
});
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");
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 ';
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)
})
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)
})
Run the test
First of all, you need to run your application, in my case
npm run start
Now run the test and watch it in the browser
npm run test:e2e tests/acceptance/features/todo.feature
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)
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 (16)
Thanks for the good article!
Hi, I'm not sure where you're getting the
page
object from in yourtodoContext.js
file. This doesn't run for me while having the same config, and I getReferenceError: page is not defined
, any ideas why?Hi sorry for the late reply but page is created in
cucumber.conf.js
file and is set global so that it can be used across all the other filesit's still not working
After rename cucumber.config.js to cucumber.conf.js, everything ok.
When execute it i am getting,
but still i am getting undefined for steps even though those are defined
and the thread issue is still not working for me
FYR
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.
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.
Hello, I see your project - playwright-bdd-example. This is what I'm trying to implement. Could you please answer to my questions? Where you are launching the browser? Just giving the device in playwright-config.ts is enough? And the same project can be done in javascript as well?
Hello! Yes, just giving the device in playwright config is enough, b/c tests are actually normal Playwright tests. The same can be done in JavaScript as well
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
I think you should remove the following
// todo.feature
from your featurefile because of your lintersettings?
Oh, right. Did forget that. :)
Thanks a lot.
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.
Hi Brother. Do you have the same sample repo project for PlayWright with BDD frame work java. Do you have a mail id to reach out?
Thanks for this. I am just wondering how we can shard these tests and run in multiple machines at the same time?