Sponsored Link
Looking for a unified way to  viewmonitor, and  debug Playwright Test Automation runs?
Try the Playwright Dashboard by today. Use the coupon code PWSOL10 for a 12 month 10% discount.

Creating custom expects in Playwright: how to write your own assertions

I enjoy reading, specially now when temperatures are in the mid-80s. It's so nice to relax by the pool with a good book. When a book is well-written, it's easy to read, and I can spend hours turning those pages, letting my imagination create scenes and characters... The same applies to code actually. When the code is well-written, I easily understand what a test/check does by reading it, just like with a book. I try my best to write readable code: from meaningful variable names to adding spaces between lines when it logically makes sense ("spaghetti code" is something I just can't look at).

Playwright makes it easy to write readable expectations. Here are some examples:

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

expect(student.grade).toBeGreaterThanOrEqual(1);
expect(student.loan).toBeTruthy();
expect(student.employer).toBeNull();
default expects

Here is a link to the generic value assertions.

But what should you do if you expect a string and you don't really care about the value but want to check the type? Unfortunately, there is nothing out of the box, so I end up writing it this way:

// how I used to check strings
let framework = "playwright"

expect(typeof framework).toBe("string");

I am not saying it's not readable, but I would rather see the code written like this:

// how I check strings now
let framework = "playwright"

expect(framework).toBeString();

If you read the code comments in the block above (another way to make the code readable), you may have already guessed that it's possible to write your own expects. You came to this page to expand your knowledge, and now we are going to extend your expects!

To make the toBeString() check work, you need to add the following to your playwright.config.ts

expect.extend({
  toBeString(received: string) {
    const check = typeof received == "string";
      
    if (check) {
      return {
        message: () => "passed",
        pass: true,
      };
    } else {
      return {
        message: () =>
          `toBeString() assertion failed.\nYou expected '${received}' to be a string but it's a ${typeof received}\n`,
        pass: false,
      };
    }
  },
});
playwright.config.ts

Explanation: we extend our expect by adding the toBeString() matcher. Even though it seems like the matcher takes a parameter, it actually doesn't. received is the value that goes in the first parenthesis of expect(value). Then we check if the received value is typeof the string, and if it is, the check passes. If it's not, we fail the check and have an excellent opportunity to make our code more readable by adding a custom message.

Let's see how it works.

let value = 123;

expect(value).toBeString();
expected to fail

Since the value is obviously not a string in this example I expect it to fail. And indeed, it fails with the custom message we added.

I have also added .toBeNumber() and .toBeBoolean()to my toolkit.

A few weeks ago, I wrote .toBeOneOfValues(), and it's been a game changer. I use it when I can't predict the exact value, but I know it should be one of the known ones. I mostly use it for writing API checks for GET requests. For example, when I have a charge parameter returned and it can be "pending", "failed" or "successful", and I just want to check that every single charge returned has a value that is one of the expected ones.

let validValues = ["failed", "pending", "successful"]
let testValue = "created"; 

expect(testValue).toBeOneOfValues(validValues);
under the hood we will check if the array includes the passed value

Again, it's going to fail with the custom message I prepared.

Here is the source code that needs to be added to playwright.config.ts

expect.extend({
  toBeOneOfValues(received: any, array: any[]) {
    const check = array.includes(received);

    if (check) {
      return {
        message: () => "passed",
        pass: true,
      };
    } else {
      return {
        message: () =>
          `toBeOneOfValues() assertion failed.\nYou expected [${array}] to include '${received}'\n`,
        pass: false,
      };
    }
  },
});
playwright.config.ts

The most important takeaway here is to realize that you can come up with custom matchers that your automation repository might need to improve readability. What helps me is looking at the value I am checking and saying in my head "I want this value to...." and then completing the sentence in English. There you have your pseudocode.

This article was written based on this documentation. If you are using TypeScript, there is an additional step required for custom matchers to work, which involves adding a global.d.ts file. Here's how mine looks for the two expects mentioned above.

export {};

declare global {
  namespace PlaywrightTest {
    interface Matchers<R> {
      toBeOneOfValues(array: any[]): R;
      toBeString(): R;
    }
  }
}
global.d.ts

I hope you find this useful, and if you did, please ❤️ and subscribe below to receive more useful tips. If you want to reach out to me personally, feel free to connect or message on LinkedIn.