Typed, decorator-driven Page Object Model for Playwright. Reusable, lazy locator chains in plain TypeScript classes.
Documentation: https://pom.shmakov.tools/
The package includes a local, read-only MCP server that lets AI coding agents inspect your existing page objects and JSX or TSX test IDs before editing tests. Agents can follow typed locator chains, reuse selectors already in the repository, and check selector coverage without launching a browser or executing application code.
It works with local stdio MCP clients including Claude Code, Cursor, VS Code, and Codex CLI. The server exposes five tools for discovering page objects, reading selector trees, finding test IDs, and auditing coverage.
Explore the MCP server docs → · Connect your MCP client →
// Before — selectors duplicated, structure invisible
await page.getByTestId("CheckoutPage").getByTestId("PromoCodeInput").fill("SAVE20");
await page.getByTestId("CheckoutPage").getByRole("button", { name: "Apply" }).click();// After — typed, composable, reusable
await checkoutPage.applyPromoCode("SAVE20");A locator-composition layer, not a framework. Decorators scope a class to a Playwright locator; child decorators resolve relative to that scope. The accessor type determines output: raw Locator, a custom control, or a built-in PageObject.
- No inheritance required for basic use
- Lazy locator chains rebuild only when accessed
- Three output styles coexist in the same suite
- TypeScript-first, ECMAScript decorators (no
experimentalDecoratorsneeded)
npm install -D playwright-page-objectRequirements:
- Node
>=20 @playwright/test >=1.35.0- TypeScript
>=5.0(withtarget: "ES2015"or higher)
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 ApplyButton!: Locator;
async applyPromoCode(code: string) {
await this.PromoCodeInput.fill(code);
await this.ApplyButton.click();
}
}import { test } from "@playwright/test";
test("apply promo code", async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.applyPromoCode("SAVE20");
});See the Quick Start guide for fixtures, page-only hosts, and the next steps.
Three styles, picked per accessor. Mix freely in the same class.
Minimal abstraction. Typed accessor, no helpers.
@Selector("PromoCodeInput")
accessor PromoCodeInput!: Locator;Pass any class whose constructor accepts a Locator. Reuse your existing control library.
@Selector("PromoCodeInput", InputControl)
accessor PromoCode!: InputControl;PageObject / ListPageObject for wait helpers, soft assertions, and filter chains.
class CheckoutPage extends RootPageObject {
@Selector("PromoCodeInput")
accessor PromoCode = new PageObject();
async applyPromo(code: string) {
await this.PromoCode.waitVisible();
await this.PromoCode.$.fill(code);
}
}Child decorators resolve in this order: a @RootSelector-managed locator, then a locator property on the host, then page.locator("body") if page is present. The first match wins.
Context Resolution reference →
The full documentation site covers every guide, the API reference, and the v1 → v2 migration:
- Getting Started — install, quick start, choosing a style
- Guides — plain classes, fragments, custom controls, built-in POM, lists, fixtures, incremental adoption
- Reference — context resolution, migration v1 → v2, troubleshooting
- API — decorators,
PageObject,RootPageObject,ListPageObject,createFixtures
This package also ships an Agent Skills-compatible skill so AI assistants load library-specific guidance on demand:
npx ctx7 skills install /sergeyshmakov/playwright-page-object playwright-page-objectIt is also indexed in Context7 and documented in a Cubic wiki. See AI Tooling in the docs.
See the migration guide. Most changes are mechanical renames.
See CONTRIBUTING.md.