Skip to content

E2E Testing the GUI with Playwright

iaktern edited this page Jan 29, 2024 · 7 revisions

We use Playwright for end-to-end testing during development and prior to deployment via our CI system.

Playwright uses similar semantics as Jest for unit tests, except the tests are executed in a headlesss browser environment. They usually consist of actions a typical user might take (like creating or deleting processes) and then use assertions on certain DOM nodes to check if the website functions as expected.

You can find the documentation for Playwright here and also some common best practices.

Example

The following code snippet shows how a Playwright test might look like.

It encapsulates a test case with test('what is expected', async () => { /* test code */ }). The page prop is used to navigate, use locators to find DOM nodes and execute user actions. By wrapping a locator in expect() you can assert that certain conditions are true and thus that the test should succeed.

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

test('allows to add a new process', async ({ page }) => {
  // Add a new process.
  await page.goto('https://localhost:33083/#/process');
  await page.getByTestId('directions').click(); // HTML element needs a data-testid="directions" attribute
  await page.getByRole('button', { name: 'Add' }).click();
  await page.getByLabel('Name*').fill('My Process');
  await page.getByLabel('Description').fill('Very important process!');
  await page.getByLabel('Department', { exact: true }).click();
  await page.getByRole('option', { name: 'Marketing' }).locator('i').click();
  await page.getByRole('option', { name: 'Purchasing' }).locator('i').click();
  
  await page.locator('div').filter({ hasText: 'Add Process' }).first().click(); // just an example, locator() is not recommended to use since it relies on the DOM structure
  await page.getByRole('button', { name: 'Add Process' }).click();

  // Check the process editor was opened.
  await expect(page).toHaveURL(/#\/process\/bpmn\/.*/);
  await expect(page.getByText('My Process Latest Version')).toBeVisible();
  await expect(page.locator('rect').first()).toBeVisible(); // locator() is not recommended to use since it relies on the DOM structure

  // Check the new process is listed.
  await page
    .getByRole('tablist')
    .filter({ hasText: 'My Process Clear tab bar' })
    .getByRole('button')
    .nth(1) // nth() is not recommended to use since it relies on the DOM structure
    .click();
  await expect(page.getByText('My Process')).toBeVisible();
  await expect(page.getByRole('cell', { name: 'Marketing Purchasing' })).toBeVisible();
});

Some Best Practices for Selecting Elements

From:

...which element to use when multiple elements match, through locator.first(), locator.last(), and locator.nth(). These methods are not recommended because when your page changes, Playwright may click on an element you did not intend. Instead, follow best practices above to create a locator that uniquely identifies the target element.

Testing by test ids is the most resilient way of testing as even if your text or role of the attribute changes the test will still pass. QA's and developers should define explicit test ids and query them with page.getByTestId(). However testing by test ids is not user facing. If the role or text value is important to you then consider using user facing locators such as getByRole() and getByText() locators.

  • it is good to use explicit IDs (data-testid="") on the highest level
    • it is difficult to use IDs on reusable components
  • getByRole() und getByText() should be used if we want to explicitly test the UI - it should have a reason (don't test every text on your page - test the functionality

When to use testid locators: You can also use test ids when you choose to use the test id methodology or when you can't locate by role or text.

CSS and XPath are not recommended as the DOM can often change leading to non resilient tests. Instead, try to come up with a locator that is close to how the user perceives the page such as role locators or define an explicit testing contract using test ids.

  • That means locator(...) should not be used

Folder structure

All our tests should be located in the top-level tests folder to encourage de-coupling from implementation details.

We create separate folders for each "page" in our application. That might be one menu item (e.g. "Tasklist") or a sufficiently complex part of the GUI to justify its own dedicated space for tests (e.g. the BPMN editor as part of the processes tab).

  • tests
    • tasklist

      • tasklist.page.ts
      • tasklist.fixtures.ts
      • tasklist.spec.ts
    • processes

      • processes.page.ts
      • processes.fixtures.ts
      • processes.spec.ts

      ...

In general, the scaffolding for an e2e test consists of three files. The first is a file for commonly re-used actions and locators (tasklist.page.ts above). This might look like the following example:

import { Locator, Page } from '@playwright/test';

export class TasklistPage {
  readonly page: Page;
  readonly emptyTasklist: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emptyTasklist = page.getByText('There are currently no tasks in your queue.');
  }

  async goto() {
    await this.page.goto('/#/tasklist');
  }

  async addTask() {
    const page = this.page;

    // Start instance.
    await page.getByRole('link', { name: 'Executions' }).click();
    await page
      .getByRole('row', { name: 'My Process' })
      .getByRole('button', { name: 'Show Instances' })
      .click();
    await page.locator('span:nth-child(4) > .v-btn').first().click();
    await page.getByRole('link', { name: 'Tasklist' }).click();
  }
}

The second file is used to setup fixtures for our tests. Fixtures are helpful to re-use setup and clean-up code in multiple test files. They can also be composed to build more complex helpers while keeping the test files clean and readable. You can learn more about fixtures in the Playwright docs.

import { test as base } from '@playwright/test';
import { TasklistPage } from './tasklist.page';

// Declare the types of your fixtures.
type MyFixtures = {
  tasklistPage: TasklistPage;
};

// Extend base test by providing "tasklistPage".
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
export const test = base.extend<MyFixtures>({
  tasklistPage: async ({ page }, use) => {
    // Set up the fixture.
    const tasklistPage = new TasklistPage(page);
    await tasklistPage.createUsertaskProcess();
    await tasklistPage.goto();

    // Use the fixture value in the test.
    await use(tasklistPage);

    // Clean up the fixture.
    await tasklistPage.removeAll();
  },
});

export { expect } from '@playwright/test';

The fixture can then be used in the tasklist.spec.ts file like page in the example above. Here the actual tests are defined which ensure our tested parts of the application function as expected.

VS Code Extension

In order to develop and debug Playwright tests, we recommend installing the "Playwright Tests for VS Code" extension from within the extension store in VS Code.

Screenshot 2023-03-22 at 01 01 39

This extension will add a "Testing" tab to the menu where you can see your Playwright files and tests. Each file and their individual tests can be run by clicking on the arrow next to their name. If the test succeeds, it will be marked with a green icon. If it fails, a red icon will be shown. Additionally, you can run the tests in debug mode by clicking the other arrow and make use of breakpoints.

Screenshot 2023-03-22 at 01 09 24

At the bottom of the sidemenu there are multiple useful options. With "Pick locator" you can select an element in the browser window and see the corresponding locator, which will be copied to your clipboard.

Screenshot 2023-03-22 at 01 43 20 Screenshot 2023-03-22 at 01 44 08

With "Record at cursor" you can perform actions in the browser window and Playwright will record them at your current position in the test file. This is useful to avoid tedious manual writing of locators and actions.

Tips

The best practices site of the Playwright docs provide useful hints for developing good end-to-end tests. Especially the section on test isolation and preferring longer tests to a lot of short ones are principles we try to follow in our tests.

Clone this wiki locally