skip to content

Why does Playwright's locator.click() throw on two matches while locator.count() returns 2 happily?

level: middleimportance: must knowfreq 64%

answer

  1. Ask what the call would have to return
  2. Ambiguous result means strict; set-shaped result does not
  3. Counting the set is a valid answer
  4. count queries once and never waits
  5. Zero matches returns zero, not an error

basics

~20 s

Strictness applies only to calls that need one target element. A click must choose a single element, so two matches is ambiguous and throws. Counting is defined over the whole matched set, so two is simply the answer it returns.

solid answer

~40 s

Playwright splits locator calls into single-element and multi-element operations. Anything that acts on or reads one element -- clicking, filling, `textContent()`, and most `expect(locator)` matchers -- resolves the locator and throws `strict mode violation` when more than one node matched, because there is no defensible way to pick. `locator.count()` is a multi-element operation: the size of the matched set *is* its result, so it returns 2, and it returns 0 rather than throwing when nothing matches. That also means `count()` does not auto-wait -- it queries once and reports what exists at that instant, which is why `expect(locator).toHaveCount(2)` is preferred whenever you want the number to settle first. The same exemption covers the array forms of the text and class matchers, which compare one expected value per matched element.

code

typescript · 11 lines
typescript
import { test, expect } from '@playwright/test';

test('counting works on many, clicking needs one', async ({ page }) => {
  await page.goto('/board/backlog');
  const cards = page.locator('[data-card]');

  await expect(cards).toHaveCount(12);
  const total = await cards.count();
  await cards.nth(total - 1).click();
  // await cards.click(); // strict mode violation: resolved to 12 elements
});

go deeper

for a junior

Remember the two groups: acting on or reading one element needs a single match, while counting the matches works on any number, including none.

for a middle

Explain the split from first principles -- a call is strict exactly when its result would be ambiguous with several matches -- and note that counting queries once without waiting.

for a senior

Use the distinction deliberately in tests: count as a probe or to derive an index, the count matcher when the number itself is under test, and never count as a readiness gate.

for a principal

Set the team convention on which of the two is allowed in assertions, so nobody ships a race that a polling matcher would have absorbed.

## Two families of locator call Every method on a Playwright `Locator` falls into one of two groups, and which group it belongs to decides whether strictness applies: - **Single-element operations** need one target. Clicking, filling, pressing a key, reading `textContent()` or `getAttribute()`, and nearly every `expect(locator)` matcher are in this group. They resolve the locator and throw a strict mode violation the moment more than one node matches. - **Multi-element operations** are defined over the whole matched set. `locator.count()` is the plainest example: the size of the set *is* the answer, so a set of five is a result, not an error. ## Why the split falls where it does Ask what the call would have to return. `locator.textContent()` on five matched nodes has no defensible answer -- picking the first is a guess, joining them is nonsense, and returning an array would change the method's type. `locator.count()` on five nodes has exactly one answer: `5`. Strictness is applied wherever ambiguity would otherwise be resolved by an arbitrary rule. The same reasoning exempts the matchers that compare per-element values: `toHaveCount()` is about the size of the set, and the array forms of the text and class matchers compare one expected entry per matched element, so they are meaningful on many. | Call | 0 matches | 1 match | 5 matches | |---|---|---|---| | `locator.click()` | waits, then times out | clicks | strict mode violation | | `locator.textContent()` | waits, then times out | returns the text | strict mode violation | | `locator.count()` | returns `0` | returns `1` | returns `5` | | `expect(locator).toHaveCount(5)` | retries, then fails | retries, then fails | passes | ## count() does not wait The second half of the difference matters as much as strictness. A single-element call auto-waits: it re-resolves the locator until something matches. `locator.count()` queries once and reports what is in the DOM at that instant, and returns `0` when nothing matched rather than throwing. That has two consequences on a board that renders asynchronously: 1. `expect(await cards.count()).toBe(12)` can fail simply because the assertion ran before the last card rendered. There is no retry inside `count()` to absorb that. 2. `expect(cards).toHaveCount(12)` polls until the number settles or the assertion times out, which is why Playwright's own API documentation steers you there for anything you want to assert. Use `count()` when you need the number as a value in test logic -- for example to derive an index -- and the count matcher when the number itself is the thing under test. ## What this looks like on an issue board Suppose the backlog column renders one card per issue and every card carries an "Assign" button. - `page.getByRole('button', { name: 'Assign' }).click()` throws, listing every card's button as a candidate. - `page.getByRole('button', { name: 'Assign' }).count()` returns the number of cards, quietly. Both calls are built from the same locator. Nothing about the locator changed; only the arity of the operation did. ## Getting from many to one When you do need to act, the locator has to end up describing a single element. Either make the description narrower, or opt out of strictness explicitly with `first()`, `last()` or `nth(i)`, which append a positional step and therefore always resolve to one node -- at the cost of no longer noticing that the set grew. Whichever you pick, `count()` remains a useful probe while you are working out what the extra matches actually are.

  • Why does Playwright's API documentation steer you to `toHaveCount()` instead of asserting on `count()`?
    `count()` queries once and returns immediately, so it captures whatever happens to be rendered at that instant. `toHaveCount()` retries until the number matches or the assertion times out, which absorbs the render your test would otherwise race.
  • What does `locator.count()` return when the locator matches nothing at all?
    Zero. An empty set is a valid answer for a multi-element call, so nothing is thrown and nothing is waited for. That makes `count()` a poor readiness check, because 'zero' and 'not rendered yet' are indistinguishable.

saying these in an interview costs you the question

  • Saying every locator method throws when several elements match
  • Believing count waits for the expected number to appear
  • Expecting count to raise a strict mode violation on many matches
  • Thinking only actions are strict and getters are exempt
  • Treating a count of zero as proof the elements never render