skip to content

A Playwright test that passed for months now fails with 'resolved to 2 elements' -- how do you diagnose it?

level: seniorimportance: should knowfreq 44%

answer

  1. The message is most of the diagnosis
  2. Compare the two generated candidate locators
  3. Instant failure, not a timeout
  4. Hidden duplicates count toward the total
  5. Ask which change added the second node

basics

~20 s

Read the candidate list in the error: it prints both matched nodes and a generated locator for each, which identifies the new duplicate. Then decide whether the page changed, or a hidden copy was always there and only now matches.

solid answer

~50 s

Start with the error itself, because it is close to a full diagnosis. `strict mode violation: <locator> resolved to 2 elements` is followed by an HTML preview of each candidate plus an `aka` locator Playwright generated that would uniquely match it, so the differing ancestors usually tell the two apart without opening a browser. Note also that the failure is instant rather than a timeout, which rules out the not-found and actionability families and means a longer timeout will change nothing. From there the usual causes on a rich page are a modal or drawer that renders its own copy of a shared control, a responsive layout where mobile and desktop variants both exist and CSS hides one, and a component that keeps a hidden template clone. Hidden nodes count, so "only one is visible" rules nothing out.

code

typescript · 10 lines
typescript
import { test } from '@playwright/test';

test('confirm the duplicate before touching the locator', async ({ page }) => {
  await page.goto('/issues/4821');
  const save = page.locator('button.save');

  console.log('matches:', await save.count());
  console.log('first: ', await save.nth(0).getAttribute('data-owner'));
  console.log('second:', await save.nth(1).getAttribute('data-owner'));
});

go deeper

for a junior

Read the whole error, not the first line. The candidates it lists, and the locator printed after each aka, tell you what the second matching element is.

for a middle

Explain why this failure is instant rather than a timeout, and why a hidden node still counts, so visibility in a screenshot proves nothing.

for a senior

Work outward from the candidate list to the change that introduced the duplicate, and judge whether the duplicate is itself a product defect before touching the test.

for a principal

Decide where the fix belongs when duplicates recur -- a shared shell component rendering controls twice is one repair, not thirty test edits -- and make that the team's default response.

## Read the error before you open a browser A strict mode violation is closer to a report than a failure. The message carries the locator as Playwright parsed it, the number of matches, and one line per candidate: an HTML preview of the node plus, after `aka`, a locator Playwright generated that would match only that candidate. On an issue tracker's issue detail page that usually reads as "one of these is inside `#composer` and the other is inside `#comment-14`", which is the diagnosis. So the first step is not reproduction. It is reading all of the message rather than its first line. ## Why it failed fast, and what that tells you The violation is raised as soon as the locator resolves; it is not retried until the timeout. That is a useful signal in itself: | Symptom | Likely cause | |---|---| | Fails in milliseconds with a candidate list | too many matches -- a strictness problem | | Hangs, then times out with a call log | zero matches, or an actionability check never passed | | Fails only on one project or viewport | a layout that renders a second, hidden variant | It also means a longer timeout, a retry, or `test.slow()` will not change the outcome, and reaching for one of those is the classic wrong move. ## The usual causes on a rich page A test that was stable for months and now sees two matches almost always means the page changed, not the test. The recurring shapes: - **A modal, drawer or popover** that renders its own copy of a shared control while the underlying page keeps its own. - **A responsive layout** where the mobile and desktop variants both exist in the DOM and CSS hides one. Hidden nodes still count, so "I only see one on screen" rules nothing out. - **A duplicated component** after a refactor -- a toolbar extracted and then rendered by both the page shell and the page body. - **A template or measurement clone** kept by a component library outside the visible tree. - **Copy changes**, when a locator that keyed on text now also matches a new label elsewhere. ## A diagnosis order that works 1. Read the candidate list and compare the two `aka` locators. Their differing ancestors name the containers the duplicates live in. 2. Confirm the ambiguity is real and current with `locator.count()`, which works happily on many and returns the number rather than throwing. 3. Inspect each candidate individually through `nth(0)` and `nth(1)` -- read an attribute, take a screenshot -- to establish which one the test meant. 4. Look at the DOM snapshot rather than a screenshot, since one of the two may be hidden. 5. Find the change that introduced the duplicate, and decide whether it is intended. ## Fix the cause, not the symptom Once you know what the second match is, the decision is straightforward. If the duplicate is a product defect -- a control rendered twice, a stale modal left in the tree -- the test found a real bug and the fix belongs in the application. If the duplicate is legitimate, the test needs to say which instance it means, either by describing the element unambiguously or, when the position genuinely is the requirement, by opting out with `first()`, `last()` or `nth(i)` and saying why. ## What not to conclude - Not "the test is flaky". A strict violation is deterministic for a given page state. - Not "the element is missing". That failure looks completely different and takes the full timeout. - Not "only the visible one counts". Visibility is checked after resolution, and resolution already failed.

  • A screenshot of the failure shows only one Save button. Does that rule out a strict mode violation?
    No. Strictness counts DOM matches, so a hidden duplicate -- a collapsed drawer, an inactive responsive layout, a template clone -- still makes two. Inspect the DOM rather than the image; the second candidate's generated locator already names the container it lives in.
  • How would you stop this class of failure from recurring across the suite?
    Treat each violation as a finding about the page, not a locator annoyance. Name the duplicate, and when a shell component has started rendering a second copy of shared controls, fix it once in that component instead of patching every test that trips over it.

saying these in an interview costs you the question

  • Calling the test flaky and adding a retry
  • Assuming an element must be visible to count as a match
  • Raising the timeout to make a strict violation go away
  • Reading only the first line and skipping the candidate list
  • Concluding the test broke when the page changed instead