Skip to content

Built-In POM

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.

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

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.

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.

MethodAccepts
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);

.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();

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.

  • 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.
  • Test ids are mostly raw — Locator accessors are enough.
  • You already have a control library — see Custom Controls.