Skip to content

Registering locators

Playwright creates locators with methods such as page.getByRole() and page.locator(). POMWright registers them under a path with the same methods and the same arguments: add(path) followed by one method. The locator is created when the path is resolved, from the page or from the locator of the segment before it.

registry.add("main").locator("main");
registry.add("main.form@login").getByRole("form", { name: "Log in" });
registry.add("main.form@login.button@login").getByRole("button", { name: "Log in" });

In a page object the same calls are this.add(…) inside defineLocators().

method the Playwright method it mirrors
getByRole(role, options?) getByRole
getByText(text, options?) getByText
getByLabel(text, options?) getByLabel
getByPlaceholder(text, options?) getByPlaceholder
getByAltText(text, options?) getByAltText
getByTitle(text, options?) getByTitle
locator(selector, options?) locator
getByTestId(testId) getByTestId
frameLocator(selector) the <iframe> element, entered with contentFrame(); see Frames
getById(id) none: POMWright’s own, see Locating by id

The arguments are Playwright’s, and so are their types: which strings are ARIA roles, which options each method takes. A registration resolves to what Playwright’s method returns:

registry.add("cart").locator("section.cart", { hasText: "Total" });
// locator('section.cart').filter({ hasText: 'Total' })
registry.add("cart.text@total").getByText("Total", { exact: true });
// getByText('Total', { exact: true })

After the method you can chain the steps Playwright chains after a locator, in any order and number: filter, nth and describe. They apply every time the path is resolved, in the order you chain them:

registry
.add("main.list.item@last")
.getByRole("listitem", { name: /Row/ })
.filter({ hasText: "Row" })
.nth("last")
.describe("Last matching row");
// getByRole('listitem', { name: /Row/ }).filter({ hasText: 'Row' }).last(), described "Last matching row"

A step needs a method before it. add(path) offers only the ten methods, so add("main").filter(…) is a compile error; from JavaScript it throws:

filter() needs a locator strategy first for "main"; call getByRole, getByText, getByLabel, getByPlaceholder, getByAltText, getByTitle, locator, frameLocator, getByTestId or getById before it.

The first argument is required, as it is in Playwright. A registration without it throws at the call, naming the method and the path:

getByRole requires a role value for "main.button".

An empty string is rejected only where Playwright cannot use it: locator("") and frameLocator("") are selectors that do not parse, getByRole("") names no role, and an empty id is invalid HTML (see Locating by id):

locator requires a non-empty selector for "main.list".
getByRole requires a non-empty role for "main.button".

getByTestId("") and the text methods keep an empty string, because Playwright matches it: getByTestId("") finds an element with data-testid="". From JavaScript, a first argument Playwright cannot take, such as null, a number or an object where a string belongs, gets the missing-value message: locator requires a selector value for "main.list".

The only call without a first argument is frameLocator(), which registers Playwright’s any-frame locator (see Frames).

A path holds one locator. Registering it again throws at the second method call and shows both records, the existing one and the attempted one:

A locator schema with the path "list.items" already exists.
Existing and attempted schemas: {
"existing": {
"locatorSchemaPath": "list.items",
"definition": { "type": "locator", "selector": "li" },
"steps": [{ "kind": "filter", "filter": { "has": "locator('b')" } }]
},
"attempted": {
"locatorSchemaPath": "list.items",
"definition": { "type": "locator", "selector": "li" },
"steps": []
}
}

The message spreads the JSON over more lines; it is shortened here. Calling a second method on the same add(path) throws too: A locator definition for "main.button" has already been provided; only one locator type can be set for a registration. A call that throws registers nothing and changes nothing that is registered.

The registry keeps a copy of every options and filter object you pass, so changing the object afterwards changes nothing:

const options = { name: "Save" };
registry.add("form.button@save").getByRole("button", options);
options.name = "Cancel";
registry.getLocator("form.button@save"); // getByRole('button', { name: 'Save' })

The copy reaches every value you pass. A RegExp is copied too, whether it is the first argument, an option or a filter value, so rewriting yours afterwards with compile() changes nothing registered. A Locator is kept as it is: every method of a Locator returns a new one, so the one you passed cannot change. This holds for add, createReusable and queries alike. To change a locator for one use, query it; see Queries.

A registration itself ends at the next call on the registry: the next add, getLocator, getNestedLocator, getLocatorSchema, verify, or the resolution of a query. A registration kept in a variable takes its steps before then, as if they were chained; after that, a call of it throws and changes nothing:

const item = registry.add("list.item").getByRole("listitem");
item.nth(0); // still part of the registration
registry.add("list").locator("ul"); // ends the registration of list.item
item.filter({ hasText: "Open" }); // throws; list.item stays getByRole('listitem').first()
filter() cannot apply to "list.item": its registration has ended; give a registration all its steps in its add(…) chain, or query the path for one use.

So nothing that reads the registry ever sees a registration change. In a page object, the check that runs after defineLocators() ends the last one.

A page object registers its locators in defineLocators(), which runs once, right after the page object is constructed. Every locator call on the page object runs it to the end first, so by the time a chain is resolved, every path is registered. Within defineLocators(), the order of the registrations matters for one thing only, and the difference is between a reference and a copy:

  • A has or hasNot path is a reference. The filter stores the path as written and looks it up each time a chain that uses it is resolved: by getLocator, getNestedLocator, a query’s getLocator and getNestedLocator, and verify(). So it may be registered after the filter that names it, and it always resolves to what is registered at that moment. The check that runs after defineLocators() is the first such resolution, and it reports a name that is never registered (see Verifying a registry).
  • A reused path is copied. add(path, { reuse: "main.list.item" }) reads the record of main.list.item at that call and registers a copy of it at once, which filter, nth and describe then narrow. So the path you reuse must be registered first; otherwise the call throws No locator schema registered for path "main.list.item". (see Reusing locators).