Skip to content

Lists

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

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.

// 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();

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");

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();

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).

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.

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.