Swacblooms🦋

Making the Moves
Menu
  • Home
  • Motivation
  • Education
  • Programming
  • About
  • Contact
  • Privacy Policy
Home
Uncategorized
Part 2 — Understanding Locators: Finding Things on the Page
Uncategorized

Part 2 — Understanding Locators: Finding Things on the Page

Samson Amaugo March 30, 2026

Introduction

Before you can click a button, fill a form, or assert that something is visible, you need to tell Playwright which element you are talking about. That is what locators are for.

A locator is not just a selector — it is a smart reference to an element that re-queries the DOM every time you use it. This means if the page re-renders between steps, Playwright finds the fresh element automatically rather than holding a stale reference that no longer exists.

In this article, you will learn some of the most common locator methods Playwright provides, when to use each one, and the priority order the Playwright team recommends.


Recommended Built-in Locators

Playwright’s docs recommend these built-in locators over CSS and XPath because they are tied to what users actually see and interact with:

  1. getByRole
  2. getByText
  3. getByLabel
  4. getByPlaceholder
  5. getByAltText
  6. getByTitle
  7. getByTestId

The guiding principle: the closer a locator is to how a real user perceives the page, the better. Users read text and interact with buttons and links — they do not think in CSS class names.


getByRole — Start Here

getByRole finds elements by their ARIA role and accessible name. This is the recommended first choice for almost every interactive element on the page.

// finds <a href="/docs/intro">Get started</a>
page.getByRole('link', { name: 'Get started' })

// finds <button>Submit</button>
page.getByRole('button', { name: 'Submit' })

// finds <h1>Installation</h1>
page.getByRole('heading', { name: 'Installation' })

// finds <input type="checkbox"> with label "Remember me"
page.getByRole('checkbox', { name: 'Remember me' })

The role comes from the HTML element itself — no aria-* attributes needed. The browser assigns roles automatically:

HTML elementImplicit ARIA role
<a href="...">link
<button>button
<h1> – <h6>heading
<input type="checkbox">checkbox
<input type="text">textbox
<img>img
<ul> / <ol>list

The accessible name comes from the element’s visible text content (as you saw in Part 1 with the Get started link). If the element has an aria-label or aria-labelledby, that takes priority over text content.

The name option accepts a string or a regular expression:

// exact string match
page.getByRole('button', { name: 'Sign in' })

// regex — useful when the text is dynamic
page.getByRole('button', { name: /sign in/i })


getByText — For Non-Interactive Elements

Use getByText when you want to find an element by its visible text content and there is no meaningful role to target — typically a <div>, <span>, or <p>.

// finds any element containing the text "Welcome back"
page.getByText('Welcome back')

// exact match only
page.getByText('Welcome back', { exact: true })

// regex
page.getByText(/welcome back/i)

Important: Playwright normalises whitespace automatically, even with { exact: true }. Multiple spaces become one, line breaks become spaces, and leading/trailing whitespace is ignored.

Avoid using getByText on buttons and links — use getByRole for those. getByText is for reading content, not for triggering actions.


getByLabel — For Form Fields

getByLabel finds an input by the text of its associated <label>. This is the right tool any time you are filling in a form.

// finds the input associated with <label>Email address</label>
page.getByLabel('Email address')

// usage in a test
await page.getByLabel('Email address').fill('user@example.com');
await page.getByLabel('Password').fill('secret');

The label association can be done two ways in HTML, and getByLabel handles both:

<!-- wrapping label -->
<label>
  Email address
  <input type="email" />
</label>

<!-- linked by for/id -->
<label for="email">Email address</label>
<input id="email" type="email" />


getByPlaceholder — When There Is No Label

Some inputs skip a visible label and rely on placeholder text instead. Use getByPlaceholder for those.

<input type="email" placeholder="Enter your email" />

await page.getByPlaceholder('Enter your email').fill('user@example.com');

Prefer getByLabel over getByPlaceholder when both are available — a visible label is better for accessibility and more stable as a test target.


getByAltText — For Images

getByAltText finds elements by their alt attribute. In practice this means images.

<img src="logo.png" alt="Company logo" />

await expect(page.getByAltText('Company logo')).toBeVisible();


getByTitle — For Title Attributes

getByTitle matches elements with a title attribute. Less common, but useful for icon buttons and tooltips that carry no visible text.

<button title="Close dialog">✕</button>

await page.getByTitle('Close dialog').click();


getByTestId — The Explicit Contract

getByTestId finds elements by a data-testid attribute (or a custom attribute you configure).

<button data-testid="submit-order">Place order</button>

