Context Resolution
How child decorators find their root locator.
When a decorated accessor is read, the library walks a fixed priority list to determine the root locator it chains from. Understanding this list explains every host pattern in the library.
Priority order
For a class instance host:
- Decorator-managed locator — set by
@RootSelector(...),@RootSelectorByRole(...), etc., on the class. Stored under an internalLOCATOR_SYMBOL. locatorproperty — a PlaywrightLocator-like value onhost.locator. Used by fragments and custom controls.pageproperty — a PlaywrightPage-like value onhost.page. Falls back tohost.page.locator("body").- Throw — if none match.
The first match wins.
Examples
1. Decorator-managed (scoped root)
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("Promo")
accessor Promo!: Locator;
}
// Promo resolves to: page.locator("body").getByTestId("CheckoutPage").getByTestId("Promo")The @RootSelector decorator stores page.locator("body").getByTestId("CheckoutPage") under LOCATOR_SYMBOL. Child accessors find it via priority 1.
2. locator property (fragment)
class PromoSection {
constructor(readonly locator: Locator) {}
@Selector("CodeInput")
accessor CodeInput!: Locator;
}
// CodeInput resolves to: locator.getByTestId("CodeInput")There is no LOCATOR_SYMBOL (no @RootSelector). The locator property is used directly — priority 2.
3. page property (page-only host)
class HomePage {
constructor(readonly page: Page) {}
@Selector("Welcome")
accessor Welcome!: Locator;
}
// Welcome resolves to: page.locator("body").getByTestId("Welcome")No LOCATOR_SYMBOL, no locator property. The page property is used via page.locator("body") — priority 3.
Edge cases
Both locator and page present
class Hybrid {
constructor(readonly page: Page, readonly locator: Locator) {}
}Priority 2 wins. locator is the more specific scope, so it takes precedence over page.locator("body").
@RootSelector() with no argument
Sets the scope to page.locator("body") — the same as a page-only host:
@RootSelector()
class A { constructor(readonly page: Page) {} }
class B { constructor(readonly page: Page) {} }
// A and B have identical child resolution.Use @RootSelector() only when the class extends RootPageObject — that base class relies on the decorator to wire up its internal state.
@Selector() with no argument
The identity selector — returns the resolved root unchanged:
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector()
accessor self!: Locator;
}
// self resolves to: page.locator("body").getByTestId("CheckoutPage")Useful for exposing the root locator directly.
No matching priority
If none of the three sources are present, accessing the decorated accessor throws:
Cannot resolve locator: the class does not implement the context
protocol (LOCATOR_SYMBOL), and has no Locator-like `locator`
property, and has no Playwright `page` property.Fix: add @RootSelector(...), or a readonly locator: Locator, or a readonly page: Page to the host.
Why this design
Every host pattern in the library — scoped root, page-only, fragment, custom control, built-in POM — is a different position on this priority list. There is one resolution rule, not four; the patterns just compose it differently.
See also
- Choosing a Style — which pattern to pick.
- Decorator API — full decorator reference.
- Troubleshooting — common resolution errors.
Last updated on