Skip to content

Context Resolution

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.

For a class instance host:

  1. Decorator-managed locator — set by @RootSelector(...), @RootSelectorByRole(...), etc., on the class. Stored under an internal LOCATOR_SYMBOL.
  2. locator property — a Playwright Locator-like value on host.locator. Used by fragments and custom controls.
  3. page property — a Playwright Page-like value on host.page. Falls back to host.page.locator("body").
  4. Throw — if none match.

The first match wins.

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

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.

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.

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

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.

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.

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.

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.