Skip to content

Choosing a Style

A plain class with @RootSelector(...) and raw Locator accessors covers most pages:

import type { Locator, Page } from "@playwright/test";
import { RootSelector, Selector } from "playwright-page-object";
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("PromoCodeInput")
accessor PromoCodeInput!: Locator;
}

That’s the whole pattern. No inheritance, no fixtures, no extra abstractions. If you can use this, use it — and skip the rest of this page until you hit a case that needs something else.

SituationWhat to doWhy
The page has no container data-testidDrop @RootSelector — keep the plain classChains from page.locator("body") automatically
A UI section repeats across pagesMake it a fragmentConstructor takes locator: Locator; nested decorators chain from it
You already have a control libraryPass it as the second arg to @Selector(...)The decorator wraps the resolved locator in your class
You want waitVisible(), .expect(), filter chainsUse PageObject / ListPageObjectBuilt-in wait helpers and assertions

Each row is a small change to the default, not a different framework. Mix freely.

@RootSelector("CheckoutPage")
class CheckoutPage extends RootPageObject {
@Selector("PromoCodeInput")
accessor PromoCode!: Locator; // raw
@SelectorByRole("button", { name: "Apply" }, ButtonControl)
accessor ApplyButton!: ButtonControl; // custom control
@ListSelector("CartItem_")
accessor Items = new ListPageObject(CartItemControl); // built-in POM
}

The decorator decides what shape to produce based on how you declare the accessor:

  • accessor X!: Locator → raw Locator
  • @Selector("Id", MyClass) → instance of a plain control class MyClass, constructed with the resolved locator. MyClass must not extend PageObject — use the initializer form below for that case.
  • accessor X = new PageObject()PageObject (or subclass), cloned and bound to the parent’s scope on each access

Pick the simplest thing that works. Evolve only when a real pain shows up:

  • Duplicate locator chains across tests — wrap them in a plain class with @RootSelector and raw Locator accessors.
  • Same methods called on the same accessor (fill, click, expect) — extract a custom control.
  • Same wait or assertion code repeating — switch that accessor to PageObject.

Three similar .fill() calls is not duplication; five matching wait blocks across files is. See Incremental Adoption for a longer walkthrough.