Skip to content
Playwright · error message

Playwright: strict mode violation: locator resolved to N elements

Permanent - do not retryLocators & waiting

Last verified

The exact message
Error: locator.click: Error: strict mode violation: getByText('Sign in') resolved to 2 elements: 1) <a href="/login">Sign in</a> aka getByRole('link', { name: 'Sign in' }) 2) <button type="submit">Sign in</button> aka getByRole('button', { name: 'Sign in' })

What it means

Locators in Playwright are strict: any action that targets a single element (click, fill, check, hover, textContent and so on) requires the locator to match exactly one element. When it matches two or more, Playwright refuses to guess and throws immediately rather than clicking the first match. The number in the message is how many elements matched at the moment of the action.

The error is helpful. Below the headline Playwright lists every match, and for each one prints an aka line with a more specific locator that would uniquely identify it. Copying the right aka suggestion is often the whole fix.

What is not wrong: the page and timing are fine; the element you want is present. The problem is purely that the locator is ambiguous. Methods that are meant to work on many elements, such as locator.count() and locator.all(), do not trigger strictness.

What it looks like

// Ambiguous: two elements say "Sign in"
// await page.getByText('Sign in').click();

// Unique
await page.getByRole('button', { name: 'Sign in', exact: true }).click();

// Scoped to one row of a table
await page.getByRole('row')
  .filter({ hasText: 'Invoice 42' })
  .getByRole('button', { name: 'Edit' })
  .click();

Why it happens

  • A text or role locator matches the same label used in several places, such as a header link and a form button both reading Sign in.
  • The page renders duplicate markup for desktop and mobile layouts, with one copy hidden.
  • A list or table repeats identical buttons (Edit, Delete) on every row.
  • A substring match: getByText('Save') also matches 'Save draft' unless exact matching is used.

How to fix it

  1. Read the numbered list in the error and copy the aka locator of the element you meant.
  2. Narrow by role and accessible name: getByRole('button', { name: 'Sign in', exact: true }).
  3. Scope the search to a container: page.getByRole('form').getByRole('button', { name: 'Sign in' }).
  4. For repeated rows, filter first: page.getByRole('row').filter({ hasText: 'Invoice 42' }).getByRole('button', { name: 'Edit' }).
  5. If duplicates differ only by visibility, add .filter({ visible: true }) or its shortcut .visible() so only the rendered copy matches.
  6. Use .first(), .last() or .nth(i) only when position is genuinely what you mean.

How to stop it recurring

Write locators the way a user would describe the element: its role, its name and the region it sits in. Chain from a container (a dialog, form, row or main) instead of searching the whole page. Treat .first() as a smell: it hides strictness errors that would otherwise tell you the page changed. Adding stable data-testid attributes to repeated components removes most ambiguity for pages you control.

Official documentation: playwright.dev/docs/locators#strictness. See all Playwright error messages.

Related errors

Definitions

Quick answers

What does "strict mode violation: locator resolved to N elements" mean in Playwright?

Locators in Playwright are strict: any action that targets a single element (click, fill, check, hover, textContent and so on) requires the locator to match exactly one element. When it matches two or more, Playwright refuses to guess and throws immediately rather than clicking the first match. The number in the message is how many elements matched at the moment of the action. The error is helpful.

How do I fix "strict mode violation: locator resolved to N elements"?

1. Read the numbered list in the error and copy the aka locator of the element you meant. 2. Narrow by role and accessible name: getByRole('button', { name: 'Sign in', exact: true }). 3. Scope the search to a container: page.getByRole('form').getByRole('button', { name: 'Sign in' }). 4. For repeated rows, filter first: page.getByRole('row').filter({ hasText: 'Invoice 42' }).getByRole('button', { name: 'Edit' }). 5. If duplicates differ only by visibility, add .filter({ visible: true }) or its shortcut .visible() so only the rendered copy matches. 6. Use .first(), .last() or .nth(i) only when…

Run browser automation you can watch and take over

Autonoly runs each job in a real cloud Chromium browser driven by Playwright. Timeouts and detached elements still happen; the difference is that you can watch the run live, see the step that failed, and take control of the browser to finish it.

Also see Live browser control.