Work with multiple elements in Playwright
Iterate over locators, read list snapshots, and assert ordered collections without overlooking dynamic-list changes.
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
- Reordering or removal can shift indexes. Re-reading
count()does not prevent skipped rows. Prefer stable business identifiers. Alternatively, repeatedly processfirst()in a pending-items collection, impose an iteration limit, and verify that the pending count decreases. - Indexes encode ordering assumptions. If order is incidental, use a meaningful locator or
filter({ hasText: '...' })instead ofnth(2). - 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
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan