playwright-page-object
Guides

Lists

Iterate, filter, and look up items with ListPageObject — or fall back to raw Locator.

Two patterns for repeated children: ListPageObject for typed iteration and filtering, raw Locator for .nth() and .count().

With ListPageObject

Use when you need iteration, filtering, or item lookup with typed item controls.

import {
  ListPageObject,
  ListSelector,
  PageObject,
  RootPageObject,
  RootSelector,
  Selector,
} from "playwright-page-object";

class CartItemControl extends PageObject {
  @Selector("Name")
  accessor Name = new PageObject();

  @Selector("RemoveButton")
  accessor RemoveButton = new PageObject();
}

@RootSelector("CartPage")
class CartPage extends RootPageObject {
  @ListSelector("CartItem_")
  accessor Items = new ListPageObject(CartItemControl);
}

@ListSelector("CartItem_") matches data-testid values starting with CartItem_ — typically CartItem_1, CartItem_2, etc. The argument is used as a regex pattern; pass a RegExp for full control.

Item access patterns

// Numeric indexing (returns a CartItemControl bound to that item)
const first = cart.Items.items[0];
const last = cart.Items.items.at(-1);

// Async iteration
for await (const item of cart.Items.items) {
  await item.RemoveButton.$.click();
}

// Count assertions
await cart.Items.waitCount(3);
await cart.Items.count(); // returns Promise<number>

// Convenience accessors
const firstItem = cart.Items.first();
const lastItem = cart.Items.last();

Filtering (returns narrowed list)

filter... methods return a new ListPageObject narrowed to matching items. Chain .first(), .count(), .getAll(), or async iteration.

const filtered = cart.Items.filterByText("Apple");
await filtered.count();
const firstApple = filtered.first();

const withRemove = cart.Items.filterByHasTestId("RemoveButton");

const byOwnId = cart.Items.filterByItemTestId("CartItem_2");

Lookup (returns single item)

getItemBy... methods return a single item bound to the first match.

const apple = cart.Items.getItemByText("Apple");
const second = cart.Items.getItemByTestId("CartItem_2");
const removable = cart.Items.getItemByRole("button", { name: "Remove" });

await apple.RemoveButton.$.click();

filterByItemTestId vs filterByHasTestId

A common confusion — the two methods address different cases:

MethodMatches when
filterByItemTestId(id)the item row itself has data-testid={id}
filterByHasTestId(id)the item row contains a descendant with data-testid={id}

Example:

<div data-testid="CartItem_1">
  <span data-testid="Name">Apple</span>
  <button data-testid="RemoveButton">Remove</button>
</div>
  • filterByItemTestId("CartItem_1") → matches this row (own id).
  • filterByHasTestId("RemoveButton") → matches this row (descendant id).

With raw Locator

Use when you only need .nth(), .count(), or basic element operations:

@ListSelector("CartItem_")
accessor ItemRows!: Locator;

const count = await cart.ItemRows.count();
const second = cart.ItemRows.nth(1);

This is the pure-Playwright form — no item controls, no filtering helpers.

Tip: prefix row ids

Use a separator suffix (CartItem_1, CartItem_2) and avoid collisions with descendant ids like CartItemName. The regex ^CartItem_ will match row ids and not descendant ids.

See also

Last updated on

On this page