Skip to content

Filters and indices

Playwright narrows a locator with locator.filter() and picks one of several matches with first(), last() and nth(). POMWright records the same steps with a registration and applies them every time the path is resolved, right after the segment’s locator and in the order they were chained. One addition: has and hasNot also take a registered path.

The examples on this page use this page:

<main>
<section>
<h2>Orders</h2>
<ul><li>Open #1</li><li>Shipped #2</li></ul>
</section>
<section>
<h2>Wishlist</h2>
<ul><li>Lamp</li></ul>
</section>
</main>

filter takes Playwright’s filter options, hasText, hasNotText, has, hasNot and visible, and adds a locator.filter() step:

registry.add("section@orders").locator("section").filter({ hasText: "Orders" });
registry.add("section@orders.item@open").getByRole("listitem").filter({ hasText: "Open" });
// locator('section').filter({ hasText: 'Orders' }).getByRole('listitem').filter({ hasText: 'Open' })

has and hasNot take a Playwright Locator, as in Playwright, or a registered path:

registry.add("main").locator("main");
registry.add("main.section@orders").locator("section").filter({ has: "main.section@orders.heading" });
registry.add("main.section@orders.heading").getByRole("heading", { name: "Orders" });
registry.add("main.section@orders.item@open").getByRole("listitem").filter({ hasText: "Open" });
registry.getNestedLocator("main.section@orders.item@open");
// locator('main').locator('section').filter({ has: getByRole('heading', { name: 'Orders' }) })
// .getByRole('listitem').filter({ hasText: 'Open' })

A path given to has or hasNot resolves to that path’s own locator with its own steps, without its ancestors, because Playwright evaluates has inside the element being filtered: the heading is looked for inside each section, not from the top of the page. A misspelt path is a compile error. The path may be registered after the filter that names it; the registry’s verification reports a name that is never registered.

Filters that refer to each other in a loop cannot be resolved, and verification reports every path in the loop:

Locator registry verification failed for 2 of 2 paths:
"main.list@orders": Detected cyclic filter reference while resolving "main.list@orders": "main.list@archive".
"main.list@archive": Detected cyclic filter reference while resolving "main.list@archive": "main.list@orders".

A reference that is neither a Locator nor a path, from JavaScript, fails the same way: Unsupported filter reference while resolving "main.list@orders". Filter has/hasNot supports Playwright Locator instances or registry path strings.

nth picks one element: "first" and "last" become Playwright’s first() and last(), and a number goes to nth() as given, so -1 is the last element:

registry.add("list").locator("ul");
registry.add("list.item@first").getByRole("listitem").nth("first");
registry.add("list.item@third").getByRole("listitem").nth(2);
registry.add("list.item@last").getByRole("listitem").nth(-1);

As in Playwright, an index belongs at the end of a chain that cannot be made unique otherwise; a filter on what identifies the element survives changes to the page that an index does not.

Steps apply in the order they are chained, so a filter after an index filters the one element the index picked:

registry.add("list.item").locator("li").filter({ hasText: "Open" }).nth(0).filter({ hasNotText: "#9" });
// locator('li').filter({ hasText: 'Open' }).first().filter({ hasNotText: '#9' })

When a path resolves to a chain, each segment’s steps apply right after that segment’s locator, before the next segment.

A step registered with the path applies to every use. A step for one test, such as the row with a given order number, goes on a query instead, which leaves the registry as it is:

const order = registry.getLocatorSchema("list.items").filter({ hasText: "#1042" }).getNestedLocator();

See Queries. A step on a frame segment applies to the <iframe> element, before the frame is entered.