How to fix Playwright "strict mode violation" for locators?
PlaywrightA Playwright strict mode violation means the locator you used resolved to more than one element, but the operation requires exactly one. The error message reads strict mode violation: locator('...') resolved to N elements and is thrown immediately — no timeout involved. It most commonly happens with generic CSS selectors or text locators that accidentally match navigation links, footers, and headings that share the same label.
Common mistake
test('deletes project', async ({ page }) => {
await page.goto('https://app.example.com/projects');
// Page has multiple "Delete" buttons — one per row
await page.getByRole('button', { name: 'Delete' }).click();
});
Using page.getByText('Save') or page.locator('.btn-danger') at the page root is another common trigger when the same text or class appears multiple times.
The fix
Option 1 — Scope to a semantic container (preferred when structure allows it):
import { test, expect } from '@playwright/test';
test('deletes Acme project', async ({ page }) => {
await page.goto('https://app.example.com/projects');
// Scope to the specific row first
const projectRow = page.getByRole('row', { name: /Acme Corp/ });
await projectRow.getByRole('button', { name: 'Delete' }).click();
// Confirm deletion dialog
await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByRole('row', { name: /Acme Corp/ })).not.toBeVisible();
});
You can also scope directly to a dialog or modal when the action lives there:
const deleteDialog = page.getByRole('dialog', { name: 'Delete workspace' });
await deleteDialog.getByRole('button', { name: 'Delete' }).click();
Option 2 — Use a more specific locator when semantic scoping is not available:
// Use a data-testid tied to the specific action
await page.getByTestId('project-delete-42').click();
Option 3 — Filter by additional attributes when modifying component HTML is not an option:
// Scope using .filter() to narrow the selection
await page.locator('button', { hasText: 'Save' })
.filter({ has: page.locator('[data-section="billing"]') })
.click();
Option 4 — Explicitly select by index only when order is stable and intentional:
// .first() or .nth() signals intentionality but is fragile if list order changes
await page.locator('.project-row').first().getByRole('button', { name: 'Delete' }).click();
Why it works
Playwright's locator API enforces a "one element, one action" contract to prevent tests from silently acting on the wrong element. This is the strict mode violation check. When you scope a locator to a parent container, the resolution happens within that subtree rather than the full document, which eliminates unintended matches. Using accessible roles with name constraints leverages the accessibility tree rather than DOM structure, making locators more stable across HTML changes.
Tips
- Use await locator.count() in a debugging session to count how many elements a locator matches before narrowing it.
- Run await locator.all() to inspect every element matched by a locator to see what you're up against.
- 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.
- Prefer scoped role locators over .nth() — index-based selection will break if the list is reordered or paginated differently.
- If the same strict mode violation appears across many tests within a single component, add a data-testid at the component level to give tests a stable anchor.