Testing with Playwright
Tags: testing, javascript, webdev, playwright
Published: 2024-04-10 15:21:03

# 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:

```js
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.
  },
});

```
## 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:

```bash
# 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

```


# How does it work ?

A Playwright test, basically and usually, performs three things;
```
Locate --> Act --> Assert
```
1) **Locate** something on the page via locators such as `getByRole()`
2) **Act** on the located element via actions 
```js
page
.getByRole('textbox') //locate the element
.fill('Peter'); //perform action
```
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:

```js

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

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

```
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](https://playwright.dev/docs/locators)

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

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:

```js
await usernameLocator.fill("myusername");
```

More on actions [here](https://playwright.dev/docs/input)

## Assertions

The final step is to evaluate the result:

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

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


```

More on assertions [here](https://playwright.dev/docs/test-assertions)

Our final test file will look like this:

```js
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');

});

```

# 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](https://playwright.dev/docs/running-tests)

# Good to knows
- You can also Contextualize tests with describe and nested tests

```js
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');
  });
});
```
- 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