Create resilient ๐ŸŽญ Playwright e2e tests with locators

Modern web apps introduce some testing challenges โ€” dynamic controls can cause flakiness and unexpected behaviors. This is where the magic of the Playwright locator API can help us build more resilient tests.

Why use locators?

The locator API was introduced in the 1.14 release and the docs describe it as:

Locator represents a view to the element(s) on the page. It captures the logic sufficient to retrieve the element at any given moment.

https://playwright.dev/docs/api/class-locator

This means we can create smarter selectors to mitigate flaky tests. Part of the smarter logic is strictness. Operations on the target DOM element will throw if more than one element matches the given selector.
e.g.

// Throws if there are several buttons in DOM
await page.locator('button').click();

This results in 3 outcomes when using locators:

  • Test works as expected
  • Selector does not match anything and test fails
  • Multiple elements match the selector (e.g. there is a second “button” added to the page somewhere), and test fails with helpful error message

This allows us to be less thoughtful about our selectors because we can rely on the Playwright locator magic to ensure the test is resilient.

It also enables “black box” testing. The tests could be authored by looking at a web page, without inspecting the source code, by a user who may not have insight or understanding of the technical details of the DOM, CSS, etc. The text selector engine in combination with locators makes this possible.

How can I use locators?

One of the most common cases where I’ve implemented locators is on page.click().
Instead of this:

await page.click('text="Login"');

We can use this:

await page.locator('text="Login"').click();

We can also use it with Playwright Test’s many web-first assertions, that offer async matchers that wait until the expected condition is met. Again, to help us battle testing the dynamic web. We can also store a locator in a variable and re-use it in combination with other locators, across multiple assertions:

const toggle = page.locator('.setting-item-toggle');
await expect(toggle.locator('text=Show original')).not.toBeChecked();
await expect(toggle.locator('text=Compare gzipped')).toBeChecked();
await expect(toggle.locator('text=Prettify markup')).not.toBeChecked();
await expect(toggle.locator('text=Multipass')).not.toBeChecked();

Working with a list? This is where using an assert of toHaveCount() can be useful.
e.g.

// Wait until there are 3 "Buy" nodes in DOM
await expect(page.locator('text=Buy')).toHaveCount(3)

Better yet, we can look for text using toHaveText()!
e.g.

// Taken from https://github.com/MarcusFelling/Demo.Playwright/blob/main/svgomg/tests/example.spec.ts
test('verify menu items', async ({ page }) => {
  await expect(page.locator('.menu li')).toHaveText([
    'Open SVG',
    'Paste markup',
    'Demo',
    'Contribute'
  ]);
});

Working with iFrames? The 1.17 release introduced FrameLocator, that of course makes sense to use with locator.
e.g.

// Click submit button inside #my-frame
const locator = page.frameLocator('#my-frame').locator('text=Submit');
await locator.click();

I want to see working code!

Check out this repo that’s used to demo various testing scenarios with Playwright, using the official test-runner and scripts authored in TypeScript: https://github.com/MarcusFelling/demo.playwright. The majority of example scripts are leveraging locators.

Happy testing!

Leave a Reply

Your email address will not be published.