What is the difference between getByRole and getByText in Playwright?
Written by Jatin Thacker, Automation Architect · Updated
getByRole finds an element by what it is (a button, link or heading) and its accessible name, while getByText finds any element containing the text, whatever kind of element it is. Use getByRole for things a user interacts with, and getByText for plain text such as a message or a note.This answer is one piece of Chapter 9: Locators I: What the User Sees.
- getByRole and the Accessible Name
- getByText, getByLabel, getByPlaceholder, getByTestId
- Strict Mode — One Locator, One Element
The difference comes down to one question: does the kind of element matter?
getByRole('link', { name: 'Catalog' }) | getByText('Catalog', { exact: true }) | |
|---|---|---|
| Matches | links named 'Catalog' | any element whose text is 'Catalog' |
| On the practice catalog | 1: the nav link | 2: the nav link and the <h1> |
| Best for | buttons, links, headings, inputs | messages, notes, paragraphs |
Clicking getByText('Catalog', { exact: true }) throws a strict mode violation, because it finds both the link and the heading. getByRole('link', ...) asks for the kind as well, so it finds only the link. That is why Playwright's docs recommend role locators for interactive elements and text locators for non-interactive ones, such as a div, span or p.
What they share: both match loosely by default. Without exact: true, the text or name matches as a case-insensitive substring. getByText('halcyon burr grinder') finds three elements on the catalog: two product headings ('… Grinder' and '… Grinder Mini') and an item in the recommended list. getByRole('heading', { name: 'Halcyon Burr Grinder' }) still finds two. Add exact: true (case-sensitive, whole string) and the heading locator finds one.
Neither is wrong. Pick the one that describes the element the way a user would.
import { test, expect } from '@playwright/test';
test('getByText vs getByRole on the catalog', async ({ page }) => {
await page.goto('/catalog.html?mock=static&seed=42&autologin=1');
// getByText does not care what kind of element it is: the nav link and the <h1> both say 'Catalog'
await expect(page.getByText('Catalog', { exact: true })).toHaveCount(2);
// getByRole asks for the kind as well, so only the link matches
await expect(page.getByRole('link', { name: 'Catalog' })).toHaveCount(1);
// Both match a case-insensitive substring by default
await expect(page.getByText('halcyon burr grinder')).toHaveCount(3);
await expect(page.getByRole('heading', { name: 'Halcyon Burr Grinder' })).toHaveCount(2);
await expect(page.getByRole('heading', { name: 'Halcyon Burr Grinder', exact: true })).toHaveCount(1);
// getByText is the right tool for text that is not a control
await expect(page.getByText('Order is seeded')).toBeVisible();
});$ npx playwright test role-vs-text.spec.js
ok 1 [chromium] › role-vs-text.spec.js:3:1 › getByText vs getByRole on the catalog (1.3s)
1 passed (2.3s)
# Clicking getByText('Catalog', { exact: true }) instead fails like this:
Error: locator.click: Error: strict mode violation: getByText('Catalog', { exact: true }) resolved to 2 elements:
1) <a aria-current="page" href="https://practice.learnplayforge.com/catalog.html?seed=42&autologin=1&mock=static">Catalog</a> aka getByRole('link', { name: 'Catalog' })
2) <h1>Catalog</h1> aka getByRole('heading', { name: 'Catalog' })<h1>Catalog</h1> and a link <a href="/catalog">Catalog</a>. What happens if you run await page.getByText('Catalog', { exact: true }).click()?Practised in the full course: the drill The button you can see.
See the chapterRelated questions
AI can write the test now. The job is knowing whether it checks anything. That's what the course teaches, one chapter at a time.
163 lessons · 226 drills · start free