Playwright tips and trick #4

Playwright tips and trick #4
Photo by UX Store / Unsplash

Continuing the series of tips and tricks, after the success of #1 #2 and #3 comes our newest addition #4. Hope you enjoy and don't forget to read also the comments inside the code snippets.

1. How to intercept multiple requests with the same route?

I remember in Cypress that this was really easy to achieve, however in Playwright its not that easy to find details for such particular scenario. Imagine you navigate thru your web app and you are interested to validate multiple requests that have the same route. Say when you open a page for a product, an API call is performed at api/id/1 to fetch data, then you need to later intercept the exact same call at api/id/1 . How would I intercept both of them ? And also validate its data ?

You can try this with waitForRequest() , but there is a more versatile way in doing this, that will open a lot of possibilities for you

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

test("Validate multiple same route calls", async ({ page }) => {
  const requests: string[] = [];

  // use route to differentiate between your network calls
  await page.route("**/api/**", async (route) => {
    // use url().includes to filter the exact call you need
    if (route.request().url().includes("id/1")) {
      await route.continue();
      const response = await (await route.request().response())?.json();
      requests.push(response);
    } else await route.continue();
  });

  // the above code is like a listener that you set it up. Put that before
  // you trigger your network calls
  
  await page.goto("www.yourapp.com");
  // imagine this step triggers the first id/1 call
  await product.click();
  // and this step triggers the second id/1 call
  await productExtraDetails.click()

  // now you wait for them and do any assertions you need
  await expect(async () => {
    expect(requests.length).toBe(2);
  }).toPass({
    intervals: [1_000, 2_000, 5_000],
    timeout: 15_000,
  });
});

// below is an extra step you need to have
// because sometimes your tests will finish faster
// than your app response and you need to unroute.
test.afterEach("Unroute all", async ({ page }) => {
  await page.unrouteAll({ behavior: "wait" });
});

Most common use case of this scenario I've seen is validating trackers.


2. Do not use .all() without asserting the count of elements before

Assuming you have a selector that will return multiple elements and you want to iterate over those elements to assert some values. If you don't assert the count first you may end up with false positives in your tests. Here's why

If you have 3 elements and do

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

test("False positive on .all()", async ({ page }) => {
  const expectedValues = ["a", "b", "c"]
  // other actions

  const allElements = await page.getByTestId(locator).all()

  for (const item of allElements) {
     await expect(item).toHaveAttribute("any-attribute", expectedValues)
  }
});

Tests will work as intended. However, if by some reason your locator will not find any elements on page, test will still pass. Because .all() does not error out if count is zero and forEach will not run if no elements.

To have this properly you must assert the count of elements before iterating over them. Remember to use auto-retry toHaveCount and not stuff like (allElements.length).toBe(3)

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

test("Proper use of .all()", async ({ page }) => {
  const expectedValues = ["a", "b", "c"]
  // other actions

  await expect(page.getByTestId(locator)).toHaveCount(5)
  const allElements = await page.getByTestId(locator).all()

  for (const item of allElements) {
    await expect(item).toHaveAttribute("any-attribute", expectedValues)
  }
});

Even playwright official docs warns you about about .all() but now you know an exact situation where this will not work properly if count is not asserted before use of .all()

pro tip: forEach is not recommended within playwright async/await style. For... of is what you would need to avoid issues

3. No need to assert toBeVisible before a click

I feel like I have to say this because I see it far too often. People are used to assert visibility of an element before interacting with it, this is a behavior learned in any tutorial or class and it has been a crucial step everyone was in the past forced to learn. But in playwright there is one particular case where you don't need to do that. The case is for the action click() . Here is what Playwright does under the hood before a click on an element:

Now these are called actionability checks and they are not limited to click()

One key aspect is that you should not confuse this with an assertion for toHaveText. Be careful when you use one of the most common assertion methods toHaveText, because toHaveText does not wait for the element to be visible first

4. How do I run only tests I work on?

Most of you who follow Playwright release notes you already know this but some of you do not know that you can use a special CLI command that will run only tests that have a change in it.

Imagine you do a git pull, then you make your own branch to start working on some tests, but you either fix some errors in multiple tests or/and even create new multiple tests. So instead of running the tests like this:

npx playwright test path/to/test1.spec.ts path/to/test2.spec.ts path/to/test3.spec.ts path/to/test4.spec.ts path/to/test5.spec.ts path/to/test6.spec.ts path/to/test7.spec.ts

You can just do this

npx playwright test --only-changed=main

This will compare your branch with branch main and run all tests that have been modified by you in your branch.


5. How do I validate table data in Playwright?

Imagine you have a table and you need to validate values rendered. I am not talking here about validating a text exists in any cell, I am talking about a value that should be in an exact cell that is reference to exact column and exact row.

You can achieve this by creating a helper function. For the sake of running this next code yourself to easily test it out, I've put everything into the same file, but of course feel free to store utils/helpers into your own way.

Now how this helper method works? You give it three params, first is the text of the header (column), second is the row your cell will be found and third is the expected text you want on that cell.

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

class WebTableHelper {
  constructor(page) {
    this.page = page;
  }

  async validateCellValueReferenceToHeader(
    headerText,
    rowToValidate,
    expectedValue
  ) {
    const elementsOfHeader = await this.page
      .getByRole("table")
      .getByRole("row")
      .first()
      .getByRole("cell")
      .all();

    const elementsOfNRow = await this.page
      .getByRole("table")
      .getByRole("row")
      .nth(rowToValidate)
      .getByRole("cell")
      .all();

    for (let i = 0; i < elementsOfHeader.length; i++) {
      const headerValueFound = await elementsOfHeader[i].innerText();
      if (headerValueFound.toLowerCase() === headerText.trim().toLowerCase()) {
        const cellValue = await elementsOfNRow[i].innerText();
        expect(cellValue).toBe(expectedValue);
        return;
      }
    }
    throw new Error(`Header with text "${headerText}" not found`);
  }
}

test("test tables", async ({ page }) => {
  await page.goto("https://cosmocode.io/automation-practice-webtable");
  const helper = new WebTableHelper(page);
  await expect(page.getByRole("table").getByRole("row")).toHaveCount(197);
  await helper.validateCellValueReferenceToHeader("Currency", 4, "Euro");
});

It may also need some data cleaning and trimming based on what your data is inside the tables. But that's very specific per use case. In most cases this would work out of the box.

This was actually initially created by Srdan Mutlak. So, credit goes to him. He challenged me to create something like this. He first has given me his version of the code and I tried to do it in my own style.

Now I challenge you. Can you make this better ? Create a Github gist and write in the comments.


6. How do I get the element background color in Playwright?

I've seen plenty of such examples where people would get the color of a background with code similar to this

const color = await btn.evaluate((element) =>
    window.getComputedStyle(element).getPropertyValue("background-color")
  );
expect(color).toBe("rgb(69, 186, 75)")

No need to do things like this anymore, we have await expect(locator).toHaveCSS('background-color',"rgb(69, 186, 75)")

And if you want to actually deal with Hex instead of RGB, here you can find a really good way to approach it.

buy me a coffee ?

Feel free to checkout other nice tips:

Playwright tips and tricks #3
Get more details about your test during test run. How to use Playwright to test multiple browser windows. Promise.all in Playwright
Playwright tips and tricks #1
Common errors, data test id hacks, auto-waits, timeouts hack, asserting an array of strings, absence of an element and many more
Playwright tips and tricks #2
I’ve written a post about tips and tricks and it got a lot of love. So, I’ve decided to do another one. 1. How to handle an element that appears after full page load in playwright One of those rare cases where an element will appear in DOM only after