Back to Playwright & UI Automation
Q2Question 2 of 7

Difference between Playwright's locator(), getByRole(), and getByText()

Key Concepts
  • getByRole finds by accessible role and name, mirroring how users and screen readers perceive controls, most reliable and accessibility-aligned
  • getByText finds by visible text, appropriate when the words are the right identifier, but risks breaking on copy changes or localization
  • locator() with CSS or XPath is the lowest-level fallback, most flexible but least readable and least stable
  • All three return a lazy Locator resolved with auto-waiting and strict mode at action time
  • The preference order is getByRole > getByLabel > getByText > getByTestId > generic locator, driven by reliability and accessibility
  • Choosing deliberately among the three and justifying the choice shows understanding of Playwright's design philosophy

Definition

getByRole locates by accessible role and name and is the most reliable and accessibility-aligned, getByText locates by visible text when the words are the real identifier, and the generic locator with CSS or XPath is the low-level, least readable fallback for when no semantic locator fits.
  • getByRole – Finds by accessibility role (button, heading, textbox) and accessible name. Most reliable, most accessibility-aligned, most readable.
  • getByText – Finds by visible text content. Appropriate when text is the real identifier. Risk: text changes for marketing or localization.
  • locator() – Accepts raw CSS or XPath. Most flexible but least reliable, readable, and maintainable. Fallback only.
  • All three – Return a lazy Locator resolved with auto-waiting and strict mode at action time.
  • Strict mode – Fails if the selector matches multiple elements, catching ambiguous locators early.

On the National Bank of India screen, getByRole is like asking for "the button labelled Transfer," which is how Rajesh himself thinks about it. getByText is like asking for "the thing that says Transfer completed," perfect for a confirmation message. The generic locator is like giving precise map coordinates, "third column, second box," which works but breaks if the furniture is rearranged and tells a reader nothing about what is there.

Understanding the Concepts

How does getByRole work?

getByRole couples the test to the element's semantic role together with its accessible name. This mirrors how a user interacts with the page and how a screen reader announces controls.

TYPESCRIPT
// getByRole: most reliable and readable
await page.getByRole('button', { name: 'Transfer' }).click();
await page.getByRole('heading', { name: 'Account Summary' }).isVisible();
await page.getByRole('link', { name: 'View Statement' }).click();
await page.getByRole('textbox', { name: 'Amount' }).fill('5000');
await page.getByRole('alert').textContent(); // Error banner

How does getByText work?

getByText locates by visible text, matching a substring, exact string, or regex. It is reliable when the text is genuinely the stable identifier.

TYPESCRIPT
// getByText: good for content assertions
await expect(page.getByText('Transfer Successful')).toBeVisible();
await expect(page.getByText('Insufficient Balance')).toBeVisible();
await page.getByText('View all transactions').click();

// Exact match to avoid partial collisions
await page.getByText('Transfer', { exact: true }).click();

How does the generic locator() work?

The generic locator accepts CSS or XPath and is the most flexible but least stable option.

TYPESCRIPT
// locator(): flexible but fragile
await page.locator('.transfer-form button.primary').click();
await page.locator('//div[@class="balance"]/span').textContent();
await page.locator('[data-testid="account-balance"]').textContent();

How do they compare across dimensions?

| Dimension | getByRole | getByText | locator() |
|---|---|---|---|
| Reliability | Best: survives refactors | Good: survives structure changes | Fragile: breaks on any markup change |
| Accessibility | Best: tests and improves a11y | Partial: reflects screen, not role | None: ignores semantics |
| Maintainability | Best: role and name rarely change | Risk: copy edits break it | Poor: generated values change |
| Readability | Best: states intent plainly | Good: readable when text is clear | Poor: long CSS paths say nothing |
| Use case | Interactive elements, landmarks | Content, messages, unique links | Last resort, scoped within a known container |

TYPESCRIPT
// Same element, three ways:

// getByRole: "click the button named Transfer" (best)
await page.getByRole('button', { name: 'Transfer' }).click();

// getByText: "click the thing that says Transfer" (good)
await page.getByText('Transfer').click();
// Risk: might match a heading, a link, or a label too

