How to fix Playwright "strict mode violation" locator errors?
PlaywrightA strict mode violation in Playwright fires when a locator resolves to more than one element at action time. The error message — strict mode violation: locator('button') resolved to 3 elements — is immediate, not a timeout, and happens because Playwright's default locator API requires a unique match before performing any action. This is intentional: silently clicking the first of three matching buttons would be a worse outcome than a clear failure. The violation is most common with shared UI patterns like "Delete", "Edit", or "Save" that repeat across table rows or card grids.
Common mistake
test('saves billing settings', async ({ page }) => {
await page.goto('https://app.example.com/settings');
// "Save" button appears in three settings sections: profile, billing, notifications
await page.getByRole('button', { name: 'Save' }).click();
});
Generic text locators at page root are the most frequent trigger: page.getByText('Delete'), page.locator('.action-btn'), or page.locator('a[href="/edit"]').
The fix
Scope to a semantic container — the cleanest approach:
import { test, expect } from '@playwright/test';
test('saves billing settings', async ({ page }) => {
await page.goto('https://app.example.com/settings');
const billingSection = page.getByRole('region', { name: 'Billing' });
await billingSection.getByRole('button', { name: 'Save' }).click();
await expect(page.getByRole('alert')).toContainText('Billing settings saved');
});
Use a dialog container when acting inside a modal:
const deleteDialog = page.getByRole('dialog', { name: 'Delete workspace' });
await deleteDialog.getByRole('button', { name: 'Delete' }).click();
Add a data-testid when semantic scoping is not enough:
// In the component
<button data-testid="billing-save">Save</button>
// In the test
await page.getByTestId('billing-save').click();
Filter by additional attributes when modifying component HTML is not an option:
await page.locator('button', { hasText: 'Save' }).filter({ has: page.locator('[data-section="billing"]') }).click();
Why it works
Playwright's strict mode exists to prevent tests from silently acting on the wrong element when selectors are ambiguous. Scoping a locator to a parent container reduces the resolution domain from the full document to a subtree, which eliminates unintended matches sharing the same label. Accessible role + name locators are preferred because they align with how assistive technologies identify elements, making them more stable across HTML refactors than CSS or XPath selectors.
Tips
- Use await locator.count() in a debugging session to count how many elements a locator matches before narrowing it.
- Page-level strict mode violation errors that appear in many tests usually indicate a missing semantic landmark (role="region", aria-label) on a repeated component — fixing the component's accessibility attributes solves the test problem as a side effect.
- Never use .first() as a quick fix unless element order is a stable, tested invariant — index-based selections break when list order changes.
- This page covers the same error as strict mode violation for locators — both pages approach disambiguation from slightly different angles.