Playwright Masters

Playwright Locators: Complete Guide with Examples

If Playwright automation is a car, locators are the steering wheel.

A Playwright test first needs to find a web element before it can click, fill, check, hover, select, upload, or verify anything.

That is why learning Playwright Locators is one of the most important steps for anyone learning Playwright automation testing.

Table of Contents

A locator tells Playwright:

“Find this particular element on the webpage.”

For example:

await page.getByRole(‘button’, { name: ‘Login’ }).click();

 

This tells Playwright to find a button whose accessible name is Login and click it.

Playwright’s official documentation recommends user-facing locators such as roles, labels, text, placeholders, and explicit test IDs because they can make tests easier to understand and maintain. Locators also provide Playwright’s auto-waiting and retry behavior.

This guide explains Playwright locators from beginner to advanced level, including practical examples, locator selection rules, CSS and XPath, chaining, filtering, strict mode, dynamic elements, Codegen, debugging, and real-world best practices.

Playwright Locaters

What Are Playwright Locators?

Playwright locators are APIs used to find web elements on a webpage so that Playwright can interact with or verify those elements.

Common Playwright locators include:

getByRole()

getByText()

getByLabel()

getByPlaceholder()

getByAltText()

getByTitle()

getByTestId()

locator()

 

Playwright also supports CSS and XPath selectors through locator().

A simple example is:

await page.getByRole(‘button’, { name: ‘Login’ }).click();

 

The locator finds the Login button, and click() performs the action.

What Is a Locator in Playwright?

Imagine a webpage contains 50 elements.

You tell Playwright:

“Click the Login button.”

Playwright needs a way to identify which element you mean.

That identification method is a locator.

For example:

const loginButton = page.getByRole(‘button’, { name: ‘Login’ });

await loginButton.click();

There are two important ideas here:

  1. getByRole() describes what element you want.
  2. click() describes what you want to do with it.

This makes Playwright tests easier to read.

Why Are Playwright Locators Important?

Locators are important because almost every UI automation task starts by finding an element.

You may need to:

  • Click a button.
  • Enter a username.
  • Enter a password.
  • Select a checkbox.
  • Choose a dropdown option.
  • Upload a file.
  • Verify a message.
  • Open a menu.
  • Check a heading.
  • Verify a table row.
  • Interact with a dialog.
  • Work with an iframe.

All of these tasks require Playwright to identify the correct element.

A good locator can make a test:

  • Easier to read.
  • Easier to debug.
  • More resistant to UI changes.
  • Less dependent on HTML structure.
  • Easier for another tester to understand.

A poor locator can make tests:

  • Difficult to maintain.
  • Sensitive to UI redesigns.
  • Ambiguous.
  • Flaky.
  • Hard to debug.

Playwright Locator Types

The main built-in locator methods are:

Locator

Best Use

getByRole()

Buttons, links, headings, checkboxes and other user-facing elements

getByLabel()

Form fields with labels

getByText()

Visible non-interactive text

getByPlaceholder()

Inputs identified by placeholder

getByAltText()

Images and elements with alternative text

getByTitle()

Elements with a title attribute

getByTestId()

Explicit testing contracts

locator()

CSS, XPath and more specialized selection

Playwright recommends prioritizing user-facing locators and explicit testing contracts rather than automatically reaching for long CSS or XPath expressions.

1. Playwright getByRole()

getByRole() is one of the most useful Playwright locator methods.

It finds an element according to its ARIA role and accessible name.

Example:

await page.getByRole(‘button’, { name: ‘Login’ }).click();

 

The role is:

button

 

The accessible name is:

Login

 

Example HTML

<button>Login</button>

 

Playwright can identify it with:

page.getByRole(‘button’, { name: ‘Login’ })

 

More Examples

 

Button

await page.getByRole(‘button’, { name: ‘Submit’ }).click();

 

Link

await page.getByRole(‘link’, { name: ‘Contact Us’ }).click();

 

Heading

await expect(

  page.getByRole(‘heading’, { name: ‘Dashboard’ })

).toBeVisible();

 

Checkbox

await page.getByRole(‘checkbox’, { name: ‘Accept Terms’ }).check();

 

Radio button

await page.getByRole(‘radio’, { name: ‘Male’ }).check();

 

Dialog

const dialog = page.getByRole(‘dialog’, { name: ‘Login’ });

 

Playwright recommends role locators because they are close to how users and assistive technologies perceive interactive elements.

When should you use getByRole()?

Use it when an element has a meaningful role and accessible name.

For example:

page.getByRole(‘button’, { name: ‘Save’ })

 

is generally easier to understand than:

