Search code examples
javascriptplaywright

How to use playwright expect to do an exact match of one of two possible values?


How can I use playwright expect to check for one of two exact matches?

Here's my function.

export const assertThirdPartyInternetPath = async (
  page: Page,
  path: string,
) => {
  expect(page.url()).toBe(path);
};

I am using it to test links to wikipedia pages.

await this.assertThirdPartyInternetPath('https://en.wikipedia.org/wiki/Larry_Sanger'

However, some sites like Wikipedia will redirect mobile devices (including playwright devices) to the m subdomain.

So I want to assert that the user is at either https://en.wikipedia.org/wiki/Larry_Sanger or https://en.m.wikipedia.org/wiki/Larry_Sanger. How can I do that?

Note that I want to do an exact match; I know I can use expect(string.toContain(myPattern) but I have various things to match and I want to do exact matches.


Solution

  • Reversing the comparison as suggested in Jest matcher to match any one of three values is possible, but makes the assertion message and overall flow a bit awkward.

    expect([
      "https://en.wikipedia.org/wiki/Larry_Sanger",
      "https://en.m.wikipedia.org/wiki/Larry_Sanger",
    ]).toContain(page.url());
    

    I'd prefer to use regex because it keeps the assertion in the normal direction:

    expect(page.url())
      .toMatch(/^https:\/\/en\.(?:m\.)?wikipedia\.org\/wiki\/Larry_Sanger$/);
    

    The problems are readability and remembering to escape regex characters and add anchors. You could use a normal string and escape it with a bit of extra utility code.

    Another option is to write a custom matcher:

    import {expect, test} from "@playwright/test"; // ^1.39.0
    
    expect.extend({
      async toBeAnyOf(received, ...possibilities) {
        if (possibilities.includes(received)) {
          return {
            message: () => "passed",
            pass: true,
          };
        }
    
        return {
          message: () =>
            `failed: '${received}' was not any of ${possibilities}`,
          pass: false,
        };
      },
    });
    
    test("is on the normal site", async ({page}) => {
      await page.goto("https://en.wikipedia.org/wiki/Larry_Sanger");
      expect(page.url()).toBeAnyOf(
        "https://en.wikipedia.org/wiki/Larry_Sanger",
        "https://en.m.wikipedia.org/wiki/Larry_Sanger",
      );
    });
    
    test("is on the mobile site", async ({page}) => {
      await page.goto("https://en.m.wikipedia.org/wiki/Larry_Sanger");
      expect(page.url()).toBeAnyOf(
        "https://en.wikipedia.org/wiki/Larry_Sanger",
        "https://en.m.wikipedia.org/wiki/Larry_Sanger",
      );
    });
    

    If you need auto-waiting, the regex will work in toHaveURL:

    await expect(page).toHaveURL(
      /^https:\/\/en\.(?:m\.)?wikipedia\.org\/wiki\/Larry_Sanger$/
    );
    

    Promise.race() is also possible. This avoids the regex and is pretty clear to read, but with the downside of being a bit verbose:

    import {expect, test} from "@playwright/test";
    
    test("navigate to one of two URLs", async ({page}) => {
      const url = "https://en.wikipedia.org/wiki/Larry_Sanger";
      const mobile = "https://en.m.wikipedia.org/wiki/Larry_Sanger";
      await page.goto(Math.random() >= 0.5 ? url : mobile);
      await Promise.race([
        expect(page).toHaveURL(url),
        expect(page).toHaveURL(mobile),
      ]);
    });
    

    You can shorten this with a macro for Promise.race() if you're using it often:

    const either = (...a) => Promise.race([...a]);
    
    // ...
    await either(
      expect(page).toHaveURL(url),
      expect(page).toHaveURL(mobile)
    );
    

    Note that Promise.race() doesn't have a timeout, so if none of the options ever match, you'd have to rely on the test case timing out. This might not show as clear a failure message as it could.