17. User-First Locators & Resilient Selection Strategies

Priority Recommended Locator Hierarchy

Playwright recommends prioritizing locators in this order:

1. page.getByRole(role, { name }): The gold standard. Tests accessibility and matches screen-reader semantics.
2. page.getByLabel('Username'): Best for form inputs with linked <label> elements.
3. page.getByPlaceholder('Enter email'): Best when no visible label exists.
4. page.getByText('Submit Order'): Locates non-interactive text elements.
5. page.getByTestId('checkout-btn'): Dedicated QA test id (data-testid).
6. page.locator('css-or-xpath'): Fallback when accessibility locators are unfeasible.

---

Strict Mode Violation

Playwright enforces strict mode on all locators by default! If your locator matches more than one element, Playwright will throw an error:
// ❌ If there are 3 buttons matching "Delete", this throws StrictModeError:
await page.getByRole("button", { name: "Delete" }).click();

// ✅ Solution 1: Filter to pinpoint specific row
await page.locator("tr", { hasText: "Order #402" })
.getByRole("button", { name: "Delete" })
.click();

// ✅ Solution 2: Filter by child locator
await page.getByRole("listitem")
.filter({ has: page.getByRole("heading", { name: "MacBook Pro" }) })
.getByRole("button", { name: "Buy" })
.click();

// ✅ Solution 3: Exact index (first / last / nth)
await page.getByRole("button", { name: "Delete" }).first().click();

---

Locator Chaining

Locators can be chained seamlessly:
const nav = page.getByRole("navigation");
const loginLink = nav.getByRole("link", { name: "Sign in" });
await loginLink.click();

Task

  • Implement buildRowDeleteLocator:
  • 1. Take rowText parameter.
  • 2. Return a selector matching the table row with rowText and targeting its delete button.
💡Common doubts
Did you like the problem?
Lesson 17 of 36
TypeScript
Loading code editor…
Next