Skip to content

Playwright tests

We use Playwright for end-to-end tests of the frontend. Start with the official installation, assertions and best practices pages. For running tests in the pipeline see Playwright in GitLab CI.

Structure

Tests live in the playwright/ folder of the platform repository. It is a separate Yarn workspace (@easy/playwright) with its own package.json. The configuration is playwright/playwright.config.ts; only Chrome is configured, do not add other browsers without agreeing it with your lead.

BASE_URL sets the application under test and defaults to http://localhost:3000. If you cannot run the application locally, point BASE_URL at a testing application. Do not commit changes to playwright.config.ts without discussing them with your lead.

Path Content
playwright/tests Test files (*.spec.ts), grouped by feature.
playwright/pages Page objects, *.page.ts. base.page.ts holds methods shared by all pages, layout_page/ the elements shared across the application.
playwright/fixtures JSON files describing entities. A file with an id describes an entity that exists in the testing database; other files fill inputs.
playwright/constants Constants used only by tests (timeouts and similar).
playwright/types Enumerations and shared types.
playwright/utils, playwright/utils.ts Helpers without Playwright logic, plus shared helpers such as autologin.

Page objects follow the Page Object Pattern: locators and the methods that interact with a page, modal or form live in *.page.ts, not in tests.

import { Page } from "@playwright/test";

export class BasePage {
  readonly page: Page;
  readonly submitBtn: string | null;

  constructor(page: Page, submitBtn: string | null = null) {
    this.page = page;
    this.submitBtn = submitBtn;
  }
}

Test file structure

Each test file has:

  1. test.describe with a short description of what the file tests,
  2. test.beforeAll() or test.beforeEach() for setup,
  3. test() blocks, each with a short description,
  4. test.afterAll() or test.afterEach() for cleanup.
import { test, Page } from "@playwright/test";

test.describe.serial("Create and work with the projects", () => {
  let page: Page;

  test.beforeAll(async ({ browser }) => {
    const pageContext = await browser.newContext();
    page = await pageContext.newPage();
    await autologin(page);
  });

  test("Create a new project #task_id", async () => {
    // test body
  });

  test.afterAll(async () => {
    await page.close();
  });
});

Selectors with data-cy

testIdAttribute is set to data-cy, so page.getByTestId("...") targets data-cy. Follow the best practices and these rules:

  1. Use id and class selectors only in combination with a data-cy attribute.
  2. Do not target elements by HTML tags (div, img, a) unless combined with data-cy.
  3. Avoid complex selectors such as div.mydiv > ul > li > a.target.
  4. Never assert on localised strings.
  5. Make data-cy values specific and semantic, preferably in the subject__action--target pattern, for example button__open--crm-index.

When the backend renders the HTML, pass the attribute from the view:

link_to(l(:label), url, class: "customize-button", "data-cy": "button__customize_page")
# or
link_to(l(:label), url, class: "customize-button", data: { cy: "button__customize_page" })

Adding data-cy is the only application change a test branch may contain.

Run tests locally

Playwright runs against a running application server.

  1. Create a database for the tests and point the application at it, for example with MYSQL_DATABASE=playwright_test in .env.development.local.
  2. Import the dump of the shared testing database (e2e-testing-source.easysoftware.com). Do not edit that database without permission from QA.
  3. Install and migrate:

    bundle install
    RAILS_ENV=development bundle exec rails easy8:install
    
  4. Start the server. In production mode FORCE_HTTP lets Puma accept plain HTTP.

    bundle exec rails server
    
  5. Install the dependencies and run the tests from the repository root:

    yarn install
    yarn playwright
    

Other scripts from the root package.json: yarn playwright:headed, yarn playwright:ui and yarn playwright:report. Run a single file with yarn playwright tests/login.spec.ts.

Change tests

  1. Create or reuse an Easy8 task that tracks the change.
  2. Branch as testautomation/task_id-short-description (for example testautomation/001-created-a-new-project-test). This format is reserved for QA.
  3. End each test description with the task ID: test("Created a project #002", ...).
  4. Keep test files under 250 lines; put the overflow in a new file.
  5. Commit with the commit message convention, including the task reference.
  6. A merge request needs two approvals. Your lead tells you where to post it.

Writing stable tests

  • After a reload, redirect or opening a new page, wait for the page to be ready. navigateToUrl in playwright/pages/base.page.ts does it with waitForDocumentReady.
  • Use waitForResponse inside Promise.all together with the action that triggers the request:

    async clickOnSaveButton() {
      await Promise.all([
        this.page.waitForResponse("**/easy_wbs.json?*"),
        this.clickOnButton("submit--mindmup"),
      ]);
    }
    
  • Do not read text with innerText; it returns the current text and breaks when the content re-renders. Use toHaveText, which waits:

    // bad
    expect(await this.page.locator("[data-cy='button__submit']").innerText()).toBe("Submit");
    // good
    await expect(this.page.locator("[data-cy='button__submit']")).toHaveText("Submit");
    
  • When you run tests against a deployment, throttleNetwork from base.page.ts slows the network so you can check stability. Remove the call before merging.

    adminPage.throttleNetwork(page); // remove before merge