await page.getByTestId('submit-order').click();

This locator is the most resilient to UI changes — the visible text and styling can change entirely without breaking your test. The trade-off: it requires you to add data-testid attributes to your HTML, and it is not based on anything a real user perceives.

Use it when:

  • A role or text locator is not specific enough
  • You want an explicit, stable contract between your tests and a specific element
  • The element has no accessible role or visible text

You can configure a custom attribute name in playwright.config.js:

use: {
  testIdAttribute: 'data-cy', // use data-cy instead of data-testid
}


CSS and XPath — Last Resort

When none of the above fit, you can fall back to CSS selectors or XPath via locator().

// ID selector
page.locator('#submit-button')

// CSS selector
page.locator('.nav-menu > li:first-child')

// XPath
page.locator('//button[@type="submit"]')

Playwright auto-detects whether you passed CSS or XPath — no prefix needed.

Why avoid these? The DOM changes. Class names get renamed, elements get restructured, and your tests break for reasons unrelated to the actual behaviour of the app. The role and text-based locators are far more stable because they are tied to what users see, not to implementation details.

One additional limitation: XPath does not pierce Shadow DOM. All other Playwright locators work inside shadow roots by default.


Filtering Locators

When a locator matches more than one element, use filter() to narrow it down.

// all list items that contain the text "In stock"
page.getByRole('listitem').filter({ hasText: 'In stock' })

// list items that do NOT contain "Sold out"
page.getByRole('listitem').filter({ hasNotText: 'Sold out' })

// list items that contain a child button labelled "Add to cart"
page.getByRole('listitem').filter({
  has: page.getByRole('button', { name: 'Add to cart' })
})


Chaining Locators

All locator methods are also available on a locator itself, so you can scope a search inside a specific part of the page.

// find the navigation section first, then look for a link inside it
page.getByRole('navigation').getByRole('link', { name: 'Pricing' })

// find a form, then find the submit button inside it
page.getByRole('form', { name: 'Checkout' }).getByRole('button', { name: 'Place order' })

This is cleaner and more precise than a deeply nested CSS selector, and it reads like plain English.


Strictness — One Element at a Time

Playwright locators are strict by default. If a locator matches more than one element and you call an action on it (like .click()), Playwright throws an error rather than silently picking the first match.

Error: locator.click: Error: strict mode violation:
getByRole('button', { name: 'Submit' }) resolved to 2 elements

This is intentional — it forces you to write specific locators. The fix is to make the locator more precise rather than reaching for .first() or .nth(0):

// vague — which Submit button?
page.getByRole('button', { name: 'Submit' })

// specific — scoped to the checkout form
page.getByRole('form', { name: 'Checkout' }).getByRole('button', { name: 'Submit' })


Summary

Here is what you learned in Part 2:

  • A locator re-queries the DOM every time it is used — no stale element references
  • Use getByRole first: it targets elements the same way users and screen readers do
  • Use getByLabel for form inputs, getByText for static content, getByAltText for images
  • Use getByTestId when you need an explicit, stable testing contract
  • Avoid CSS and XPath unless nothing else fits — they are tied to implementation details
  • filter() and chaining let you narrow down locators precisely
  • Locators are strict: if more than one element matches, Playwright errors rather than guessing

What’s Next

In Part 3 we put locators to work. You will learn some of the actions Playwright can perform — clicking, typing, hovering, selecting, uploading files — and how Playwright’s auto-waiting means you almost never need to add a manual wait.

Prev Article
Next Article

Related Articles

hacking your life
Have you read lots of books that revolve around motivational, …

Creating positive fallbacks by hacking your life

links
Hi folks, we are back and banging, I once talked …

Sharing node_modules on windows

About The Author

Samson Amaugo

I am Samson Amaugo. I am a full-stack developer and I specialize in DotNet and the MERN stack.

Search Site

Recent Posts

  • Cloudflare Tunnel to Postgres, plus Swarm’s static IP problem
  • The Topological Sort
  • Building a Perceptron in .NET
  • Part 4 — Assertions: Verifying What You See
  • Part 3 — Actions and Auto-Waiting: Putting Locators to Work

Categories

  • EDUCATION
  • Motivation
  • Programming
  • Uncategorized

Get more stuff

Subscribe to our mailing list and get interesting stuff and updates to your email inbox.

Thank you for subscribing.

Something went wrong.

we respect your privacy and take protecting it seriously

RSS feed: Swacblooms Swacblooms

Swacblooms🦋

Making the Moves
Copyright © 2026 Swacblooms🦋
Swacblooms - Making the Moves