skip to content

In Playwright, what does locator.all() return, and why can a loop over its result be flaky?

level: seniorimportance: should knowfreq 52%

answer

  1. One locator per element, right now
  2. No auto-waiting, unlike everything else
  3. Empty array means a silently passing test
  4. Entries are positional, not identity-bound
  5. Pin the count before you snapshot

basics

~20 s

It returns one locator per currently matching element and waits for nothing. The array length is a snapshot, so a list still loading yields too few entries, and each entry is positional, so removing elements mid-loop shifts what the rest point at.

solid answer

~40 s

`locator.all()` resolves to an array of locators, one per element matching **right now**. Unlike almost every other locator operation it does **not** auto-wait: if the issue board has not rendered yet you get an empty array, the loop body never runs, and the test passes having asserted nothing. Each returned locator is positional — effectively index 0, 1, 2 of the same set — and re-resolves on use, so archiving cards as you iterate makes index 1 refer to a different issue than when the array was built. The safe shape is to pin the set first with a retrying count assertion, then snapshot; or to avoid the loop entirely in favour of an assertion over the whole set. `allTextContents()`, `allInnerTexts()` and `count()` share the same one-shot, non-retrying behaviour.

code

typescript · 13 lines
typescript
const cards = page.getByTestId('issue-card');

// Flaky: snapshot may be empty or stale, and archiving shifts positions.
for (const card of await cards.all())
  await card.getByRole('button', { name: 'Archive' }).click();

// Safer: pin the set with a retrying assertion before snapshotting.
await expect(cards).toHaveCount(5);
const titles = await cards.allInnerTexts();

// Draining a shrinking set: re-resolve every round instead of snapshotting.
while (await cards.count() > 0)
  await cards.first().getByRole('button', { name: 'Archive' }).click();

go deeper

for a junior

Remember that all gives you one locator per element that matches at that moment, and that it is the rare locator call which does not wait for anything to appear.

for a middle

Explain the two failure modes separately: a snapshot taken too early yields too few entries, and positional entries drift when the action you run removes elements from the set.

for a senior

Show the safe pattern in a real suite: pin the set with a retrying count assertion, prefer set-wide assertions over loops, and drain a shrinking list by re-resolving rather than snapshotting.

for a principal

Treat silent-pass loops as a suite-wide risk, not a single test bug. A test that asserts nothing when the page is slow is worse than a failing one, and that argues for review rules on hand-rolled iteration.

## What `all()` is, mechanically `locator.all()` returns a `Promise<Locator[]>` — one locator per element currently matching. It is not magic: it reads the current match count and builds a locator for each index, so `all()` on a locator matching three issue cards gives you locators equivalent to index 0, index 1 and index 2 of that same set. Two consequences follow immediately, and they are the whole question: - the **array length is a snapshot** taken the moment you called it; - each **element in the array is positional**, and is re-resolved against the live DOM every time you use it. ## Why it does not wait Every other locator operation auto-waits: a click retries until the element is actionable, an assertion retries until it passes. `all()` deliberately does not. It returns whatever is present at that instant, and if nothing matches yet it hands back an empty array — no retry, no timeout, no error. A loop over an empty array simply does nothing, and the test passes while asserting absolutely nothing. That silent pass is the most dangerous property of the method. ## The flaky loop On an issue board that streams cards in, this is the classic broken shape: ```ts // Flaky: the board may still be loading, and it may reorder while we iterate. for (const card of await page.getByTestId('issue-card').all()) await card.getByRole('button', { name: 'Archive' }).click(); ``` Two independent failure modes live in those two lines: 1. **Too few, too early.** The board had two cards when `all()` ran and five a moment later, so three were never touched — and the test still passed. 2. **Drift while iterating.** Each archived card leaves the column, so index 1 now refers to a different issue than it did when the array was built. You archive the wrong ones, or run off the end of a shrunk list. The second one is subtle precisely because the returned locators *are* live. They re-resolve, which is normally the good property — here it means they follow the position, not the issue. ## Making a loop safe 1. **Pin the set first** with a retrying assertion, so the list is known to be complete before you snapshot it: `await expect(cards).toHaveCount(5)`. 2. **Do not mutate the set while iterating.** If the action removes elements, loop on a locator that re-resolves each round, or drive the same locator repeatedly until the count reaches zero. 3. **Prefer an assertion over a loop** where you can. Checking every card's text is a job for a retrying text assertion on the whole set, not a hand-rolled `for` loop. ## Reading a set back as text When you genuinely need the values rather than the elements, two one-shot readers exist: | Method | Reads | Includes hidden text | Whitespace | |---|---|---|---| | `allTextContents()` | `node.textContent` | yes | raw, as authored | | `allInnerTexts()` | `node.innerText` | no | collapsed as rendered | Both share `all()`'s snapshot nature: they read once and do not retry. So they are excellent for **diagnosis** — dumping what a locator actually matched when a count is wrong — and poor as the basis of an assertion, where a retrying matcher belongs instead. `count()` behaves the same way: a single query, no retry. ## When not to use it at all Reach for `all()` when you need per-element work that no single locator expresses — collecting attributes across a set, or performing a genuinely different action per row. Do not reach for it to "check the list is right"; that is what retrying assertions are for, and the loop version trades their auto-waiting for a race you have to manage by hand.

  • Why is a `for` loop over an empty `all()` result more dangerous than a failing locator?
    Because it fails open. No element matched, the body never ran, and nothing raised, so the test reports green while having checked nothing. A retrying assertion on the set would have failed loudly instead, which is why pinning the count first matters.
  • How do `allTextContents()` and `allInnerTexts()` differ?
    `allTextContents()` returns raw `textContent` for each match, including text in hidden elements and the whitespace as authored. `allInnerTexts()` returns `innerText`, which is the rendered text: hidden content is excluded and whitespace is collapsed. Both read once and never retry.
  • When is `all()` still the right tool?
    When you need genuinely per-element work that no single locator expresses — collecting an attribute from every row, or driving a different action per card. Even then, wait for the set to be complete first, and do not mutate it inside the loop.

It is a photograph of a moving queue. The photo tells you who was third at that instant, but walking back and tapping the third person later may reach someone else entirely.

saying these in an interview costs you the question

  • Believes all() auto-waits like other locator calls
  • Treats an empty result as proof the list is empty
  • Assumes the returned locators are bound to specific elements
  • Removes elements while iterating a snapshot
  • Uses allTextContents for assertions instead of a retrying matcher