Skip to content

Quick Start

This page walks through a minimal working setup using a plain class with decorators and raw Locator accessors — the simplest entry point.

import type { Locator, Page } from "@playwright/test";
import { RootSelector, Selector, SelectorByRole } from "playwright-page-object";
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("PromoCodeInput")
accessor PromoCodeInput!: Locator;
@SelectorByRole("button", { name: "Apply" })
accessor ApplyPromoButton!: Locator;
async applyPromoCode(code: string) {
await this.PromoCodeInput.fill(code);
await this.ApplyPromoButton.click();
}
}

The @RootSelector("CheckoutPage") scopes every child accessor under page.locator("body").getByTestId("CheckoutPage"). Each child accessor resolves lazily — the locator chain is rebuilt on every access, so the same accessor can be reused across awaits without staleness.

import { test } from "@playwright/test";
test("apply promo code", async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.applyPromoCode("SAVE20");
});

For test suites that use Playwright fixtures, createFixtures wires up instantiation:

import { test as base } from "@playwright/test";
import { createFixtures } from "playwright-page-object";
export const test = base.extend<{ checkoutPage: CheckoutPage }>(
createFixtures({ checkoutPage: CheckoutPage }),
);
test("apply promo code", async ({ checkoutPage }) => {
await checkoutPage.applyPromoCode("SAVE20");
});

See the Fixtures guide for factory-function support and shared configuration.

  • @RootSelector(id) set the class’s root locator to page.locator("body").getByTestId(id).
  • @Selector(id) resolved each child accessor as root.getByTestId(id).
  • @SelectorByRole(...) resolved as root.getByRole(...).
  • Locators were lazy — each .fill() or .click() rebuilt the chain from the current page.