Playwright locators are a lie. They don’t find elements; they assert that an element matching a selector exists and is visible, then return a handle to it.
Here’s a real test running:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://www.example.com")
# This locator doesn't "find" anything yet.
# It's just a recipe for finding something later.
h1_locator = page.locator("h1")
# This is where the actual assertion and retrieval happens.
# If no h1 is visible, this will throw an error.
h1_text = h1_locator.text_content()
print(f"Found H1: {h1_text}")
browser.close()
When you write page.locator("h1"), Playwright doesn’t immediately scan the DOM. Instead, it creates a Locator object. This object holds the selector and a reference to the Page it belongs to. The magic, and the reliability, happens when you interact with that Locator object. Methods like click(), fill(), text_content(), is_visible(), etc., all trigger Playwright’s retry mechanism. It will repeatedly attempt to locate the element using the provided selector, waiting for it to meet certain criteria (like being visible and actionable) up to a default timeout (30 seconds). This is why Playwright tests are generally more robust than traditional Selenium tests, which often require explicit waits.
The problem Playwright locators solve is flaky tests. In the past, tests would break because an element wasn’t ready when the test script tried to interact with it. Developers would then sprinkle time.sleep() calls or complex WebDriverWait conditions, which are brittle and hard to maintain. Playwright’s locators abstract this away. They are designed to inherently wait for elements to be in a usable state before performing an action. This means you can write simpler, more readable tests that are less prone to timing issues.
Let’s break down how you control this. The core of locator reliability comes from the selector itself and how you refine it.
-
CSS Selectors: The most common.
page.locator("div.container > button#submit"). These are fast and familiar. -
XPath Selectors: For more complex DOM traversals.
page.locator('//div[@class="user-list"]/span[contains(text(), "Admin")]'). Use these when CSS selectors become unwieldy or when you need to navigate based on relationships that CSS can’t easily express. -
Text Content: Directly match visible text.
page.locator("text=Sign In"). This is great for buttons, links, and headings. Playwright also supports partial text matching:page.locator("text=Sign")orpage.locator('text=/Sign In/i')for case-insensitive regex. -
Alt Text and Images:
page.locator('[alt="Company Logo"]')orpage.locator('img[src="logo.png"]'). Essential for testing images and accessibility. -
Role and Label: Leveraging ARIA attributes.
page.locator('role=button', has_text='Submit'). This is powerful for accessibility and semantic testing. You can combine roles with other attributes or text. -
Chaining Locators: Nesting locators to narrow down the search.
page.locator("div.sidebar").locator("a", has_text="Settings"). This is crucial for finding elements within a specific context, like finding a "Delete" button only within a particular row of a table. The parent locator (div.sidebar) is evaluated first, and then the child locator (a, has_text="Settings" ) is searched only within the elements matched by the parent. -
hasandhas_not: Filtering based on the presence or absence of child elements.page.locator("div.card", has=page.locator("button.favorite")). This findsdiv.cardelements that contain abutton.favorite. Conversely,has_notfinds those that do not contain it.
The most surprising true thing about Playwright locators is that they are lazy by default and always retry until the default timeout unless told otherwise. You can override this behavior with timeout=0 for immediate failure or timeout=5000 for a custom wait, but the default 30-second retry is the engine of reliability. This means you don’t usually need explicit waits for elements to appear; Playwright handles it for you.
The next concept you’ll run into is understanding the different states locators can be in and how to assert them beyond just interaction, which leads to exploring methods like wait_for().