Testing with Playwright

hatemtemimi

Hatem Temimi

Posted on April 10, 2024

Testing with Playwright

Intro

In this brief walkthrough, we will learn how to create and run a test with playwright, an End to End testing library.
Basic javascript // typescript knowledge and a frontend project that you want to test, are required.
This tutorial will cover the playwright fundamentals, and I will go into more advanced subjects in future articles, such as authentication and running tests on ci for example.

If my code runs, why should i test it ?

Once the code works, we usually go through some cleanup, and then we call it a day, with a bit of luck, it might not cause problems, but.. Hear me out on this; Even if code runs and appears to be functioning correctly, there can still be hidden bugs, edge cases, and performance issues that may only become apparent later on, once we start stockpiling lines of code.
Testing helps to uncover these problems and ensure that the code is working as intended in various situations and conditions. Additionally, testing helps to ensure that future changes to the code do not break existing functionality.
In other words, testing helps to ensure that the code is reliable, maintainable, and fit for its intended purpose. By thoroughly testing code, you can increase confidence in its behavior and reduce the likelihood of problems arising in the future.

But what is playwright ?

Playwright is the Javascript library we will be using the test our code to perform End To End Tests. When we run the tests, it will start a browser and run through the set of tests we have defined through the code.
Playwright's particularity in the market now, is that it can run tests on any browser, compared to it's competitors who do not cover web-kit which is the browser engine used by safari.

Installation

  • npx is used to install and run playwright (npx runs npm packages)
  • npx playwright install && npx playwright install-deps will install the required dependencies in your projects

Configuration

In the root of the your project, create a file named: playwright.config.ts this file will hold the playwright's default configuration to run the tests with, it can be overriden from the test file itself, but that's a subject for another day.
Here is an example config file that goes through the different configuration attributes:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Look for test files in the "tests" directory, relative to this configuration file.
  testDir: 'tests', // Value: A string representing the directory path.

  headless: false // Explanation below 

  // Run all tests in parallel.
  fullyParallel: true, // Value: true or false.

  // Fail the build on CI if you accidentally left test.only in the source code.
  forbidOnly: !!process.env.CI, // Value: true or false.

  // Retry on CI only.
  retries: process.env.CI ? 2 : 0, // Value: A number, typically 0 or more retries.

  // Opt out of parallel tests on CI.
  workers: process.env.CI ? 1 : undefined, // Value: A number representing the number of workers or undefined.

  // Reporter to use
  reporter: 'html', // Value: Name of the reporter to use, e.g., 'html', 'dot', etc.

  use: {
    // Base URL to use in actions like `await page.goto('/')`.
    baseURL: 'http://127.0.0.1:3000', // Value: A string representing the base URL.

    // Collect trace when retrying the failed test.
    trace: 'on-first-retry', // Value: 'on-first-retry' or 'off'.
  },

  // Configure projects for major browsers.
  projects: [
    {
      name: 'chromium', // Value: Name of the browser, e.g., 'chromium'.
      use: { ...devices['Desktop Chrome'] }, // Value: Configuration object for the browser.
    },
  ],

  // Optionally run your local dev server before starting the tests.
  webServer: {
    command: 'npm run start', // Value: A string representing the command to start the local server.
    url: 'http://127.0.0.1:3000', // Value: A string representing the URL of the local server.
    reuseExistingServer: !process.env.CI, // Value: true or false.
  },
});

Enter fullscreen mode Exit fullscreen mode

Headless vs Headed browsing

the headless attribute will tell playwright wether to start the testing browsers with or without a GUI, the browser you are using right now to read this, is most likely in headed mode, which means the GUI is running and you can navigate the interface. With headless browsing there is no GUI, which means it starts and runs faster, this mode comes in handy with large numbers of automated tests, since it makes the execution time significantly faster.
For our case, we will start the browsers in headed mode, so you can have a more visual experience, you can change it later.

With headless=false which means the browser will run with a GUI,
If you had errors with the browsers starting, you can try the following commands:

# Install Xvfb (X Virtual Framebuffer) for headless browsing.
# This package provides a virtual display server for running the browser in headless mode.
sudo apt-get install xvfb

# Start Xvfb on display :99 in the background.
# Xvfb creates a virtual display that allows the browser to render without a physical screen.
Xvfb :99 &

# Set the DISPLAY environment variable to point to the virtual display.
# This ensures that the browser renders its output to the virtual display created by Xvfb.
export DISPLAY=:99

Enter fullscreen mode Exit fullscreen mode

How does it work ?

A Playwright test, basically and usually, performs three things;

Locate --> Act --> Assert
Enter fullscreen mode Exit fullscreen mode

1) Locate something on the page via locators such as getByRole()
2) Act on the located element via actions

page
.getByRole('textbox') //locate the element
.fill('Peter'); //perform action
Enter fullscreen mode Exit fullscreen mode

3) Assert the result of the performed action via assertions like expect(page).toHaveTitle('some title');

Now that we know all the steps of the test we will go through an idiomatic example that illustrates each one of those:

Your First Test

As per the config file we created, we said we will store our tests in the directory /tests, so we will create that directory and create our first test file inside of it: username.spec.ts with this boiler plate:


import { test, expect } from '@playwright/test';

test('has username', async ({ page }) => {
 //
 // testing code here
});

Enter fullscreen mode Exit fullscreen mode

This will create a test named 'has username', that we will eventually run once we fill the test logic part, for now, note that we have access to the 'page' object, which we will be using later to navigate.

Locators

Locators are used to get elements on pages, once you get the element with the locator you are free to perform actions on it and assert the results, there are a multitude of locators depending on the situation:

  • page.getByRole() to locate by explicit and implicit accessibility attributes.
  • page.getByText() to locate by text content.
  • page.getByLabel() to locate a form control by associated label's text.
  • page.getByPlaceholder() to locate an input by placeholder.
  • page.getByAltText() to locate an element, usually image, by its text alternative.
  • page.getByTitle() to locate an element by its title attribute.
  • page.getByTestId() to locate an element based on its data-testid attribute (other attributes can be configured).

In our example we will use getByRole()

more on locators here

await page.goto('/login')
const usernameLocator = await page.getByRole('input', { name: 'username' })
Enter fullscreen mode Exit fullscreen mode

Our input element is now captured and stored in the usernameLocator variable, now to the second step: Perform an action on the element

Actions

For the sake of simplicity, we will fill the username input, and that will be our action:

await usernameLocator.fill("myusername");
Enter fullscreen mode Exit fullscreen mode

More on actions here

Assertions

The final step is to evaluate the result:

// Evaluate the content of our input
await expect(usernameLocator).toContainText('myusername');  

// The other way around using .not
await expect(usernameLocator).not.toContainText('some text');


Enter fullscreen mode Exit fullscreen mode

More on assertions here

Our final test file will look like this:

import { test, expect } from '@playwright/test';

test('has username', async ({ page }) => {

// Locate
await page.goto('/login')
const usernameLocator = await page.getByRole('input', { name: 'username' })

// Act
await usernameLocator.fill("myusername");

// Assert

await expect(usernameLocator).toContainText('myusername');  

await expect(usernameLocator).not.toContainText('some text');

});

Enter fullscreen mode Exit fullscreen mode

Running tests

By default, npx playwright test will run all the tests located in the folder chosen in the playwright.config.ts, which is /tests in our case
You can run only specific tests in a file, by mentionning the file name as an argument:

npx playwright test username.spec.ts

More on running test here

Good to knows

  • You can also Contextualize tests with describe and nested tests
import { test, expect } from '@playwright/test';

//this can be used to group tests or to give more context
test.describe('navigation', () => {

//A particularly useful hook, since it can be used for authentication for example
  test.beforeEach(async ({ page }) => {
    // Go to the starting url before each test.
    await page.goto('/login');
  });

  test('main navigation', async ({ page }) => {
    // Assertions use the expect API.
    await expect(page).toHaveURL('/login');
  });
});
Enter fullscreen mode Exit fullscreen mode
  • VSCode Playwright plugin makes your life a little bit easier(am a cli guy)
  • You do not need to think about racing tests with playwright, it manages the event loop for you.
  • npx killp PORT_NUMBER will kill ports for you just run npx killp and it will prompt you for installation
💖 💪 🙅 🚩
hatemtemimi
Hatem Temimi

Posted on April 10, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related