Decorator Resolution System
Relevant source files
The following files were used as context for generating this wiki page:
The Decorator Resolution System is the core architectural component of the playwright-page-object library. It transforms standard TypeScript classes into a typed, composable control graph by replacing manual page.locator() chains with lazy-evaluated decorators. The system operates on a hierarchical model where root decorators establish a top-level context, and child decorators resolve locators relative to that context.
This system is designed for incremental adoption, supporting three distinct output styles: raw Playwright Locator objects, external control classes, and a built-in Page Object Model (POM) layer. By utilizing ECMAScript accessor decorators, the system ensures that selectors stay lazy and are only rebuilt when accessed during test execution.
Sources: README.md:1-20, README.md:183-205, package.json:4-6
Architectural Overview
The system is divided into two primary decorator tiers: Root Decorators and Child Decorators. Together, they form a recursive resolution chain that navigates the DOM structure via Playwright's locator engine.
Context Propagation Flow
The resolution logic follows a strict top-down flow. A root class (either a plain class or one extending RootPageObject) is initialized with a Playwright Page. The root decorator wraps this instance to provide a base locator (usually the body or a specific test ID). When a decorated accessor is accessed, it requests the parent's locator as its own root.
The diagram above illustrates how the locator context is passed from the Page level down through nested decorators.
Sources: src/decorators/rootSelectors.ts:44-93, README.md:65-79
Root Context Resolution
Root decorators are applied to classes that serve as the entry point for a page or a major component. They ensure that any subsequent child selectors have a valid parent locator to resolve from.
Root Selector Types
The system provides several specialized root decorators that map directly to Playwright's Locator API:
| Decorator | Playwright Mapping | Description |
|---|---|---|
@RootSelector(id?) | getByTestId(id) or p | Sets the root to a specific Test ID or the body if omitted. |
@ListRootSelector(id) | getByTestId(new RegExp(id)) | Used for containers containing items with patterned IDs. |
@RootSelectorByRole(role, opts) | getByRole(role, opts) | Sets root based on ARIA roles. |
@RootSelectorByText(text) | getByText(text) | Sets root based on visible text. |
Sources: src/decorators/rootSelectors.ts:108-185, README.md:213-225
Implementation Logic
Root decorators use a higher-order function RootSelectorBy which intercepts the class constructor. It validates that the first argument passed to the constructor is a Playwright Page object. For classes extending RootPageObject, it sets the internal root and selector properties; for plain classes, it attaches the resolved locator to a internal LOCATOR_SYMBOL.
function RootSelectorBy(selector: SelectorType) {
return function <TClass extends AnyConstructor>(
target: PageFirstConstructor<TClass>,
context: ClassDecoratorContext<TClass>,
): TClass {
// ... constructor override logic ...
const page = resolvePage(args[0], target.name);
const root = page.locator("body");
this.root = root;
this.selector = selector;
}
}
Sources: src/decorators/rootSelectors.ts:44-88
Child Selector Resolution
Child decorators define relative selectors within a decorated class. They must be applied to class accessor properties.
Resolution Mechanisms
When an accessor decorated with @Selector (or its variants) is called, the system performs the following steps:
- Context Retrieval: It identifies the parent instance's locator.
- Locator Derivation: it applies the decorator's specific selector (e.g.,
getByRole) to the parent locator. - Output Transformation: Based on the accessor's type or the decorator's arguments, it returns:
- A raw
Locator. - An instance of an external control class (if a constructor is provided as a trailing argument).
- A built-in
PageObjectorListPageObject(if the accessor was initialized with one).
- A raw
Sources: src/decorators/selectors.ts:46-85, README.md:81-94, skills/playwright-page-object/SKILL.md:50-70
Interaction Sequence
The following sequence diagram demonstrates the lazy resolution that occurs when a test script accesses a decorated property:
The diagram shows that the locator is not computed at class instantiation but rather at the moment of property access.
Sources: src/page-objects/PageObject.ts:68-80, src/decorators/selectors.ts:50-70
Custom Selector Logic
For complex scenarios where standard Playwright locators are insufficient, the system provides the @SelectorBy(fn) decorator. This allows developers to inject custom logic for locator derivation.
- Function Signature:
(root: Locator, value: T) => T - Usage: It can be used to wrap existing accessors and modify the locator based on runtime state or complex DOM relationships.
Sources: src/decorators/selectorBy.ts:8-25, src/index.ts:14
Built-in Page Object Integration
The resolution system is tightly integrated with the PageObject and ListPageObject classes.
PageObject Context Cloning
When a child decorator encounters an accessor initialized with a PageObject instance, it calls the cloneWithContext method. This creates a new instance of the same class but bound to the newly resolved root and selector. This enables deep nesting and ensures that the $ accessor, wait helpers, and assertions work correctly within the sub-component.
cloneWithContext(root: Locator, selector: SelectorType): this {
const PageObjectClass = this.constructor as PageObjectConstructor<this>;
return new PageObjectClass(root.page(), root, selector);
}
Sources: src/page-objects/PageObject.ts:114-118, src/decorators/selectors.ts:64-75
Summary of Resolution Decorators
The following table summarizes the available child decorators and their mapping to the Playwright API:
| Child Decorator | Playwright Mapping |
|---|---|
@Selector(id?) | getByTestId(id) (identity if empty) |
@ListSelector(id) | getByTestId(new RegExp(id)) |
@ListStrictSelector(id) | getByTestId(id) (non-regex) |
@SelectorByRole(role, opts) | getByRole(role, opts) |
@SelectorByText(text) | getByText(text) |
@SelectorByLabel(label) | getByLabel(label) |
@SelectorByPlaceholder(p) | getByPlaceholder(p) |
@SelectorByAltText(alt) | getByAltText(alt) |
@SelectorByTitle(title) | getByTitle(title) |
Sources: src/decorators/selectors.ts:106-215, README.md:227-241
Conclusion
The Decorator Resolution System provides a robust mechanism for managing UI selectors in Playwright. By centralizing locator logic within decorators and utilizing lazy evaluation, it reduces duplication and improves the maintainability of Page Object Models. Its ability to work with plain classes, external controls, and built-in POM layers makes it a flexible solution for various testing architectures.
Sources: README.md:243-265, skills/playwright-page-object/SKILL.md:144-156