Work with multiple elements in Playwright

Iterate over locators, read list snapshots, and assert ordered collections without overlooking dynamic-list changes.

Best practices
Illustration of software development and testing

A locator can match a collection: page.locator('ul > li') represents all matching list items. Use nth(i) to target an item, toHaveCount() to assert the size, and toHaveText([...]) to check the complete ordered list of text values.

The fragments below assume test and expect are imported from @playwright/test and the relevant page is loaded.

Act on each item

test('check every todo', async ({ page }) => {
  // Navigate to your application's todo page before these steps.
  const items = page.locator('ul.todos > li');

  await expect(items).toHaveCount(3); // The fixture contains three items.
  for (let i = 0; i < await items.count(); i++) {
    await items.nth(i).getByRole('checkbox').check();
  }
});

nth() uses a zero-based index. first() and last() target the first and last matches. This loop assumes checking a box does not remove or reorder its row.

Read a collection

const texts = await page.locator('ul > li').allTextContents();
const locators = await page.locator('ul > li').all();

These methods return the current collection rather than waiting for a dynamic list to finish loading. Wait for a meaningful readiness condition first. A known fixture size can use toHaveCount(n); if text arrives later, assert that content separately.

Assert the collection

await expect(page.locator('ul > li')).toHaveCount(3);

// Exact ordered collection.
await expect(page.locator('ul > li')).toHaveText(['First', 'Second', 'Third']);

// An ordered subset, not an unordered match.
await expect(page.locator('ul > li')).toContainText(['First']);

The array form of toContainText() preserves order while allowing other matching elements between the expected entries.

Process items conditionally

const rows = page.locator('table tbody tr');
// Wait for the application's list-ready signal before calling all().
for (const row of await rows.all()) {
  if ((await row.locator('.status').innerText()) === 'Failed') {
    await row.getByRole('button', { name: 'Retry' }).click();
  }
}

This approach also assumes the relevant row positions remain stable during the loop. Locators returned by all() are not immutable copies of the original DOM nodes.

Avoid dynamic-list traps

  1. Reordering or removal can shift indexes. Re-reading count() does not prevent skipped rows. Prefer stable business identifiers. Alternatively, repeatedly process first() in a pending-items collection, impose an iteration limit, and verify that the pending count decreases.
  2. Indexes encode ordering assumptions. If order is incidental, use a meaningful locator or filter({ hasText: '...' }) instead of nth(2).
  3. Prefer locators to page.$$(). An ElementHandle refers to a particular DOM node and can become stale when it is replaced. Some handle actions do perform actionability checks; the distinction is not that handles never wait.

Frequently asked questions

What if locator.click() matches several elements?

A single-target action fails with a strict-mode violation. Narrow the locator to the intended element. Use nth() or first() only when that choice expresses the test's actual intent.

How do I process only the first N items?

For a stable list, use for (let i = 0; i < Math.min(3, await items.count()); i++). CSS such as ul > li:nth-child(-n+3) selects list items among the first three child positions; it depends on the container's structure.

Can I parallelize clicks to make the loop faster?

Avoid concurrent actions on one page when they share UI state or depend on ordering. A timeout is an upper bound, not a delay added to every operation, so lowering it does not automatically speed successful actions. Use a trace to identify actual waiting. Collection assertions can simplify the test without promising a particular performance gain.

References

Related reading

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting