1. What is a Locator?
A locator is Playwright's way of identifying an element on a web page. Instead of directly depending on an element's current position in the DOM, Playwright allows you to describe the element using user-facing properties, accessibility information, CSS, text, labels, roles, test IDs, and more.
2. Different Types of Playwright Locators
Role Locator
Locates elements using their accessible role and accessible name.
RecommendedLabel Locator
Useful for form fields associated with labels.
RecommendedPlaceholder Locator
Finds inputs using their placeholder text.
UsefulText Locator
Locates elements based on visible text.
UsefulTest ID
Uses a dedicated testing attribute such as data-testid.
Very StableCSS Locator
Uses CSS selectors to locate elements.
Use CarefullyXPath
Uses XPath expressions to locate elements.
Last ResortChained Locator
Narrows the search from a parent element to a child element.
ProfessionalFiltering
Finds elements using text, attributes, or other locator conditions.
Professional3. Role Locator — Preferred Starting Point
Playwright recommends user-facing locators such as roles because they closely represent how users and assistive technologies interact with the application.
HTML
<button>Login</button> <button>Register</button> <input type="email" aria-label="Email">
Playwright
page.get_by_role("button", name="Login").click()
page.get_by_role("button", name="Register").click()
page.get_by_role("textbox", name="Email").fill("user@example.com")
btn-primary to button-main,
the role-based locator can continue to work as long as the
user-facing semantics remain the same.
4. Label Locator — Excellent for Forms
When an input is associated with a visible label, use
get_by_label().
<label for="email">Email Address</label> <input id="email" type="email"> <label for="password">Password</label> <input id="password" type="password">
page.get_by_label("Email Address").fill("user@example.com")
page.get_by_label("Password").fill("Secret123")
5. Placeholder Locator
Inputs frequently contain placeholder text such as "Search products", "Enter email", or "Search customers".
<input
type="text"
placeholder="Search products"
>
page.get_by_placeholder("Search products").fill("Laptop")
get_by_label().
6. Text Locator
Text locators are useful when a user-visible piece of text uniquely identifies an element.
page.get_by_text("Order placed successfully").click()
Example
message = page.get_by_text("Payment successful")
message.wait_for()
7. Test ID — Strong Contract Between App and Test
A dedicated test ID can be one of the most stable ways to identify an element when user-facing semantics are not unique or appropriate.
Application HTML
<button data-testid="checkout-button">
Checkout
</button>
Playwright
page.get_by_test_id("checkout-button").click()
8. CSS Locator
CSS selectors are powerful and sometimes necessary, especially when working with custom components or attributes.
page.locator("#login-button").click()
page.locator(".product-card").first.click()
page.locator("[data-status='active']").click()
Attribute Selector
page.locator("input[name='email']").fill("user@example.com")
9. XPath Locator
Playwright supports XPath, but XPath should generally not be your first choice when a more resilient locator is available.
page.locator("//button[@type='submit']").click()
Fragile XPath Example
page.locator(
"/html/body/div[2]/div[1]/div/form/div[3]/button"
).click()
10. Chained Locators — Find the Right Element Inside a Component
Modern applications frequently contain repeated components. Instead of selecting a generic button globally, first identify the correct component and then locate the button inside it.
Example UI
<div class="product-card">
<h3>Laptop</h3>
<button>Add to Cart</button>
</div>
<div class="product-card">
<h3>Phone</h3>
<button>Add to Cart</button>
</div>
Robust Locator
laptop = page.get_by_role(
"heading",
name="Laptop"
).locator("..")
laptop.get_by_role(
"button",
name="Add to Cart"
).click()
An even stronger approach for component-heavy applications is to give the component a stable test ID and locate the child element from it.
product = page.get_by_test_id("product-card-laptop")
product.get_by_role(
"button",
name="Add to Cart"
).click()
11. Filtering Locators
Filtering is extremely useful when a page contains many similar elements.
Example
product = page.locator(".product-card").filter(
has_text="Laptop"
)
product.get_by_role(
"button",
name="Add to Cart"
).click()
Filtering by Child Locator
card = page.locator(".product-card").filter(
has=page.get_by_role(
"heading",
name="Laptop"
)
)
card.get_by_role(
"button",
name="Add to Cart"
).click()
12. Nested Locators for Real Applications
Consider an application showing customer orders. Each order has a customer name, status, amount and action buttons.
order = page.locator(".order-row").filter(
has_text="John Smith"
)
order.get_by_role(
"button",
name="View Order"
).click()
The test is not dependent on whether John's order happens to be row 1, row 5, or row 100.
13. How Playwright Makes Locators Robust
One of Playwright's important strengths is that locator actions automatically wait for the target element to become ready.
page.get_by_role(
"button",
name="Submit"
).click()
You generally do not need to write a manual sleep before every action. Playwright waits for relevant actionability conditions and retries locating the element when appropriate.
time.sleep(5)
as your default synchronization strategy.
14. Dynamic Element Example
Imagine a shopping application where the Checkout button appears only after the cart has finished loading.
page.get_by_test_id("cart-loader").wait_for(
state="hidden"
)
page.get_by_role(
"button",
name="Checkout"
).click()
For assertions, prefer Playwright's web-first assertions.
from playwright.sync_api import expect
expect(
page.get_by_role(
"button",
name="Checkout"
)
).to_be_visible()
15. Locator Comparison
| Locator | Example | Typical Stability | Best Use |
|---|---|---|---|
| Role | get_by_role() |
High | Buttons, links, headings, controls |
| Label | get_by_label() |
High | Forms and inputs |
| Test ID | get_by_test_id() |
Very High | Stable application contracts |
| Placeholder | get_by_placeholder() |
Medium | Inputs without strong labels |
| Text | get_by_text() |
Medium | User-visible messages and content |
| CSS | locator(".class") |
Variable | Custom DOM targeting |
| XPath | locator("//...") |
Variable | Special cases |
| Filter | locator().filter() |
High | Repeated components |
16. Fragile vs Robust Locators
❌ Fragile
page.locator(
"div.container:nth-child(2) > div:nth-child(3) > button"
).click()
❌ Generated Class
page.locator(".css-1x7abc9").click()
❌ Index-Based Selection
page.locator("button").nth(4).click()
✅ Robust
page.get_by_role(
"button",
name="Submit Order"
).click()
17. Real-Time Example — Login Page
Consider a typical enterprise login page.
<label for="username">Username</label> <input id="username" type="text"> <label for="password">Password</label> <input id="password" type="password"> <button type="submit">Sign In</button>
def login(page, username, password):
page.get_by_label("Username").fill(username)
page.get_by_label("Password").fill(password)
page.get_by_role(
"button",
name="Sign In"
).click()
18. Real-Time Example — E-Commerce Product
Assume a page contains hundreds of products. Each product has the same "Add to Cart" button.
product = page.locator(".product-card").filter(
has_text="MacBook Pro"
)
product.get_by_role(
"button",
name="Add to Cart"
).click()
This is more maintainable than selecting the 7th "Add to Cart" button.
19. Real-Time Example — Customer Table
Imagine an admin dashboard containing thousands of customer records.
customer = page.locator("tr").filter(
has_text="John Smith"
)
customer.get_by_role(
"button",
name="View"
).click()
The locator identifies the row based on its content and then performs the action inside that specific row.
20. Locator Inside an iframe
Modern applications sometimes embed payment widgets, support tools, reports, or third-party components inside iframes.
frame = page.frame_locator(
"#payment-frame"
)
frame.get_by_label(
"Card number"
).fill("4111111111111111")
frame_locator() to work with them.
21. Debugging Locators
When a locator does not behave as expected, first inspect how many elements it matches.
locator = page.get_by_role(
"button",
name="Submit"
)
print(locator.count())
Playwright's Inspector and code generation tools can also help you discover and validate locators during development.
22. Recommended Locator Strategy
23. Professional Locator Design
A professional automation framework should treat locators as part of the test architecture rather than scattering selectors throughout tests.
Example Page Object
from playwright.sync_api import Page
class LoginPage:
def __init__(self, page: Page):
self.page = page
self.username = page.get_by_label("Username")
self.password = page.get_by_label("Password")
self.sign_in = page.get_by_role(
"button",
name="Sign In"
)
def login(self, username, password):
self.username.fill(username)
self.password.fill(password)
self.sign_in.click()
Test
def test_login(page):
login_page = LoginPage(page)
login_page.login(
"testuser",
"TestPassword123"
)
24. What Makes a Locator Robust?
- It identifies the intended element uniquely.
- It reflects stable user-facing behavior or an intentional test contract.
- It does not depend unnecessarily on DOM position.
- It does not depend on generated CSS classes.
-
It avoids unnecessary
nth()usage. - It works correctly when the page contains repeated components.
- It can survive reasonable UI implementation changes.
- It works naturally with Playwright's auto-waiting behavior.
- It is readable enough for another automation engineer to understand.
25. Bad vs Good Locator Thinking
| Bad Question | Better Question |
|---|---|
| What is the CSS class? | What does this element represent to the user? |
| What is the element's position? | What uniquely identifies this component? |
| Can I use XPath? | Is there a semantic or stable locator available? |
| Can I click the 5th button? | Which business component contains the button? |
| How do I force the click? | Why is the intended element not actionable? |
26. Complete Robust Locator Example
from playwright.sync_api import Page, expect
def test_customer_order(page: Page):
page.goto("https://example.com/orders")
# Find the customer row using business-visible information
customer = page.locator("tr").filter(
has_text="John Smith"
)
# Verify customer is displayed
expect(customer).to_be_visible()
# Find the order action inside that customer row
customer.get_by_role(
"button",
name="View Order"
).click()
# Verify destination
expect(
page.get_by_role(
"heading",
name="Order Details"
)
).to_be_visible()
27. Playwright Locator Cheat Sheet
| Goal | Playwright |
|---|---|
| Button | page.get_by_role("button", name="Login") |
| Link | page.get_by_role("link", name="Home") |
| Input by label | page.get_by_label("Email") |
| Input by placeholder | page.get_by_placeholder("Search") |
| Visible text | page.get_by_text("Success") |
| Test ID | page.get_by_test_id("submit") |
| CSS | page.locator(".submit-button") |
| XPath | page.locator("//button") |
| Filter by text | locator.filter(has_text="Laptop") |
| Filter by locator | locator.filter(has=child_locator) |
| Iframe | page.frame_locator("#frame") |
28. Locator Best Practices
-
Prefer
get_by_role()when the element has meaningful accessible semantics. -
Use
get_by_label()for properly labeled form controls. -
Use
get_by_test_id()when a stable testing contract is appropriate. - Use text when the visible text itself is a meaningful identifier.
- Use filtering for repeated components such as cards and table rows.
- Avoid selectors that depend on generated CSS class names.
- Avoid long DOM paths.
-
Avoid unnecessary
nth()selectors. - Avoid fixed sleeps as a synchronization strategy.
- Use Playwright's web-first assertions for dynamic UI validation.
29. Final Mental Model
Once you understand this mindset, locator design becomes much easier. Playwright then provides the synchronization and retry behavior around those locators so your tests can remain readable and maintainable.
30. Next Learning Step
Next → Assertions
Learn how Playwright verifies text, visibility, URL, attributes, values and application state using web-first assertions.
Then → Waits
Understand auto-waiting, actionability, explicit waiting, network synchronization and why hard sleeps should be avoided.
Professional → Page Object Model
Move locators into reusable page classes and build scalable automation frameworks.