A locator is the mechanism Playwright uses to identify an element on a web page before performing an action or assertion.
User-Facing
Locate elements through roles, labels, visible text, placeholders and other user-facing properties.
DOM-Based
CSS selectors, XPath and attributes can be used when structural or technical identification is appropriate.
Composable
Locators can be chained, filtered and narrowed to a particular component.
| Strategy | Playwright API | Typical Use |
|---|---|---|
| Role | get_by_role() | Buttons, links, headings, checkboxes, radio buttons, etc. |
| Label | get_by_label() | Form fields associated with labels. |
| Text | get_by_text() | Visible user-facing text. |
| Placeholder | get_by_placeholder() | Inputs with meaningful placeholders. |
| Alt Text | get_by_alt_text() | Images and elements identified through alt text. |
| Title | get_by_title() | Elements with title attributes. |
| Test ID | get_by_test_id() | Explicit automation identifiers. |
| CSS | locator() | Stable DOM selectors. |
| XPath | locator() | Complex structural relationships when appropriate. |
Role locators identify elements using their accessible role and accessible name.
page.get_by_role(
"button",
name="Login"
).click()
get_by_role("button", name="Login")
Finds a button named Login.
get_by_role("link", name="Orders")
Finds a navigation link named Orders.
get_by_role("heading", name="Dashboard")
Finds a heading named Dashboard.
get_by_role("checkbox", name="Remember me")
Finds the Remember me checkbox.
get_by_role("radio", name="Credit Card")
Finds the Credit Card radio option.
Label locators are particularly useful for forms.
<label for="email">
Email Address
</label>
<input
id="email"
type="email"
/>
page.get_by_label(
"Email Address"
).fill(
"john@example.com"
)
Use this locator when the visible text itself is the meaningful way to identify an element.
page.get_by_text(
"Order Confirmed"
).is_visible()
Exact Text
page.get_by_text(
"Save",
exact=True
).click()
Useful when the placeholder is meaningful and stable.
<input
placeholder="Enter username"
/>
page.get_by_placeholder(
"Enter username"
).fill(
"john"
)
Alt Text
<img
src="logo.png"
alt="Company Logo"
/>
page.get_by_alt_text(
"Company Logo"
).click()
Title
<button
title="Delete User"
>
🗑
</button>
page.get_by_title(
"Delete User"
).click()
A test ID is useful when user-facing semantics are not suitable or are not stable enough.
<button
data-testid="submit-order"
>
Submit
</button>
page.get_by_test_id(
"submit-order"
).click()
Playwright supports CSS selectors through the generic locator() API.
# ID
page.locator("#username")
# Class
page.locator(".login-button")
# Attribute
page.locator(
'input[name="email"]'
)
# Multiple classes
page.locator(
".btn.btn-primary"
)
# Descendant
page.locator(
".checkout-form button"
)
Playwright supports XPath through locator().
page.locator(
'//button[normalize-space()="Login"]'
).click()
page.locator(
'//input[@name="email"]'
).fill(
"john@example.com"
)
page.locator(
'//div[@class="product-card"]'
)
Playwright lets you locate a component first and then locate an element inside it.
<div id="login-form">
<input
placeholder="Username"
>
<input
placeholder="Password"
>
<button>
Login
</button>
</div>
login_form = page.locator(
"#login-form"
)
login_form.get_by_placeholder(
"Username"
).fill("john")
login_form.get_by_placeholder(
"Password"
).fill("Password123")
login_form.get_by_role(
"button",
name="Login"
).click()
Filtering is especially useful when several components contain identical buttons.
<div class="product-card">
<h3>Laptop Pro</h3>
<button>Add to Cart</button>
</div>
<div class="product-card">
<h3>Phone Pro</h3>
<button>Add to Cart</button>
</div>
product = page.locator(
".product-card"
).filter(
has_text="Laptop Pro"
)
product.get_by_role(
"button",
name="Add to Cart"
).click()
You can filter one locator based on another locator.
product = page.locator(
".product-card"
).filter(
has=page.get_by_role(
"heading",
name="Laptop Pro"
)
)
product.get_by_role(
"button",
name="Add to Cart"
).click()
When multiple matching elements exist, Playwright provides methods for selecting a particular match.
# First matching element
page.get_by_role(
"button",
name="Edit"
).first.click()
# Last matching element
page.get_by_role(
"button",
name="Edit"
).last.click()
# Specific zero-based index
page.get_by_role(
"button",
name="Edit"
).nth(2).click()
A typical enterprise login page might contain Email, Password, Remember Me and Sign In.
from playwright.sync_api import expect
def test_login(page):
page.goto(
"https://example.test/login"
)
page.get_by_label(
"Email"
).fill(
"john@example.com"
)
page.get_by_label(
"Password"
).fill(
"Password123"
)
page.get_by_role(
"checkbox",
name="Remember me"
).check()
page.get_by_role(
"button",
name="Sign In"
).click()
expect(page).to_have_url(
r".*/dashboard"
)
def test_customer_order(page):
page.goto(
"https://shop.example.test"
)
expect(
page.get_by_role(
"heading",
name="Products"
)
).to_be_visible()
product = page.locator(
".product-card"
).filter(
has_text="Laptop Pro"
)
product.get_by_role(
"button",
name="Add to Cart"
).click()
page.get_by_role(
"link",
name="Cart"
).click()
page.get_by_role(
"button",
name="Checkout"
).click()
page.get_by_label(
"Email"
).fill(
"john@example.com"
)
page.get_by_role(
"button",
name="Place Order"
).click()
expect(
page.get_by_role(
"heading",
name="Order Confirmed"
)
).to_be_visible()
Dynamic values such as order IDs can be handled with regular expressions.
Order ORD-1001 has been successfully created.
import re
expect(
page.get_by_text(
re.compile(
r"Order ORD-\d+ has been successfully created"
)
)
).to_be_visible()
Potentially Fragile
<button
class="css-92871"
>
Place Order
</button>
page.locator(
".css-92871"
).click()
Generated classes may change when the application is rebuilt.
User-Facing
page.get_by_role(
"button",
name="Place Order"
).click()
The locator describes the intended user interaction.
Locators become especially powerful when combined with Playwright's web-first assertions.
Immediate Check
assert page.get_by_text(
"Success"
).is_visible()
Web-First Assertion
expect(
page.get_by_text(
"Success"
)
).to_be_visible()
Playwright actions perform relevant actionability checks before interacting with elements.
page.get_by_role(
"button",
name="Submit"
).click()
You normally should not solve synchronization problems by inserting arbitrary delays.
Avoid
import time
page.get_by_role(
"button",
name="Submit"
).click()
time.sleep(5)
Prefer
page.get_by_role(
"button",
name="Submit"
).click()
expect(
page.get_by_text("Success")
).to_be_visible()
Selenium can use several locator mechanisms to identify elements.
from selenium.webdriver.common.by import By
# ID
driver.find_element(
By.ID,
"username"
)
# CSS
driver.find_element(
By.CSS_SELECTOR,
".login-button"
)
# XPath
driver.find_element(
By.XPATH,
"//button[normalize-space()='Login']"
)
# Link text
driver.find_element(
By.LINK_TEXT,
"My Orders"
)
| Requirement | Playwright | Selenium |
|---|---|---|
| By ID | page.locator("#username") | driver.find_element(By.ID, "username") |
| By CSS | page.locator(".login-button") | driver.find_element(By.CSS_SELECTOR, ".login-button") |
| By XPath | page.locator("//button[text()='Login']") | driver.find_element(By.XPATH, "//button[text()='Login']") |
| By visible text | page.get_by_text("Login") | Commonly XPath or link-text APIs. |
| By role | page.get_by_role("button", name="Login") | Usually represented using CSS/XPath or other locator mechanisms. |
| By label | page.get_by_label("Email") | Usually handled using CSS/XPath and HTML relationships. |
| By placeholder | page.get_by_placeholder("Username") | Usually CSS/XPath. |
| Test ID | page.get_by_test_id("login") | CSS/XPath can target the data attribute. |
| Filtering | locator.filter() | Usually requires additional selector or lookup logic. |
Playwright
page.goto(
"https://example.test/login"
)
page.get_by_label(
"Email"
).fill(
"john@example.com"
)
page.get_by_label(
"Password"
).fill(
"Password123"
)
page.get_by_role(
"button",
name="Sign In"
).click()
Selenium
driver.get(
"https://example.test/login"
)
driver.find_element(
By.ID,
"email"
).send_keys(
"john@example.com"
)
driver.find_element(
By.ID,
"password"
).send_keys(
"Password123"
)
driver.find_element(
By.XPATH,
"//button[normalize-space()='Sign In']"
).click()
Selenium
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
button = WebDriverWait(
driver,
10
).until(
EC.element_to_be_clickable(
(By.ID, "login")
)
)
button.click()
Playwright
page.get_by_role(
"button",
name="Login"
).click()
page.get_by_role(
"checkbox",
name="Accept Terms"
).check()
page.get_by_role(
"radio",
name="Credit Card"
).check()
page.get_by_role(
"combobox",
name="Country"
).select_option("IN")
submit = page.get_by_role(
"button",
name="Submit Order"
)
expect(
submit
).to_be_enabled()
submit.click()
remember = page.get_by_role(
"checkbox",
name="Remember me"
)
remember.check()
expect(
remember
).to_be_checked()
Consider a transfer workflow in an online banking application.
def test_transfer(page):
page.goto(
"https://bank.example.test/transfer"
)
expect(
page.get_by_role(
"heading",
name="Transfer Money"
)
).to_be_visible()
page.get_by_label(
"Amount"
).fill(
"500"
)
page.get_by_role(
"button",
name="Review Transfer"
).click()
expect(
page.get_by_text(
"Review your transfer"
)
).to_be_visible()
Suppose an admin application displays several customer records.
<div class="customer-card">
<h2>John Smith</h2>
<p>Premium Customer</p>
<button>
View Profile
</button>
</div>
customer = page.locator(
".customer-card"
).filter(
has_text="John Smith"
)
customer.get_by_role(
"button",
name="View Profile"
).click()
| Question | Recommended Locator | Example |
|---|---|---|
| Is it a button? | Role | get_by_role("button", name="Save") |
| Is it a link? | Role | get_by_role("link", name="Orders") |
| Is it a labeled input? | Label | get_by_label("Email") |
| Is visible text the identity? | Text | get_by_text("Order Confirmed") |
| Is placeholder stable? | Placeholder | get_by_placeholder("Search") |
| Do you have a stable automation ID? | Test ID | get_by_test_id("checkout") |
| Is there a stable DOM selector? | CSS | locator(".product-card") |
| Do you need a complex structural relationship? | XPath | locator("//div[@data-id='123']") |
A stable test ID can be preferable to a frequently changing accessible name. Likewise, a stable CSS selector can be appropriate for a technical element that has no meaningful user-facing identity.
- Using XPath for every element.
- Using generated CSS classes.
- Using long absolute XPath expressions.
- Using text that changes frequently.
- Using partial text when several elements match.
- Using nth() when a unique locator is available.
- Using hard waits instead of waiting for application state.
- Ignoring accessible labels and roles.
- Creating huge selectors instead of scoping components.
- Assuming that Selenium and Playwright must use identical locator syntax.
- Prefer stable user-facing semantics when appropriate.
- Use get_by_role() for meaningful interactive elements.
- Use get_by_label() for form controls.
- Use get_by_text() for meaningful visible text.
- Use get_by_test_id() when the application exposes a stable automation contract.
- Use CSS for stable DOM/component structure.
- Use XPath when it provides a useful structural relationship.
- Scope locators using chaining and filtering.
- Prefer web-first assertions.
- Avoid arbitrary sleep() and fixed delays.
The important point is to choose the locator based on stability, readability and the application's UI contract.
Long Structural XPath
//div[2]/div[1]/form/div[3]
/button[1]
Highly dependent on DOM structure.
User-Facing Locator
page.get_by_role(
"button",
name="Submit"
).click()
Expresses the intended interaction directly.
Yes. Selenium can automate the same business workflows and can locate elements using ID, CSS, XPath, link text and other mechanisms.
The difference is primarily in the APIs, locator model, synchronization model and overall automation architecture.
| Concept | Playwright | Selenium |
|---|---|---|
| Semantic role API | get_by_role() | No equivalent locator API with the same abstraction. |
| Text API | get_by_text() | Commonly XPath or link-text mechanisms. |
| Label API | get_by_label() | Usually CSS/XPath and HTML relationships. |
| Test ID API | get_by_test_id() | CSS/XPath can target the data attribute. |
| Locator chaining | Built into locator model. | Can be achieved through WebElement lookups. |
| Synchronization | Actions/assertions integrate with Playwright's waiting model. | Explicit and implicit waits are commonly used. |
This example combines multiple locator strategies in a realistic customer order journey.
import re
from playwright.sync_api import expect
def test_customer_completes_order(page):
# Open application
page.goto(
"https://shop.example.test"
)
# Heading
expect(
page.get_by_role(
"heading",
name="Products"
)
).to_be_visible()
# Find product component
product = page.locator(
".product-card"
).filter(
has_text="Laptop Pro"
)
# Add product
product.get_by_role(
"button",
name="Add to Cart"
).click()
# Open cart
page.get_by_role(
"link",
name="Cart"
).click()
# Checkout
page.get_by_role(
"button",
name="Checkout"
).click()
# Customer information
page.get_by_label(
"Email"
).fill(
"john@example.com"
)
page.get_by_label(
"Shipping Address"
).fill(
"100 Main Street"
)
# Payment
page.get_by_role(
"radio",
name="Credit Card"
).check()
# Terms
page.get_by_role(
"checkbox",
name="Accept Terms"
).check()
# Place order
page.get_by_role(
"button",
name="Place Order"
).click()
# Verify result
expect(
page.get_by_role(
"heading",
name="Order Confirmed"
)
).to_be_visible()
expect(
page.get_by_text(
re.compile(
r"Order .* has been confirmed"
)
)
).to_be_visible()
| Element / Requirement | Playwright |
|---|---|
| Button | page.get_by_role("button", name="Login") |
| Link | page.get_by_role("link", name="Orders") |
| Heading | page.get_by_role("heading", name="Dashboard") |
| Input by label | page.get_by_label("Email") |
| Visible text | page.get_by_text("Order Confirmed") |
| Placeholder | page.get_by_placeholder("Search") |
| Alt text | page.get_by_alt_text("Company Logo") |
| Title | page.get_by_title("Delete User") |
| Test ID | page.get_by_test_id("submit-order") |
| CSS | page.locator(".product-card") |
| XPath | page.locator("//button[text()='Login']") |
| Filter | page.locator(".card").filter(has_text="Laptop") |
| First match | locator.first |
| Last match | locator.last |
| Specific match | locator.nth(2) |
Q1. What is a locator in Playwright?
A locator identifies an element and provides the mechanism for Playwright to perform actions and assertions against it.
Q2. What is get_by_role()?
It identifies an element through its accessible role and optionally its accessible name.
Q3. Does Playwright support XPath?
Yes. XPath can be used through locator().
Q4. What is get_by_label() used for?
It is useful for locating form controls through their associated labels.
Q5. What is get_by_test_id()?
It locates an element through a configured test identifier such as data-testid.
Q6. Can Selenium do the same UI automation?
Yes. Selenium can automate the same browser workflows. The frameworks differ in APIs, locator abstractions, synchronization behavior and other architecture-level features.
Q7. Which locator should I use?
Choose a stable, understandable locator that matches the application's UI contract. User-facing semantics are often useful, but CSS, test IDs and XPath also have valid use cases.
User interaction → Role / Label / Text
Automation contract → Test ID
Stable DOM structure → CSS
Complex structural relationship → XPath
Multiple similar components → Filter + Chain
Expected UI state → Web-first assertion
The practical difference is that Playwright provides a cohesive locator model with semantic locators, locator composition, filtering, actionability checks and web-first assertions.
The goal is not to find the most complicated selector. The goal is to find the most stable and maintainable locator.