Built-In POM
Use RootPageObject and PageObject for wait helpers, assertions, and structured child controls.
The built-in POM layer adds wait helpers, soft assertions, and a typed .expect() API on top of the locator-composition primitives. Use it when these helpers save real code; use plain classes when they do not.
Root: RootPageObject
A page object that is instantiated directly from Page. Pair with @RootSelector(...) to set the scope.
import {
PageObject,
RootPageObject,
RootSelector,
Selector,
SelectorByRole,
} from "playwright-page-object";
@RootSelector("CheckoutPage")
class CheckoutPage extends RootPageObject {
@Selector("PromoCodeInput")
accessor PromoCode = new PageObject();
@SelectorByRole("button", { name: "Apply" })
accessor ApplyButton = new PageObject();
async applyPromoCode(code: string) {
await this.PromoCode.waitVisible();
await this.PromoCode.$.fill(code);
await this.ApplyButton.$.click();
}
}
// Usage
const checkout = new CheckoutPage(page);
await checkout.expect().toBeVisible();
await checkout.applyPromoCode("SAVE20");Children: PageObject
A PageObject is a nested control. Decorate accessors with = new PageObject() (the initializer form) — the library clones it under the parent's scope on each access.
Accessing the raw locator
await checkout.PromoCode.$.fill("SAVE20");
// ^ raw Locator.$ is the raw Playwright Locator. Use it whenever a built-in wait helper does not cover your case.
Wait helpers
| Method | Accepts |
|---|---|
waitVisible() / waitHidden() | — |
waitText(text) | string | RegExp |
waitValue(value) | string | RegExp | number |
waitCount(count) | number |
waitChecked() / waitUnChecked() | — |
const promo = checkout.PromoCode;
await promo.waitVisible();
await promo.waitValue("SAVE20");
await promo.waitText(/applied/i);Assertions via .expect()
.expect() returns Playwright's assertion API on the underlying locator. Pass { soft: true } for soft assertions, { message: "..." } for a custom failure message.
await checkout.PromoCode.expect().toBeVisible();
await checkout.PromoCode.expect({ soft: true }).toHaveText("Code applied");
await checkout.ApplyButton.expect({ message: "Apply must be enabled" }).toBeEnabled();Nested page objects
A custom PageObject subclass uses the same initializer form:
class CartItemControl extends PageObject {
@SelectorByRole("button", { name: "Remove" })
accessor RemoveButton = new PageObject();
async remove() {
await this.RemoveButton.$.click();
}
}
@RootSelector("CartPage")
class CartPage extends RootPageObject {
@Selector("CartItem_1")
accessor FirstItem = new CartItemControl();
}For subclasses with custom constructor arguments, implement cloneWithContext() — see the PageObject API reference.
When to pick this style
- You want wait helpers and
.expect()on accessors without writing them yourself. - You're working with lists and want
filterByText,getItemByTestId, async iteration — see Lists. - The page object has enough nested structure that the helpers pay back.
When to skip it
- Test ids are mostly raw —
Locatoraccessors are enough. - You already have a control library — see Custom Controls.
See also
- PageObject API — full method reference.
- RootPageObject API — root-only methods and constructor contract.
- Lists —
ListPageObjectfor repeated controls.
Last updated on