playwright-page-object
Getting Started

Choosing a Style

Start simple, add structure only when it pays back.

Start here

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.

Depart from the default when...

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.

Mixing in the same class

@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

Adoption path

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.

Next

Last updated on

On this page