Skip to content

ListPageObject

ListPageObject is the base for accessors that resolve to multiple matching elements. Pair with @ListSelector(...) or @ListRootSelector(...).

import {
ListPageObject,
ListSelector,
PageObject,
RootPageObject,
RootSelector,
Selector,
} from "playwright-page-object";
class CartItemControl extends PageObject {
@Selector("Name")
accessor Name = new PageObject();
}
@RootSelector("CartPage")
class CartPage extends RootPageObject {
@ListSelector("CartItem_")
accessor Items = new ListPageObject(CartItemControl);
}
new ListPageObject(itemType?)

itemType is either:

  • a PageObject subclass constructor — items are new ItemType() then bound via cloneWithContext,
  • a PageObject instance — items are cloned via cloneWithContext,
  • undefined — items are typed as PageObject (no custom helpers).

Invalid itemType throws at construction time:

new ListPageObject("not a class"); // throws
new ListPageObject(42); // throws
  • .$ — raw Locator matching all items (use .count(), .nth(i) etc.)
  • .page — getter for the underlying Page.
  • .expect() — assertion API on the multi-element locator.
  • waitCount(n) — wait until the list has n items.

.items is a lazy proxy that supports numeric indexing, .at(), and async iteration:

list.items[0]; // first item, typed as itemType
list.items.at(-1); // last item
for await (const item of list.items) { /* ... */ }

Each access constructs a fresh item bound to that index.

Returns Promise<number> of matching elements.

const n = await list.count();

Return a single item bound to the first / last match.

const head = list.first();
const tail = list.last();

Returns an array of all items (resolved at call time).

const all = await list.getAll();
for (const item of all) { /* ... */ }

Filter methods (return narrowed ListPageObject)

Section titled “Filter methods (return narrowed ListPageObject)”

Each filter returns a new ListPageObject of the same item type, narrowed to matching items. Chain .first(), .count(), .getAll(), or async iteration.

MethodMatches when
filterByText(text)item contains text (string | RegExp)
filterByItemTestId(id)the item row itself has data-testid={id} (string | RegExp)
filterByHasTestId(id)the item row contains a descendant with data-testid={id}
filterByRole(...)the item row contains an element matching the role
const apples = list.filterByText("Apple");
await apples.count();
const named = list.filterByItemTestId("CartItem_2");
const withRemoveButton = list.filterByHasTestId("RemoveButton");

Each returns a single item bound to the first match.

MethodReturns
getItemByText(text)first item containing text
getItemByTestId(id)first item with data-testid={id} (string | RegExp)
getItemByRole(...)first item containing the role
const apple = list.getItemByText("Apple");
const second = list.getItemByTestId("CartItem_2");
const byPattern = list.getItemByTestId(/^CartItem_/);
const removable = list.getItemByRole("button", { name: "Remove" });
<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 data-testid).
  • filterByHasTestId("RemoveButton") → matches this row (descendant data-testid).

The two methods address different cases — pick by which level the id lives on.

If you don’t need item helpers, decorate with @ListSelector but type the accessor as Locator:

@ListSelector("CartItem_")
accessor Rows!: Locator;
const count = await cart.Rows.count();
const second = cart.Rows.nth(1);