page.locator(‘#saveBtn’)



2. Playwright getByLabel()

getByLabel() is designed mainly for form controls associated with labels.

Suppose your HTML is:

<label>

  Email

  <input type=”email”>

</label>

 

You can write:

await page.getByLabel(‘Email’).fill(‘user@example.com’);

 

Another common HTML structure is:

<label for=”username”>Username</label>

<input id=”username”>

 

The locator can still be:

await page.getByLabel(‘Username’).fill(‘john’);

 

Why is getByLabel() useful?

A form normally tells users what each field means.

For example:

Username

Password

Email

Phone Number

Address

 

Using those labels makes the test easy to understand.

Example Login

await page.getByLabel(‘Username’).fill(‘john123’);

await page.getByLabel(‘Password’).fill(‘mypassword’);

await page.getByRole(‘button’, { name: ‘Login’ }).click();

 

This reads almost like a manual test case.

Playwright’s documentation specifically recommends label locators for form controls with associated labels.

3. Playwright getByText()

getByText() finds an element using its visible text.

Example:

await expect(

  page.getByText(‘Login successful’)

).toBeVisible();

 

Suppose the webpage contains:

<div>Payment successful</div>

 

You can write:

await expect(

  page.getByText(‘Payment successful’)

).toBeVisible();

 

Exact Text Matching

If several elements contain similar text, use exact matching:

await page.getByText(‘Save’, { exact: true }).click();

 

Regular Expressions

You can also use a regular expression:

await expect(

  page.getByText(/payment successful/i)

).toBeVisible();

 

Playwright normalizes whitespace during text matching, and its documentation supports substring, exact, and regular-expression matching.

When should you use getByText()?

Use it when the visible text itself is the meaningful identifier.

For example:

Order placed successfully

Payment completed

Welcome, John

No products found

 

For interactive controls such as buttons and links, prefer getByRole() when it provides a clear locator.

4. Playwright getByPlaceholder()

Many input fields contain placeholder text.

Example:

<input placeholder=”Enter your email”>

 

You can locate it using:

await page

  .getByPlaceholder(‘Enter your email’)

  .fill(‘john@example.com’);

 

Another example:

await page.getByPlaceholder(‘Search products’).fill(‘Laptop’);

 

Important Limitation

A placeholder is not always the best long-term locator.

Designers may change:

Enter your email

 

to:

Email address

 

because of UI or marketing changes.

Therefore, if a proper label exists, getByLabel() is often a better choice.

5. Playwright getByAltText()

Images can have alternative text.

Example:

<img alt=”Playwright logo” src=”logo.png”>

 

You can locate the image using:

await page.getByAltText(‘Playwright logo’).click();

 

This locator is useful for elements such as images that expose meaningful alternative text.

Playwright officially documents getByAltText() for elements supporting alternative text, such as img and area.

6. Playwright getByTitle()

  • If an element has a title attribute, you can use:

    await page.getByTitle(‘Settings’).click();


    Example:

    <button title=”Settings”>

      ⚙

    </button>


    Locator:

    page.getByTitle(‘Settings’)


    This is useful when the title is a stable and meaningful attribute.

7. Playwright getByTestId()

A test ID is an attribute specifically designed to help automated tests identify an element.

Example:

<button data-testid=”login-button”>

  Login

</button>

 

You can write:

await page.getByTestId(‘login-button’).click();

 

By default, Playwright uses data-testid for getByTestId(), although the test ID attribute can be configured.

When should you use Test IDs?

Use a test ID when:

  • User-facing text is unstable.
  • The element has no useful accessible name.
  • A component is difficult to identify semantically.
  • Your development team has deliberately created a stable test contract.

Example:

<button data-testid=”checkout-submit”>

  Continue

</button>

 

Test:

await page.getByTestId(‘checkout-submit’).click();

 

Important Rule

Do not add data-testid to every element automatically.

First ask:

Can I identify this element clearly using its role, label, text, or another stable user-facing property?

If yes, that may be preferable.

If not, a test ID can be a very good explicit contract.

8. Playwright locator()

page.locator() is the general locator API.

For example:

page.locator(‘#username’)

 

or:

page.locator(‘.login-button’)

 

You can also use CSS:

await page.locator(‘button[type=”submit”]’).click();

 

And XPath:

await page.locator(‘//button[@type=”submit”]’).click();

 

Playwright supports CSS and XPath, but official guidance recommends user-facing locators where practical.

CSS Selectors in Playwright

CSS selectors are widely used in web automation.

Examples:

page.locator(‘#username’)

 

page.locator(‘.login-button’)

 

page.locator(‘input[name=”email”]’)

 

page.locator(‘button[type=”submit”]’)

 

CSS can be useful when an element has a stable attribute that clearly identifies it.

Good CSS Example

page.locator(‘input[name=”email”]’)

 

Fragile CSS Example

page.locator(‘div.container > div:nth-child(2) > div > button’)

 

Why is the second example risky?

Because changing the page structure can break it.

If a developer adds one <div>, the selector may stop matching the intended element.

XPath in Playwright

XPath can locate elements based on attributes, text, relationships, and document structure.

Example:

await page.locator(‘//button[@type=”submit”]’).click();

 

Another example:

page.locator(‘//input[@name=”username”]’)

 

XPath is powerful, but powerful does not automatically mean better.

A long XPath tied to the DOM structure can become difficult to maintain.

Avoid This Style When Possible

page.locator(

  ‘/html/body/div[1]/div[2]/div[3]/form/div[2]/button’

)

 

If the page layout changes, this XPath can break.

Better Alternative

page.getByRole(‘button’, { name: ‘Login’ })

 

The best locator is not necessarily the shortest locator.

The best locator is the one that clearly identifies the intended element and remains useful when the UI changes.

Playwright Locator Priority: Which Locator Should You Choose?

There is no universal rule that says one locator must always be used for every element.

Instead, use this practical decision process.

Situation

Recommended Locator

Button or link with clear name

getByRole()

Form field with label

getByLabel()

Visible message or text

getByText()

Input with useful placeholder

getByPlaceholder()

Image with meaningful alt text

getByAltText()

Element with stable title

getByTitle()

Explicit automation contract

getByTestId()

Stable CSS attribute

locator()

Complex fallback

CSS/XPath with locator()

The official Playwright recommendation is to prioritize user-facing locators and explicit contracts.

The Most Important Playwright Locator Rule

Locate Elements the Way Users Understand Them

Compare:

page.locator(‘#loginButton’)

 

with:

page.getByRole(‘button’, { name: ‘Login’ })

 

The second version explains what the test actually cares about:

A user sees a Login button.

This is more meaningful than depending only on an implementation-specific ID.

However, this does not mean IDs are always bad.

If an ID is deliberately stable and meaningful, it can be perfectly valid.

The real goal is:

Choose a stable, readable and meaningful locator.

Locator Chaining in Playwright

Sometimes a page contains many elements with the same name.

Imagine an online shopping page:

Laptop

  Add to cart

Keyboard

  Add to cart

Mouse

  Add to cart

This locator may match multiple buttons:

page.getByRole(‘button’, { name: ‘Add to cart’ })

Instead, first locate the product.

const keyboard = page

  .getByRole(‘listitem’)

  .filter({ hasText: ‘Keyboard’ });

Then find the button inside it:

await keyboard

  .getByRole(‘button’, { name: ‘Add to cart’ })

  .click();

This is locator chaining.

Playwright officially supports chaining and filtering to narrow locators to the required part of the page.

Playwright Locator Filtering

Filtering is extremely useful when a page contains repeated components.

Filter by Text

const product = page

  .getByRole(‘listitem’)

  .filter({ hasText: ‘Keyboard’ });

 

Then:

await product

  .getByRole(‘button’, { name: ‘Add to cart’ })

  .click();

 

Filter Using Another Locator

You can also use has.

const product = page

  .getByRole(‘listitem’)

  .filter({

    has: page.getByRole(‘heading’, { name: ‘Keyboard’ })

  });

 

This is useful when text alone is not enough to uniquely identify a component.

hasText vs has

These two concepts are easy to confuse.

hasText

Use it when you want to narrow an element based on text.

.filter({ hasText: ‘Keyboard’ })

 

has

Use it when you want to narrow an element based on another locator.

.filter({

  has: page.getByRole(‘heading’, { name: ‘Keyboard’ })

})

 

A simple way to remember:

hasText → Find by text inside

 

has → Find by another locator inside



Playwright Strict Mode

One of the most important concepts beginners should understand is strictness.

Suppose your page contains three buttons named:

Save

Save

Save

You write:

await page.getByRole(‘button’, { name: ‘Save’ }).click();

Playwright may report a strict-mode violation because the locator identifies multiple matching elements while the action expects one specific target.

This is useful.

It tells you:

“Your locator is not specific enough.”

Instead of immediately using:

.first()

or:

.nth(1)

ask:

Why are multiple elements matching?

Maybe you should scope the locator.

For example:

const settings = page.getByRole(‘dialog’, { name: ‘Settings’ });

await settings

  .getByRole(‘button’, { name: ‘Save’ })

  .click();

Now the Save button is identified inside the Settings dialog.

This is usually more meaningful than blindly selecting the first matching button.

first(), last() and nth()

Playwright provides methods such as:

.first()

 

.last()

 

.nth(2)

 

Example:

await page.getByRole(‘button’).first().click();

 

Or:

await page.getByRole(‘button’).nth(2).click();

 

These can be useful when the order is genuinely meaningful.

But avoid using them simply to hide an ambiguous locator.

Risky Example

page.getByRole(‘button’, { name: ‘Delete’ }).first()

 

If developers reorder the buttons, your test could click a different Delete button.

Better Approach

Scope the locator to the correct component.

const userRow = page

  .getByRole(‘row’)

  .filter({ hasText: ‘John’ });

 

await userRow

  .getByRole(‘button’, { name: ‘Delete’ })

  .click();

Dynamic Elements and Playwright Locators

Modern websites often generate dynamic content.

Examples include:

  • Product lists.
  • Notifications.
  • Search results.
  • User tables.
  • Dashboards.
  • Popups.
  • React components.
  • Angular components.
  • Infinite scrolling lists.

Avoid depending on unstable generated values when possible.

For example, this can be fragile:

page.locator(‘#user-483927’)

 

if the number changes every time.

Instead, identify the user using stable information:

page

  .getByRole(‘row’)

  .filter({ hasText: ‘John’ });

 

Then locate the required action within that row.

This is one of the biggest differences between writing a locator that works today and engineering a locator that survives tomorrow.

Playwright Locators and Auto-Waiting

One reason Playwright locators are powerful is that Playwright performs actionability checks before actions such as clicking.

For example:

await page.getByRole(‘button’, { name: ‘Submit’ }).click();

 

Playwright can wait for the target to become actionable instead of requiring you to add arbitrary sleeps.

The official best-practices documentation specifically recommends relying on Playwright’s locator auto-waiting rather than introducing unnecessary manual waiting.

Avoid This

await page.waitForTimeout(3000);

await page.getByRole(‘button’, { name: ‘Submit’ }).click();

 

The three-second delay may be:

  • Too short on a slow system.
  • Too long on a fast system.
  • Unnecessary if Playwright can wait for the actionability condition.

Prefer:

await page.getByRole(‘button’, { name: ‘Submit’ }).click();



Locator vs Selector: What Is the Difference?

These terms are often used interchangeably, but there is a useful distinction.

A selector is a pattern used to identify an element.

Examples:

#username

.login-button

//button[@type=’submit’]

 

A locator is Playwright’s higher-level mechanism for identifying and interacting with elements.

Examples:

page.getByRole(‘button’, { name: ‘Login’ })

 

page.getByLabel(‘Username’)

 

page.locator(‘#username’)

 

So:

Selector = way to describe an element

 

Locator = Playwright object/API used to work with that element

Playwright Locators vs Selenium Locators

If you are coming from Selenium, the concept will feel familiar.

Selenium commonly uses:

id

name

class name

CSS

XPath

link text

 

Playwright supports CSS and XPath too, but it adds a strong set of user-facing locator APIs:

getByRole()

getByLabel()

getByText()

getByPlaceholder()

getByAltText()

getByTitle()

getByTestId()

 

Playwright’s locator model is designed around readable element identification, auto-waiting and retryability.

For someone moving from Selenium to Playwright, learning getByRole() and locator chaining is especially valuable.

Real-World Login Test Using Playwright Locators

Here is a complete beginner-friendly example.

import { test, expect } from ‘@playwright/test’;

 

test(‘login test’, async ({ page }) => {

 

  await page.goto(‘https://example.com/login’);

 

  await page.getByLabel(‘Username’).fill(‘john123’);

 

  await page.getByLabel(‘Password’).fill(‘password123’);

 

  await page.getByRole(‘button’, { name: ‘Login’ }).click();

 

  await expect(

    page.getByRole(‘heading’, { name: ‘Dashboard’ })

  ).toBeVisible();

 

});

 

Notice the structure:

Open page

   ↓

Find username

   ↓

Enter username

   ↓

Find password

   ↓

Enter password

   ↓

Find Login button

   ↓

Click

   ↓

Find Dashboard heading

   ↓

Verify

 

This is much easier to understand than a test filled with long XPath expressions.

Real-World E-Commerce Locator Example

Imagine an online store has:

Laptop

₹50,000

Add to cart

 

Keyboard

₹2,000

Add to cart

 

You want to add only the keyboard.

Use:

const keyboard = page

  .getByRole(‘listitem’)

  .filter({ hasText: ‘Keyboard’ });

 

await keyboard

  .getByRole(‘button’, { name: ‘Add to cart’ })

  .click();

 

The test is saying:

Find the product containing “Keyboard”, then click its Add to cart button.

That is a strong locator strategy because it describes the business action rather than the page’s exact HTML structure.

Locating Elements Inside a Dialog

Suppose a page contains multiple Save buttons but one belongs to a Settings dialog.

Use:

const settingsDialog = page.getByRole(‘dialog’, {

  name: ‘Settings’

});

 

await settingsDialog

  .getByRole(‘button’, { name: ‘Save’ })

  .click();

 

This is an example of scoping.

Instead of searching the whole page, you tell Playwright where to search.

Locating Elements Inside an Iframe

Frames have their own document context.

Playwright provides frameLocator() for working with elements inside frames.

Example:

const frame = page.frameLocator(‘#payment-frame’);

 

await frame

  .getByLabel(‘Card Number’)

  .fill(‘4111111111111111’);

 

You can then continue locating elements inside that frame.

This is much cleaner than trying to treat iframe content as if it were part of the main document.

Using Regular Expressions with Locators

Regular expressions are useful when text changes slightly.

For example:

await expect(

  page.getByText(/welcome, john/i)

).toBeVisible();

 

You can also use a regular expression with role names:

await page.getByRole(‘button’, {

  name: /submit/i

}).click();

 

This can match variations such as:

Submit

SUBMIT

Submit Form

 

Use regular expressions when flexibility is useful.

Do not use them simply because exact matching is available.

Exact Matching in Playwright

Suppose the page contains:

Save

Save changes

Save and continue

 

This:

page.getByText(‘Save’)

 

may match more than you expect depending on the surrounding content.

Use:

page.getByText(‘Save’, { exact: true })

 

when you specifically need the exact text.

Exact matching is particularly useful when similar labels appear on the same page.

Locator Reuse

You can store a locator in a variable.

const loginButton = page.getByRole(

  ‘button’,

  { name: ‘Login’ }

);

 

await expect(loginButton).toBeVisible();

 

await loginButton.click();

 

This improves readability when the same locator is used multiple times.

It also works well with Page Object Model.

Playwright Locators in Page Object Model

In a real automation framework, you should avoid putting every locator directly inside every test.

A Page Object can store commonly used locators.

Example:

import { Page, Locator } from ‘@playwright/test’;

export class LoginPage {

  readonly username: Locator;

  readonly password: Locator;

  readonly loginButton: Locator;

  constructor(private page: Page) {

    this.username = page.getByLabel(‘Username’);

    this.password = page.getByLabel(‘Password’);

    this.loginButton = page.getByRole(

      ‘button’,

      { name: ‘Login’ }

    );

  }

  async login(username: string, password: string) {

    await this.username.fill(username);

    await this.password.fill(password);

    await this.loginButton.click();

  }

}

Your test becomes:

const loginPage = new LoginPage(page);

await loginPage.login(

  ‘john123’,

  ‘password123’

);

This keeps locator definitions in one place.

If the UI changes, you have fewer places to update.

Playwright Codegen for Finding Locators

You do not always need to write every locator from zero.

Playwright provides Codegen, which can generate test code while you interact with a webpage.

For example:

npx playwright codegen

 

Or:

npx playwright codegen https://example.com

 

Codegen can help beginners discover possible locators quickly.

But there is an important rule:

Do not blindly copy every locator generated by Codegen.

Review the generated locator.

Ask:

  1. Is it readable?
  2. Is it unique?
  3. Is it stable?
  4. Does it describe the user’s action?
  5. Could the text change frequently?
  6. Would a test ID be a better explicit contract?
  7. Is the locator unnecessarily complicated?

Playwright’s documentation recommends using Codegen to generate a locator and then editing it when appropriate.

Playwright Inspector for Locator Debugging

When a locator does not work, Playwright Inspector can help.

You can run:

npx playwright test –debug

 

The Inspector lets you pause execution, inspect elements and experiment with locators.

This is useful when:

  • A locator matches multiple elements.
  • A locator finds nothing.
  • A dynamic component changes.
  • A locator works in one state but not another.
  • You are unsure about the accessible name.

A good debugging workflow is:

Failing locator

      ↓

Inspect the page

      ↓

Check matching elements

      ↓

Understand why it matches

      ↓

Narrow the locator

      ↓

Run the test again

Common Playwright Locator Errors

Error 1: Locator Matches Multiple Elements

Problem:

page.getByRole(‘button’, { name: ‘Save’ })

 

matches several buttons.

Solution:

Scope it.

const dialog = page.getByRole(‘dialog’, {

  name: ‘Profile’

});

 

await dialog

  .getByRole(‘button’, { name: ‘Save’ })

  .click();

 

Error 2: Locator Finds No Element

Possible reasons include:

  • Wrong accessible name.
  • Element is inside an iframe.
  • Element appears only after another action.
  • Wrong page or URL.
  • Incorrect selector.
  • Element is rendered conditionally.
  • Text does not match what you expected.

Debug the page state instead of immediately adding a timeout.

Common Locator Mistakes Beginners Make

Mistake 1: Using XPath for Everything

Example:

page.locator(‘//div[2]/div[1]/button’)

 

This can be difficult to maintain.

Try a user-facing locator first.

Mistake 2: Using nth() Too Quickly

This:

page.getByRole(‘button’).nth(4)

 

does not explain why the fifth button is the correct one.

Prefer meaningful scoping when possible.

Mistake 3: Using Arbitrary Waits

Avoid:

await page.waitForTimeout(5000);

 

when Playwright can synchronize with the element naturally.

Mistake 4: Selecting by CSS Classes That Are Used for Styling

Example:

page.locator(‘.blue-button’)

 

A designer can change the class without changing the application’s behavior.

Mistake 5: Creating Extremely Long Selectors

Long selectors often indicate that the locator strategy needs improvement.

Instead of:

page.locator(

  ‘div.container > div:nth-child(2) > form > div:nth-child(3) > button’

)

 

look for:

page.getByRole(‘button’, { name: ‘Submit’ })

Best Practices for Playwright Locators

1. Prefer User-Facing Locators

 

Start with:

getByRole()

getByLabel()

getByText()

 

when they clearly describe the intended element.

2. Use Accessible Names

 

Instead of:

getByRole(‘button’)

 

prefer:

getByRole(‘button’, { name: ‘Login’ })

 

when a meaningful accessible name exists.

This makes the locator more specific.

3. Scope Repeated Components

 

Instead of searching the entire page:

page.getByRole(‘button’, { name: ‘Delete’ })

 

scope it to the relevant row:

const row = page

  .getByRole(‘row’)

  .filter({ hasText: ‘John’ });

 

await row

  .getByRole(‘button’, { name: ‘Delete’ })

  .click();

 

4. Use Test IDs Intentionally

 

Use:

getByTestId()

 

when your team has deliberately created a stable test contract.

Do not add test IDs everywhere without a reason.

5. Avoid DOM-Position-Based Locators

 

Avoid relying on:

nth-child

first child

second child

deep XPath

 

unless the position itself is part of the requirement.

6. Let Playwright Handle Synchronization

 

Prefer Playwright’s built-in waiting behavior over arbitrary sleeps.

7. Treat Strictness as Feedback

 

If Playwright tells you that your locator matches multiple elements, do not automatically suppress the problem.

Ask:

“How can I make this locator describe the exact element I mean?”

8. Review Generated Locators

 

Codegen is a productivity tool.

It is not a replacement for locator design.

A Simple Playwright Locator Decision Tree

When you need to locate an element, ask these questions:

Is it an interactive element?

        |

       Yes

        |

Does it have a clear role/name?

        |

       Yes

        ↓

    getByRole()

        |

       No

        ↓

Is it a form field with a label?

        |

       Yes

        ↓

    getByLabel()

        |

       No

        ↓

Does meaningful visible text identify it?

        |

       Yes

        ↓

    getByText()

        |

       No

        ↓

Is there a useful placeholder?

        |

       Yes

        ↓

 getByPlaceholder()

        |

       No

        ↓

Is there a stable test contract?

        |

       Yes

        ↓

   getByTestId()

        |

       No

        ↓

Use a stable CSS/XPath locator

 

This decision tree is more useful than memorizing a single “best locator.”

Playwright Locators Cheat Sheet

Requirement

Example

Button

page.getByRole(‘button’, { name: ‘Login’ })

Link

page.getByRole(‘link’, { name: ‘Home’ })

Heading

page.getByRole(‘heading’, { name: ‘Dashboard’ })

Checkbox

page.getByRole(‘checkbox’, { name: ‘Subscribe’ })

Radio

page.getByRole(‘radio’, { name: ‘Male’ })

Form field

page.getByLabel(‘Email’)

Visible text

page.getByText(‘Success’)

Placeholder

page.getByPlaceholder(‘Search’)

Image

page.getByAltText(‘Logo’)

Title

page.getByTitle(‘Settings’)

Test ID

page.getByTestId(‘submit-button’)

CSS

page.locator(‘#username’)

XPath

page.locator(‘//button[@type=”submit”]’)

Filter text

.filter({ hasText: ‘Laptop’ })

Filter locator

.filter({ has: locator })

First match

.first()

Last match

.last()

Specific index

.nth(2)

Frame

page.frameLocator(‘#frame’)

Playwright Locators: Beginner Practice Exercise

If you are learning Playwright, create a simple login page containing:

Username

Password

Login

Forgot Password?

 

Your task is to create locators for each element.

Username

page.getByLabel(‘Username’)

 

Password

page.getByLabel(‘Password’)

 

Login

page.getByRole(‘button’, { name: ‘Login’ })

 

Forgot Password

page.getByRole(‘link’, {

  name: ‘Forgot Password?’

})

 

Then write a test that:

  1. Opens the login page.
  2. Enters the username.
  3. Enters the password.
  4. Clicks Login.
  5. Verifies the Dashboard.

This small exercise teaches the core locator skills needed for larger automation frameworks.

Intermediate Practice Exercise

Create an e-commerce page with five products.

Each product should contain:

Product Name

Price

Add to Cart

 

Write a test that adds only:

Wireless Keyboard

 

to the cart.

Do not use:

nth()

 

Instead, use:

filter()

 

and locator chaining.

This teaches you a skill that is used frequently in real automation projects.

Advanced Practice Exercise

Create a page containing:

  • Multiple tables.
  • Multiple rows.
  • Multiple Edit buttons.
  • Multiple Delete buttons.
  • A Settings dialog.
  • Dynamic notifications.

Then write tests that:

  1. Find a specific user.
  2. Edit only that user.
  3. Delete only that user.
  4. Verify a success notification.
  5. Work correctly even if other rows are added.

This exercise teaches scoping, filtering, strictness and stable locator design.

How to Make Playwright Locators Stable

A stable locator should answer three questions:

1. What am I locating?

Example:

Login button

 

2. Why is this the correct element?

Example:

It is the Login button inside the Login dialog.

 

3. What could change without breaking my locator?

If your locator depends on:

div number

CSS position

generated class

DOM depth

 

it may be fragile.

If it depends on:

role

accessible name

label

stable test ID

meaningful text

 

it may better represent the intended behavior.

Locator Stability Is More Important Than Locator Length

A common beginner mistake is thinking:

“The shorter locator is always better.”

That is not true.

Consider:

page.locator(‘button’)

 

It is very short.

But if the page has 15 buttons, it is not useful for identifying one specific button.

Now consider:

page.getByRole(‘button’, {

  name: ‘Place Order’

})

 

It is slightly longer, but it communicates the exact purpose.

The goal is not:

Shortest locator.

The goal is:

Stable + readable + unique + meaningful locator.

Accessibility and Playwright Locators

There is another important benefit to role-based locators.

When you write:

page.getByRole(‘button’, {

  name: ‘Submit’

})

 

you are testing the element in a way related to how users and assistive technologies perceive it.

This can encourage better accessible markup.

However, an important distinction is:

Using role locators does not mean your application has passed an accessibility audit.

Functional locator success is not the same as full accessibility compliance.

Playwright’s official documentation makes this distinction clear.

Playwright Locators for Dynamic Web Applications

Modern applications can change while a test is running.

For example:

User clicks Search

        ↓

API request starts

        ↓

Loading appears

        ↓

Results are rendered

        ↓

Buttons become available

 

A locator such as:

const result = page.getByRole(

  ‘button’,

  { name: ‘Buy Now’ }

);

 

can be used when the element becomes available.

You generally do not need to locate the element once and manually manage a stale DOM reference.

This locator-based model is one reason Playwright is well suited to modern web applications.

10 Rules for Writing Better Playwright Locators

Remember these rules:

Rule 1

Prefer meaningful user-facing locators.

Rule 2

Use getByRole() for clearly identifiable interactive elements.

Rule 3

Use getByLabel() for properly labelled form fields.

Rule 4

Use getByText() when visible text is the meaningful identifier.

Rule 5

Use getByTestId() when an explicit testing contract is appropriate.

Rule 6

Use chaining and filtering for repeated components.

Rule 7

Do not use nth() merely to hide ambiguous locators.

Rule 8

Avoid long DOM-dependent CSS and XPath when a stronger locator exists.

Rule 9

Do not add arbitrary waitForTimeout() calls to solve locator problems.

Rule 10

When a locator fails, understand why before changing it.

Final Takeaway

Playwright Locators are the foundation of reliable Playwright automation.

Do not learn them as a list of commands.

Learn how to choose the right locator for the right situation.

Start with:

getByRole()

getByLabel()

getByText()

getByPlaceholder()

getByAltText()

getByTitle()

getByTestId()

Then learn:

locator()

CSS

XPath

chaining

filter()

hasText

has

strictness

frames

dynamic elements

Codegen

Inspector

Most importantly, remember this:

A good locator does not merely find an element. It explains why that element is the one your test intends to use.

If you can look at a real application and quickly decide whether to use getByRole(), getByLabel(), getByText(), getByTestId(), filtering, chaining, CSS, or XPath, you have moved beyond memorizing Playwright syntax.

You are developing a real Playwright automation engineering skill.

Frequently Asked Questions About Playwright Locators

What are Playwright Locators?

Playwright locators are APIs used to identify elements on a webpage so Playwright can interact with or verify them.

Examples include getByRole(), getByText(), getByLabel(), getByPlaceholder(), getByTestId(), and locator().

Which Playwright locator is best?

There is no single locator that is best for every element.

For many interactive elements, start with:

getByRole()

 

For form fields, consider:

getByLabel()

 

For meaningful visible text:

getByText()

 

For an explicit testing contract:

getByTestId()

 

Use CSS or XPath when they provide a suitable stable way to identify an element.

Playwright’s official guidance is to prioritize user-facing locators and explicit contracts.

Is getByRole() better than CSS?

For many user-facing interactive elements, getByRole() is a strong choice because it describes the element semantically.

For example:

page.getByRole(‘button’, { name: ‘Login’ })

 

can communicate intent more clearly than:

page.locator(‘#login’)

 

However, a stable CSS selector can still be appropriate when it represents a reliable application or testing contract.

Is XPath supported in Playwright?

Yes.

For example:

page.locator(‘//button[@type=”submit”]’)

 

Playwright supports XPath through locator(). However, long structural XPath expressions can be difficult to maintain, so use them when they are genuinely useful rather than as the default strategy.

Is CSS supported in Playwright?

Yes.

Example:

page.locator(‘#username’)

 

CSS is useful when you have a stable and meaningful selector.

Avoid building selectors that depend heavily on DOM position or presentation-only classes.

What is getByTestId() in Playwright?

getByTestId() finds an element using a test ID.

Example:

page.getByTestId(‘login-button’)

 

The default attribute is:

data-testid

 

Example:

<button data-testid=”login-button”>

  Login

</button>

 

Playwright also allows the test ID attribute to be configured.

What is locator chaining in Playwright?

Locator chaining means using one locator to narrow the search before locating another element.

Example:

const product = page

  .getByRole(‘listitem’)

  .filter({ hasText: ‘Laptop’ });

 

await product

  .getByRole(‘button’, { name: ‘Add to cart’ })

  .click();

 

It is especially useful for repeated UI components.

What is Playwright locator filtering?

Filtering narrows a locator to specific elements.

Example:

page

  .getByRole(‘listitem’)

  .filter({ hasText: ‘Laptop’ });

 

You can also filter using another locator:

.filter({

  has: page.getByRole(‘heading’, { name: ‘Laptop’ })

})

 

What is Playwright strict mode?

Playwright’s strictness helps identify cases where a locator intended for a single element actually matches multiple elements.

Instead of hiding the ambiguity with .first() or .nth(), improve the locator so it clearly identifies the intended element.

Should I use XPath for every Playwright element?

No.

XPath is powerful, but it should not automatically be your first choice.

Try:

getByRole()

getByLabel()

getByText()

getByPlaceholder()

getByTestId()

 

before writing complicated XPath expressions.

Should I use CSS selectors or Playwright locators?

Use whichever provides the clearest stable locator for the element.

For example:

page.getByRole(‘button’, { name: ‘Login’ })

 

is often a strong choice for a Login button.

A CSS selector such as:

page.locator(‘[data-testid=”login-button”]’)

 

may be appropriate when your application deliberately defines that test contract.

How do I debug a Playwright locator?

Start with:

npx playwright test –debug

 

You can use Playwright Inspector to inspect the page, test locators and understand why a locator matches or fails.

Also check whether the element is inside an iframe, whether its accessible name is what you expect, and whether multiple elements match.

Why does my Playwright locator work sometimes and fail sometimes?

Common causes include:

  • Unstable locator.
  • Dynamic page content.
  • Multiple matching elements.
  • Incorrect page state.
  • Iframe content.
  • Changing text.
  • Race conditions caused by poor synchronization.
  • Overly specific CSS or XPath.

First inspect the locator and page state instead of adding a fixed sleep.

Do Playwright locators automatically wait?

Playwright locators work with Playwright’s auto-waiting and retryability mechanisms. Before actions, Playwright performs relevant actionability checks rather than simply clicking immediately.

This is one reason Playwright tests can be written without adding arbitrary delays for many normal UI interactions.

Can Playwright locators handle React applications?

Yes.

Playwright locators are designed to work with dynamic modern web applications, including applications whose DOM is updated or re-rendered.

A locator is resolved against the current page state when it is used, which helps with changing DOM content.

Can I use Playwright locators with Page Object Model?

Yes.

Locators are commonly stored as properties in Page Object classes.

Example:

class LoginPage {

  username = this.page.getByLabel(‘Username’);

  password = this.page.getByLabel(‘Password’);

  login = this.page.getByRole(‘button’, {

    name: ‘Login’

  });

}

 

This centralizes locator definitions and makes large test suites easier to maintain.

Playwright Masters automation testing logo - White Back ground

Playwright Masters Team

Playwright Automation Testing Experts | Industry-Focused Training & Practical Learning

Playwright Masters is a dedicated automation testing training platform focused on helping learners build practical skills in Playwright automation testing. Our training covers real-world automation concepts, framework practices, coding, debugging, API testing, cross-browser testing, CI/CD, and interview preparation to help learners develop job-ready testing skills.

Scroll to Top