Skip to content

Chapter 3 · Locators and Strict Mode ​

Chapter goals: Master the "re-query on every action" design philosophy of Locators and all built-in locator types, understand why strict mode saves your tests, and learn to use filter chains for precise list-item targeting.

3.1 Locator vs ElementHandle: One Re-render Away from Disaster ​

A Locator is the core of Playwright's auto-wait and retry capabilities: it does not represent a specific DOM node, but rather a query for "how to find the node". Every time you perform an operation on a Locator, it re-runs the lookup against the current DOM:

python
locator = page.get_by_role("button", name="Sign in")
locator.hover()   # lookup once, at this moment
locator.click()   # lookup again — using the freshest DOM

If the page re-renders between the two operations (a daily occurrence with modern frontend frameworks), the Locator automatically points to the new element. The older ElementHandle "freezes" the node at creation time — after a re-render it becomes an orphan and any operation throws Element is not attached to the DOM:

python
# ❌ Anti-pattern: handle is a snapshot, dead after re-render
handle = page.query_selector("#submit")

# ✅ Correct: locator is a live query
page.locator("#submit").click()

Remember one sentence

Your test code should almost never contain query_selector or $. Seeing an ElementHandle usually means you've fallen into an anti-pattern.

3.2 Built-in Locator Family ​

The official recommendation is to prefer user-facing attribute locators — they are more testable and more semantic:

python
# By ARIA role + accessible name (preferred)
page.get_by_role("button", name="Sign in").click()
page.get_by_role("checkbox", name="Subscribe").check()

# By label (form controls)
page.get_by_label("Password").fill("secret")

# By placeholder
page.get_by_placeholder("name@example.com").fill("me@test.com")

# By text (for non-interactive div/span; supports regex and exact=True)
expect(page.get_by_text(re.compile("welcome, john", re.IGNORECASE))).to_be_visible()

# By image alt text, by title attribute
page.get_by_alt_text("playwright logo").click()

# By test id (default: data-testid attribute)
page.get_by_test_id("directions").click()

Priority recommendation: get_by_role → form fields use get_by_label → non-interactive text uses get_by_text → if the team agrees on test ids, use get_by_test_id.

Test ids are the most structure-resilient anchor — copy changes and DOM restructuring don't break them. They read the data-testid attribute by default; you can change the attribute name globally:

python
playwright.selectors.set_test_id_attribute("data-pw")

CSS/XPath are still available (page.locator("css=..."), auto-detects //button), but long chains like #tsf > div:nth-child(2) > ... are specifically called out as bad practice by the docs — they shatter as soon as the DOM structure changes.

3.3 Strict Mode: Killing Ambiguity Before It Runs ​

Traditional frameworks silently click the first match when multiple elements match — errors get swallowed. Playwright enables strict mode by default: any operation/assertion that resolves to multiple elements fails immediately:

python
# With two <button> elements on the page:
page.locator("button").click()
# TimeoutError: strict mode violation: locator resolved to 2 elements

This is protection, not annoyance — mismatched "the button I thought I was clicking" and "the button that actually got clicked" is a classic source of flaky tests. Actively narrow the scope instead of leaving it to chance:

python
page.locator("button", has_text="Save").first.click()      # Explicitly take the first
page.get_by_role("listitem").nth(1)                        # Explicitly take the nth (use with caution)
page.locator("button").filter(visible=True).click()        # Match only visible elements

3.4 Filter Chains: The Right Way to Target List Items ​

To click the buy button in the second product row, the correct approach is to filter the "row" first, then drill down:

python
# Text filter: find the list item containing "Product 2", then find the button inside it
page.get_by_role("listitem").filter(
    has_text="Product 2"
).get_by_role("button", name="Add to cart").click()

# Sub-element condition filter: the row must contain a matching heading
page.get_by_role("listitem").filter(
    has=page.get_by_role("heading", name="Product 2")
).get_by_role("button", name="Add to cart").click()

# Negative filter: exclude out-of-stock items
expect(page.get_by_role("listitem").filter(has_not_text="Out of stock")).to_have_count(5)

Note: inner locators in filter(has=...) match downward from the filtered element — don't write selectors that start from the document root. Multiple filters can be chained to narrow further.

Shadow DOM needs no special treatment either: all locators (except XPath) pierce shadow roots by default (closed mode excluded).

3.5 Chapter Summary ​

  • Locator = live query, re-resolved on every action; ElementHandle = dead snapshot, broken after re-render;
  • Locator priority: role → label/text → placeholder/alt/title → test id → CSS/XPath fallback;
  • Strict mode makes "matched multiple elements" fail immediately, forcing unambiguous locators;
  • For lists, use filter(has_text= / has= / has_not_text=) chains to drill down;
  • Test ids are most resilient; customize with selectors.set_test_id_attribute().

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. After a page re-renders, which approach continues to work correctly?

2. A locator matches 3 elements and you call click() — what happens?

3. To click the "Add to cart" button inside the list item containing "Product 2", the correct写法 is?

4. Which statement about get_by_test_id is incorrect?

🛠️ Hands-On Exercises ​

  1. Open any site with a search box; locate and type into it using both get_by_placeholder and get_by_role("searchbox").
  2. Build a local HTML page with two identically-named buttons; reproduce the strict mode violation, then fix it with .first and filter(visible=True) respectively.
  3. Write a page with 5 <li> products (2 marked "Out of stock"); assert that the count of in-stock items is 3.

Proceed to Chapter 4: Auto-Wait and Actionability.