// locator: "click the element at this CSS path" (fragile)
await page.locator('.form-actions > button.btn-primary').click();

What do they share in common?

All three return a Playwright Locator, a lazy handle resolved with auto-waiting and strict mode at action time.

TYPESCRIPT
// All three auto-wait and enforce strict mode:

// Auto-waiting: waits until the element is actionable
await page.getByRole('button', { name: 'Transfer' }).click();
// No need for explicit waits or sleeps

// Strict mode: fails if multiple elements match
// This catches ambiguous locators early in development

When should you combine them?

Combine locators for precision, scoping a text match within a role.

TYPESCRIPT
// Combine: find the alert that contains a specific message
const error = page.getByRole('alert').filter({
  hasText: 'Insufficient Balance',
});
await expect(error).toBeVisible();

// Combine: find a button within a specific row
const row = page.getByRole('row', { name: 'ACC-001' });
await row.getByRole('button', { name: 'Transfer' }).click();

What are common mistakes?

The first mistake is defaulting to the generic locator by copying selectors from the browser inspector. Those CSS paths are almost always fragile.

The second mistake is using getByText for a button when getByRole is available. getByText does not distinguish a button from a plain span with the same words, so it can match the wrong element.

TYPESCRIPT
// WRONG: getByText for a button (might match a label too)
await page.getByText('Transfer').click();

// CORRECT: getByRole specifies the element type
await page.getByRole('button', { name: 'Transfer' }).click();

What are common interview follow-up questions?

  • "Can you use getByRole for a div?" Only if the div has an explicit ARIA role. Most semantic elements have implicit roles.
  • "What if the text changes for localization?" Use getByRole with an ARIA label that stays consistent, or use getByTestId for localized text.
  • "Does getByText match hidden text?" By default no, only visible text. Use the includeHidden option if needed.

Visual Overview

DIAGRAM
  Comparison: getByRole vs getByText vs locator()
  ────────────────────────────────────────────────

  getByRole('button', { name: 'Transfer' })
  ┌────────────────────────────────────┐
  │ Finds: <button>Transfer</button>   │
  │ Reliability: BEST                  │
  │ A11y: BEST (tests role + name)     │
  │ Readability: "click the button     │
  │   named Transfer"                  │
  └────────────────────────────────────┘

  getByText('Transfer')
  ┌────────────────────────────────────┐
  │ Finds: anything with "Transfer"    │
  │ Could be button, heading, or span  │
  │ Reliability: GOOD                  │
  │ A11y: PARTIAL (ignores role)       │
  │ Readability: "the thing that says  │
  │   Transfer"                        │
  └────────────────────────────────────┘

  locator('.btn-primary')
  ┌────────────────────────────────────┐
  │ Finds: element matching CSS        │
  │ Breaks if class is renamed         │
  │ Reliability: FRAGILE               │
  │ A11y: NONE                         │
  │ Readability: "the element at this  │
  │   CSS path" (says nothing)         │
  └────────────────────────────────────┘

Key Takeaway

All three methods return a Playwright Locator resolved with auto-waiting and strict mode, so the difference is how they find the element and the trade-offs that brings. getByRole finds by accessible role and name and is my default because it is the most reliable, most accessibility-aligned, and most readable. getByText finds by visible text and is good for content assertions, but risks breaking on copy changes or localization. The generic locator takes CSS or XPath and is the most flexible but least reliable and readable.

My preference order is getByRole for interactive elements and landmarks, getByLabel for form fields, getByText for content, getByTestId as a stable fallback, and generic locator only as a last resort. I often combine them for precision, scoping a text match within a role.

At the National Bank of India I standardized our page objects on this order. The original suite leaned on generic CSS locators and broke on nearly every release; I migrated to getByRole and getByLabel, used getByText only for genuine message assertions, and reserved generic locators behind test identifiers. Flakiness and maintenance both dropped.

Remember one sentence: getByRole is the most reliable, accessibility-aligned, and readable locator; getByText is good for content; and generic locator is the fragile last resort.

---

Share
Use ← → arrow keys to navigate