How to fix Playwright strict mode errors from overbroad `getByText()` locators?
PlaywrightgetByText() matches any element in the document that contains the specified text, including headings, list items, table cells, and labels that happen to share the same wording. In a typical application, words like "Save", "Delete", "Edit", or "View" appear in navigation, tables, cards, and modals simultaneously — causing strict mode violations or silent clicks on the wrong element. The fix is scope narrowing, not abandoning getByText() entirely.
Common mistake
test('views user profile', async ({ page }) => {
await page.goto('https://app.example.com/users');
// "View" appears in table action column for every user row — strict mode violation
await page.getByText('View').click();
});
Also fragile at the assertion level:
// Passes even when "Saved" text appears in a notification unrelated to the submit action
await expect(page.getByText('Saved')).toBeVisible();
The fix
Scope getByText to a container, or switch to role-based locators that provide semantic context:
import { test, expect } from '@playwright/test';
test('views Ada Lovelace user profile', async ({ page }) => {
await page.goto('https://app.example.com/users');
// Scope action to the specific row
const adaRow = page.getByRole('row', { name: /Ada Lovelace/ });
await adaRow.getByRole('link', { name: 'View' }).click();
await expect(page.getByRole('heading', { name: 'Ada Lovelace' })).toBeVisible();
});
For shared UI text in a specific section:
test('saves billing settings', async ({ page }) => {
await page.goto('https://app.example.com/settings');
const billingCard = page.getByRole('region', { name: 'Billing' });
await billingCard.getByLabel('Company name').fill('Acme Corp');
// Scope "Save" to the billing card rather than page root
await billingCard.getByRole('button', { name: 'Save' }).click();
// Assert on the confirmation toast — scope it too
await expect(page.getByRole('alert')).toContainText('Billing settings saved');
});
For assertions, prefer toContainText with a meaningful substring and container scope:
// Instead of this — matches any visible element with 'Saved' text
await expect(page.getByText('Saved')).toBeVisible();
// Do this — scoped assertion on a specific UI region
await expect(page.getByRole('status')).toContainText('Settings saved successfully');
Why it works
getByText() traverses the entire accessibility tree of the page by default. When the text is common, multiple nodes match and Playwright's strict mode rejects the ambiguous locator. Scoping the locator with .locator() chaining or a parent container reduces the search domain to a specific region where the text should uniquely appear. Using getByRole('button', { name: 'Save' }) is more precise than getByText('Save') because it additionally constrains the match to elements with the button role, which is typically unique within a form section.
Tips
- If getByText('Save') resolves to more than one element, use getByRole('button', { name: 'Save' }) first — it's both more specific and more semantically meaningful.
- getByText accepts a regex: page.getByText(/^Save$/) requires an exact match and won't match "Save changes" or "Save and continue", reducing false positives.
- For text that changes based on locale or i18n, avoid getByText and use getByTestId with a stable component-level ID instead.
- Scoping strategies are the same as for strict mode violations from other locator types — see strict mode violation for locators for the full disambiguation approach.