← Back to libraryQuestion 168 of 273
🎭PlaywrightBeginner

Locators & the Web-First Locator Strategy

📌 Definition:

A Locator is a lazy, re-evaluated description of how to find an element. Unlike a Selenium WebElement, which is a handle to one snapshot of the DOM, a Playwright Locator is resolved fresh at the moment of each action, so it never goes stale.

📖 Detailed Explanation:

Selenium's findElement returns a WebElement bound to a specific DOM node; if the page re-renders, that reference becomes stale (StaleElementReferenceException). Playwright's page.locator(...) instead stores the query, not the node, and re-runs it on every action — so re-renders, virtual lists and React re-mounts don't break it. Playwright strongly recommends user-facing, semantic locators that mirror how users and assistive technology perceive the page: getByRole, getByLabel, getByPlaceholder, getByText and getByTestId. These are more resilient than brittle CSS/XPath tied to DOM structure. Locators also support chaining and filtering (.filter(), .getByRole(), .nth()), letting you scope within a component instead of writing long absolute paths.

🔑 Key Points:
  • Locators are lazy and re-queried per action — no stale element exceptions
  • Preferred order: getByRole > getByLabel/getByPlaceholder > getByText > getByTestId > CSS/XPath
  • Semantic locators double as accessibility checks and survive DOM refactors
  • Chain and .filter() to scope within a component instead of brittle absolute paths
  • Strictness: a locator that matches multiple elements throws unless you use .first()/.nth() deliberately
🌍 Real-World Example:

A product card list re-renders when a filter is applied. A Selenium WebElement captured before filtering throws StaleElementReferenceException after. The Playwright locator page.getByRole('listitem').filter({ hasText: 'Wireless Mouse' }).getByRole('button', { name: 'Add to cart' }) keeps working, because it re-resolves against the fresh DOM at click time.

📎 Code Example:
// Resilient, user-facing locators
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByLabel('Email').fill('qa@qapractice.com');
await page.getByPlaceholder('Search products').fill('mouse');

// Scope within a component with chaining + filter
const card = page.getByRole('listitem').filter({ hasText: 'Wireless Mouse' });
await card.getByRole('button', { name: 'Add to cart' }).click();

// Strictness: this throws if 3 buttons match — be explicit
await page.getByRole('button', { name: 'Delete' }).first().click();

// Last resort — structural locator
await page.locator('#legacy-widget .row:nth-child(2)').click();
🎯 Scenario-Based Interview Question:

Scenario: A suite uses XPath like //div[3]/table/tbody/tr[2]/td[4]//button everywhere. It breaks on almost every UI change and reviewers reject the tests as unmaintainable. How do you fix the locator strategy?