(Updated: )

Five Playwright Tips to Level Up Your Testing Game

Share on social

Five Playwright tips to level up your testing game
Table of contents

I joined Checkly a few months ago, and because our platform enables you to use Microsoft’s Playwright to run your synthetic monitoring, I started getting my hands dirty with the end-to-end testing framework. I’m a massive fan of learning in public, so I started publishing weekly Playwright tips on YouTube.

Did you miss a few videos? Don’t sweat it! Here’s the first collection of Playwright tricks I discovered over the last few months.

The 'test' function’s hidden functionality

I was surprised to learn that Playwright is way more than a browser control. Over time, it has become a full-fledged test runner that provides everything you need to group, run, and control hundreds of tests. Let’s have a look at it!

A standard test case usually includes a describe block that includes all your test cases.

test.describe("All my tests", () => {
  test.beforeEach(async ({ page }) => { /* … */ })
  test(
    "has the correct title",
    async ({ page }) => { /* ... */}
  )
})

Using describe, beforeEach and afterEach isn’t exciting because these methods were all included in the testing frameworks I’ve used. But Playwright’s test object provides some surprises!

// Only run a specific test
test.only()
// Skip a test and mark it as being broken
test.fixme()
// Mark a test as slow and triple the default timeout
test.slow()
// Add an additional level of grouping in your test reports
test.step()

Some of these methods make debugging easier (e.g. test.only), while others help you keep an overview when your test suite grows (e.g. test.fixme and test.step), and you potentially run hundreds of tests.

The listed methods above are only my favorites, though. Head over to the Playwright Test documentation to learn more about ways to control your test suite!

Component visual regression testing is baked into Playwright

I dabbled with visual regression testing for the first time almost ten years ago and was delighted when finding out that Playwright provides this functionality out of the box.

All you have to do is locate an element and call the toHaveScreenshot assertion with it.

test("headline is rendering correctly", async ({ page }) => {
  await page.goto("https://checklyhq.com/")
  const headline = page.getByRole("heading", { level: 1 })
  await expect(headline).toHaveScreenshot("headline.png")
})

This assertion captures a visual component snapshot and compares all future test runs with your created base image. If your component changes visually, it won’t match your checked-in snapshot, and your test case fails!

And the best thing: Playwright will also create a visual comparison and include it in your test reports! That’s well played Playwright team!

Speaking of assertions: I didn’t realize it at first, but Playwright’s assertions include way more sugar than just screenshotting.

The power of web-first assertions

When reading the Playwright docs, you might discover the term web-first assertions. But what does this term mean because you’re running a headless browser to check if your website is functioning?

Playwright’s web-first assertions are tailored to the web use case. The provided methods cover more functionality than the usual isEqual or isGreaterThan methods that you find in testing libraries.

await expect(page.locator('#component')).toHaveClass(/selected/)
await expect(page.locator('.my-element')).toBeVisible()
await expect(page.getByRole('textbox')).toBeEditable()
// and many more!

The provided assertions include all the goodies of Jest’s expect library paired with methods such as isHidden, toHaveClass and toBeFocused tailored to interacting with the DOM.

These methods ease UI testing, but Playwright doesn’t stop there! When using web-first assertions, you might have noticed that they’re asynchronous. Why’s that?

Playwright’s custom assertions are “async matchers” and wait for a condition to be met. Otherwise, they’ll time out and throw an error. And while this functionality doesn't seem like a big deal at first, it allows you to drop multiple waitFor statements. Playwright’s web-first assertions don’t represent a met condition at one moment in time, but every assertion is a condition in a time range! Let’s look at an example.

test("throws confetti", async ({ page }) => {
  await page.goto("https://www.my-confetti-site.com/")
  const button = page.locator("#party-time")
  const canvas = page.locator("canvas")
  // wait until there’s no canvas element
  await expect(canvas).toHaveCount(0)
  await button.click()
  // wait until there’s one canvas element
  await expect(canvas).toHaveCount(1)
  // wait until there’s one canvas element
  await expect(canvas).toHaveCount(0)
})

The test case above has the critical task of checking if a particular button on a site renders confetti rain. The confetti is rendered inside a canvas element, and this essential functionality can be tested by checking if the mentioned canvas element is present in the DOM.

And look at it: there are no waitFor statements. Every assertion is asynchronous and waits until the condition is met. I’m still amazed about how much this functionality streamlines writing asynchronous UI test code.

Do you want to see the assertions in action? Have a look at the YouTube explainer covering web-first assertions.

Use page.evaluate to gather custom metrics

Playwright’s testing framework covers almost everything I need in my daily work, but I discovered that web performance metrics aren’t available in Playwright yet. But that’s no big deal because one can always roll their custom performance metrics aggregation using page.evaluate.

If you want to inject custom JavaScript into the tested page, page.evaluate enables you to do just that!

test("has a good LCP", async ({ page }) => {
  await page.goto("https://checklyhq.com/")
  // Inject custom JavaScript into the page,
  // Evaluate the “largest contentful paint” metric
  // and pass it back to the Playwright scope
  const LCP = await page.evaluate(() => {
    return new Promise((resolve) => {
      new PerformanceObserver((list) => {
        const entries = list.getEntries()
        const LCP = entries.at(-1)
        resolve(LCP.startTime)
      }).observe({
        type: "largest-contentful-paint",
        buffered: true,
      })
    })
  })
  expect(+LCP).toBeLessThan(50)
})

The above test measures the Core Web Vital metric “Largest Contentful Paint” by injecting a PerformanceObserver into the loaded page. So, whenever you want to evaluate a metric that Playwright doesn’t provide yet, page.evaluate is your friend!

Block unnecessary resources to speed up your tests

When your test suite grows, you want to guarantee that your tests run as quickly as possible. One way to do this is to block all unnecessary requests.

Are all your page’s images needed when you want to test that your menu toggle works? Probably not. Do you need to wait for fonts to load when you’re testing page navigations? I doubt it. Should your analytics count all your end-to-end tests? Of course not!

Luckily Playwright allows you to quickly enter the network layer using page.route and block all requests slowing you down.

test("homepage has correct meta tags", async ({ page }) => {
  // Abort all unnecessary image requests
  await page.route(
    "**/*.{png,jpg,jpeg,webp,svg}",
    (route) => route.abort()
  )
  await page.goto("https://www.checklyhq.com")
  await page.screenshot({ path: "./screenshot.png" })
})

But watch out, if you want to intercept or change network requests, you have to call page.route before navigating somewhere.

If you want to see this approach in action, here’s the YouTube video about request interception.

That’s the first mixed bag of Playwright tips

This article was the first collection of Playwright tips. If you want to see more of them, hit the subscribe button on YouTube. I’m not planning to stop sharing all these nerdy discoveries anytime soon!

And if you want to share some tips, I’m all ears at Checkly’s Community Slack! Come and say “Hi”!

Share on social