### Run E2E Tests from Example Folder
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/example/README.md
Commands to navigate to the example folder, build the parent package, install dependencies, and run E2E tests.
```bash
cd ..
npm run build
cd example
npm install
npm run test:e2e
```
--------------------------------
### Preview Documentation Locally
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/CONTRIBUTING.md
Steps to preview the documentation site locally. Navigate to the docs directory, install dependencies, and start the development server.
```bash
cd docs
npm install
npm run dev
```
--------------------------------
### Run Example Tests
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/example/README.md
Commands to build the package and run the example tests.
```bash
npm run build
npm run test:example
```
--------------------------------
### Verify Playwright Page Object Installation
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/getting-started/installation.mdx
A minimal smoke test to verify the library is correctly installed and wired into your Playwright tests. This example demonstrates basic usage of the RootSelector and Selector decorators.
```typescript
import type { Locator, Page } from "@playwright/test";
import { test } from "@playwright/test";
import { RootSelector, Selector } from "playwright-page-object";
@RootSelector("App")
class AppPage {
constructor(readonly page: Page) {}
@Selector("Header")
accessor Header!: Locator;
}
test("library is wired", async ({ page }) => {
const app = new AppPage(page);
await app.Header.waitFor();
});
```
--------------------------------
### Install playwright-page-object
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/getting-started/installation.mdx
Install the library as a development dependency using npm.
```bash
npm install -D playwright-page-object
```
--------------------------------
### Run Development Server
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/example/README.md
Command to start the React development server.
```bash
npm run dev
```
--------------------------------
### Install Playwright Page Object Agent Skill
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/ai-tooling/agent-skills.mdx
Use this command to install the Agent Skill for playwright-page-object. Assistants with skill support will automatically load it when working with this library.
```bash
npx ctx7 skills install /sergeyshmakov/playwright-page-object playwright-page-object
```
--------------------------------
### Install Packed Package
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/example/README.md
Instructions for installing a packed version of the playwright-page-object package to resolve potential duplicate module issues.
```bash
npm pack --pack-destination .
npm install ../playwright-page-object-1.0.0.tgz
```
--------------------------------
### With raw Locator
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Example of using ListPageObject with a raw Locator when item-specific helpers are not needed.
```APIDOC
## With raw `Locator`
If you don't need item helpers, decorate with `@ListSelector` but type the accessor as `Locator`:
```ts
@ListSelector("CartItem_")
accessor Rows!: Locator;
const count = await cart.Rows.count();
const second = cart.Rows.nth(1);
```
```
--------------------------------
### Test Fixture Setup
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/example/README.md
Demonstrates setting up Playwright test fixtures using `createFixtures` to automatically provide page objects like `checkoutPage` to tests.
```typescript
export const test = base.extend(
createFixtures({ checkoutPage: CheckoutPage })
);
test("example", async ({ checkoutPage }) => {
await checkoutPage.applyPromoCode("SAVE20");
await checkoutPage.expectCartHasItemCount(2);
});
```
--------------------------------
### Async Iteration over List Items
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/example/README.md
Example of iterating over items in a list using `for await` to perform actions or checks on each item.
```typescript
for await (const item of checkoutPage.CartItems.items) {
await item.expect({ soft: true }).toBeVisible();
}
```
--------------------------------
### Basic Fixture Setup with Constructors
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/fixtures.mdx
Use `createFixtures` with page object constructors to automatically instantiate them with the Playwright `page` instance. Pass the result to `test.extend()` to make them available in your tests.
```typescript
import { test as base } from "@playwright/test";
import { createFixtures } from "playwright-page-object";
import { CheckoutPage } from "./page-objects/CheckoutPage";
import { CartPage } from "./page-objects/CartPage";
export const test = base.extend({
checkoutPage: CheckoutPage,
cartPage: CartPage,
}(createFixtures({
checkoutPage: CheckoutPage,
cartPage: CartPage,
})));
test("apply promo code", async ({ checkoutPage }) => {
await checkoutPage.applyPromoCode("SAVE20");
});
```
--------------------------------
### Run Local Development Commands
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/CONTRIBUTING.md
Commands to set up and run the project locally. Use `npm ci` for dependency installation, `npm run dev` for watching changes, `npm test` for running tests, and `npm run lint` for code linting.
```bash
npm ci
npm run dev # tsup watch mode
npm test # vitest
npm run lint # biome check
```
--------------------------------
### Minimal Page-Only Host Example
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/page-only-hosts.mdx
Defines a page-only host class for a checkout page. Child selectors like 'PromoCodeInput' will be scoped to 'page.locator("body")' by default.
```typescript
import type { Locator, Page } from "@playwright/test";
import { Selector } from "playwright-page-object";
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("PromoCodeInput")
accessor PromoCodeInput!: Locator;
}
```
--------------------------------
### Usage Example
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/create-fixtures.mdx
Demonstrates how to use `createFixtures` to extend Playwright's test instance with custom page object fixtures. It shows how to define fixtures for both class constructors and factory functions.
```APIDOC
## Usage
```ts
import { test as base } from "@playwright/test";
import { createFixtures } from "playwright-page-object";
export const test = base.extend<{ checkoutPage: CheckoutPage; authPage: AuthPage }>(createFixtures({
checkoutPage: CheckoutPage,
authPage: (page) => new AuthPage(page, authConfig),
}));
```
The `<{ ... }>` generic on `base.extend` tells TypeScript the fixture types. `createFixtures` produces matching runtime wiring.
```
--------------------------------
### FilterByItemTestId vs FilterByHasTestId
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Explanation and example differentiating `filterByItemTestId` and `filterByHasTestId`.
```APIDOC
## `filterByItemTestId` vs `filterByHasTestId`
```html
Apple
```
- `filterByItemTestId("CartItem_1")` → matches this row (own `data-testid`).
- `filterByHasTestId("RemoveButton")` → matches this row (descendant `data-testid`).
The two methods address different cases — pick by which level the id lives on.
```
--------------------------------
### Mixed Fixture Setup with Constructors and Factories
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/fixtures.mdx
Combine page objects defined as classes and those defined as factory functions within the same `createFixtures` map. The library intelligently handles instantiation based on whether a class or a function is provided.
```typescript
export const test = base.extend({
homePage: HomePage,
authPage: AuthPage,
}(createFixtures({
homePage: HomePage, // class
authPage: (page) => new AuthPage(page, authConfig), // factory
})));
```
--------------------------------
### Get All List Items
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Shows how to retrieve all matching items from a ListPageObject as an array using the .getAll() method. The items are resolved at the time of the call.
```typescript
const all = await list.getAll();
```
```typescript
for (const item of all) { /* ... */ }
```
--------------------------------
### Assert and Get List Item Count
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/lists.mdx
Use `.waitCount()` to assert the number of items in the list and `.count()` to get the current number of items.
```typescript
// Count assertions
await cart.Items.waitCount(3);
await cart.Items.count(); // returns Promise
```
--------------------------------
### Fixture Setup with Factory Functions
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/fixtures.mdx
When a page object constructor requires additional arguments beyond the Playwright `page` instance, use an arrow function as a factory. This function receives the `page` and should return an instance of your page object, potentially with extra configuration.
```typescript
class AuthPage {
constructor(readonly page: Page, readonly config: AuthConfig) {}
}
export const test = base.extend({
authPage: AuthPage,
}(createFixtures({
authPage: (page) => new AuthPage(page, authConfig),
})));
```
--------------------------------
### Get First or Last List Item
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Illustrates how to get a single item bound to the first or last match in a ListPageObject using .first() and .last().
```typescript
const head = list.first();
```
```typescript
const tail = list.last();
```
--------------------------------
### Quick Start: Define a Page Object Class
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/README.md
Defines a basic Page Object class using decorators to map selectors to properties. The `RootSelector` decorator scopes the class, while `Selector` and `SelectorByRole` map specific elements.
```typescript
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();
}
}
```
--------------------------------
### CheckoutPage Custom Methods
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/example/README.md
Example of custom methods within a CheckoutPage page object for applying promo codes and checking cart item counts.
```typescript
// CheckoutPage
async applyPromoCode(code: string) {
await this.PromoCode.$.fill(code);
await this.ApplyPromoButton.$.click();
}
async expectCartEmpty() {
await this.CartItems.waitCount(0);
}
async expectCartHasItemCount(n: number) {
await this.CartItems.waitCount(n);
}
```
--------------------------------
### Minimal Custom Control Example
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/custom-controls.mdx
Defines a custom control class `InputControl` with `fill` and `expectValue` methods. It's then used with the `@Selector` decorator to associate it with a specific element on the `CheckoutPage`.
```typescript
import type { Locator, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { RootSelector, Selector } from "playwright-page-object";
class InputControl {
constructor(readonly locator: Locator) {}
async fill(value: string) {
await this.locator.fill(value);
}
async expectValue(value: string) {
await expect(this.locator).toHaveValue(value);
}
}
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("PromoCodeInput", InputControl)
accessor PromoCode!: InputControl;
}
// Usage
// await checkoutPage.PromoCode.fill("SAVE20");
// await checkoutPage.PromoCode.expectValue("SAVE20");
```
--------------------------------
### Define a RootPageObject with @RootSelector
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/root-page-object.mdx
Extend RootPageObject and use the @RootSelector decorator to define the root element for this page object. This example shows how to define a CheckoutPage with a PromoCode accessor.
```typescript
import {
PageObject,
RootPageObject,
RootSelector,
Selector,
} from "playwright-page-object";
@RootSelector("CheckoutPage")
class CheckoutPage extends RootPageObject {
@Selector("PromoCode")
accessor PromoCode = new PageObject();
}
const checkout = new CheckoutPage(page);
```
--------------------------------
### Define RootPageObject with @RootSelector
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/built-in-pom.mdx
Instantiate a RootPageObject directly from Page and set its scope using @RootSelector. This example defines a CheckoutPage with input and button accessors.
```typescript
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");
```
--------------------------------
### Manual PageObject Instantiation in Tests
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/skills/playwright-page-object/SKILL.md
Manually instantiate PageObjects within tests when not using `createFixtures`. This approach is straightforward and suitable when complex fixture setup is not required.
```typescript
test("...", async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.PromoInput.fill("CODE");
});
```
--------------------------------
### Access Raw Locator with .$
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/built-in-pom.mdx
Use the `.$` accessor to get the raw Playwright `Locator` when built-in wait helpers are insufficient. This allows direct interaction with the element.
```typescript
await checkout.PromoCode.$.fill("SAVE20");
// ^ raw Locator
```
--------------------------------
### Create Playwright Fixtures from Page Objects
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/create-fixtures.mdx
Use `createFixtures` to map page object classes or factories to Playwright fixtures. This example shows how to define fixtures for `checkoutPage` and `authPage`, with `authPage` using a factory function that accepts custom configuration.
```typescript
import { test as base } from "@playwright/test";
import { createFixtures } from "playwright-page-object";
export const test = base.extend({
checkoutPage: CheckoutPage,
authPage: (page) => new AuthPage(page, authConfig),
});
```
--------------------------------
### Fragment Context Resolution Chain
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/fragments.mdx
Illustrates the selector resolution chain when using fragments. The resolution starts from the page's root locator and chains through the parent's selectors and the fragment's selectors.
```plaintext
page.locator("body")
.getByTestId("CheckoutPage")
.getByTestId("PromoSection")
.getByTestId("CodeInput")
```
--------------------------------
### Output Style: Built-in Page Object
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/README.md
Extends `PageObject` or `ListPageObject` to leverage built-in helpers for waiting, soft assertions, and filter chains. The example shows using `waitVisible` and accessing the underlying locator via `$`.
```typescript
class CheckoutPage extends RootPageObject {
@Selector("PromoCodeInput")
accessor PromoCode = new PageObject();
async applyPromo(code: string) {
await this.PromoCode.waitVisible();
await this.PromoCode.$.fill(code);
}
}
```
--------------------------------
### Quick Rules for Decorators
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/decorators.mdx
Essential guidelines for using decorators with accessors, raw locators, and PageObject-based accessors.
```APIDOC
## Quick rules
- Use `accessor` (not `get`/`set` or regular properties) on decorated members.
- Use `!:` for raw `Locator` accessors (`accessor X!: Locator`).
- Use `= new ...()` for `PageObject`-based accessors (`accessor X = new PageObject()`).
- Pass a custom control class as the *last* argument to any child decorator.
```
--------------------------------
### Get Count of List Items
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Demonstrates how to retrieve the total number of matching elements in a ListPageObject using the .count() method.
```typescript
const n = await list.count();
```
--------------------------------
### When to Use createFixtures
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/create-fixtures.mdx
Provides guidance on scenarios where `createFixtures` is beneficial, such as when already using Playwright fixtures, requiring a centralized configuration for page objects, or needing factories for page objects with custom constructor arguments.
```APIDOC
## When to use it
- Your suite already uses Playwright fixtures.
- You want a single point of configuration for page-object setup.
- You need factories for page objects with custom constructor arguments.
Manual instantiation (`new CheckoutPage(page)` inside each test) is equally valid. The library does not require fixtures.
```
--------------------------------
### Get List Item by Role
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Retrieves the first item in a ListPageObject that contains an element matching the specified ARIA role and options.
```typescript
const removable = list.getItemByRole("button", { name: "Remove" });
```
--------------------------------
### Get List Item by Text
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Retrieves the first item in a ListPageObject that contains the specified text. This method returns a single item.
```typescript
const apple = list.getItemByText("Apple");
```
--------------------------------
### Get List Item by Test ID
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Retrieves the first item in a ListPageObject that has a `data-testid` attribute matching the provided string or regular expression.
```typescript
const second = list.getItemByTestId("CartItem_2");
```
```typescript
const byPattern = list.getItemByTestId(/^CartItem_/);
```
--------------------------------
### Constructor
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/root-page-object.mdx
Instantiates a new RootPageObject subclass. The first argument must be a Playwright Page. A root decorator is required to correctly resolve child selectors.
```APIDOC
## Constructor
```ts
new CheckoutPage(page);
```
The first argument must be a Playwright `Page`. The root decorator wires the resolved root locator into the instance — without a root decorator, the class will not resolve children correctly.
```
--------------------------------
### Basic Page Object with RootSelector
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/getting-started/choosing-a-style.mdx
Use a plain class with `@RootSelector` and raw `Locator` accessors for most pages. This is the simplest pattern and covers many common scenarios without extra abstractions.
```typescript
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;
}
```
--------------------------------
### Filter List Items by Text
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Example of filtering a ListPageObject to find items containing specific text. This method returns a new, narrowed ListPageObject.
```typescript
const apples = list.filterByText("Apple");
```
```typescript
await apples.count();
```
--------------------------------
### Define ListPageObject with Item Type
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Example of defining a CartPage with a ListPageObject for CartItemControl. The ListPageObject is configured to use CartItemControl as its item type for typed access.
```typescript
import {
ListPageObject,
ListSelector,
PageObject,
RootPageObject,
RootSelector,
Selector,
} from "playwright-page-object";
class CartItemControl extends PageObject {
@Selector("Name")
accessor Name = new PageObject();
}
@RootSelector("CartPage")
class CartPage extends RootPageObject {
@ListSelector("CartItem_")
accessor Items = new ListPageObject(CartItemControl);
}
```
--------------------------------
### Instantiate root-decorated classes with Playwright Page
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/reference/troubleshooting.mdx
Ensure the first constructor argument is a `Page` object for root-decorated classes, or use `createFixtures()` to automatically provide it.
```typescript
// ✅ manual instantiation
new CheckoutPage(page);
```
```typescript
// ✅ via fixtures
const test = base.extend(createFixtures({ checkoutPage: CheckoutPage }));
```
--------------------------------
### Before: Direct Selectors
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/index.mdx
Illustrates the traditional approach using direct Playwright selectors, which can lead to duplicated selectors and less readable code.
```typescript
// Before — selectors duplicated, structure invisible
await page.getByTestId("CheckoutPage").getByTestId("PromoCodeInput").fill("SAVE20");
await page.getByTestId("CheckoutPage").getByRole("button", { name: "Apply" }).click();
```
--------------------------------
### Class vs Factory Detection Logic
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/create-fixtures.mdx
Explains how `createFixtures` differentiates between class constructors and factory functions for page object instantiation. It highlights the importance of using arrow functions for factories to ensure correct detection.
```APIDOC
## Class vs factory detection
For each entry, `createFixtures` decides whether to `new`-call or directly call:
```ts
const isConstructor =
typeof entry === "function" &&
entry.prototype !== undefined &&
entry.prototype.constructor === entry;
const instance = isConstructor
? new entry(page)
: entry(page);
```
This means:
| Entry shape | Detected as | Called as |
|-------------|-------------|-----------|
| `class Foo {}` (ES class) | constructor | `new entry(page)` |
| `function Foo() {}` (regular function) | constructor | `new entry(page)` |
| `(page) => new Foo(page, config)` (arrow) | factory | `entry(page)` |
**Use arrow functions for factories.** Regular functions have a `prototype` and will be treated as constructors.
```
--------------------------------
### Accessor Output Types with Decorators
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/skills/playwright-page-object/SKILL.md
Demonstrates different ways to declare accessors using decorators for raw Locators, custom controls, PageObjects, and ListPageObjects. For PageObject subclasses, use the initializer form (`= new MyControl()`) instead of a factory argument.
```typescript
@Selector("Promo")
accessor PromoInput!: Locator;
// Custom control (must accept Locator in constructor — arrow factory also works)
@Selector("Promo", MyInputControl)
accessor PromoInput!: MyInputControl;
// PageObject (built-in, nested) — use the initializer form, not the factory arg
@Selector("Promo")
accessor PromoInput = new PageObject();
// ListPageObject (built-in, repeated items)
@ListSelector("Item_")
accessor Items = new ListPageObject(ItemControl);
```
--------------------------------
### Constructor
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Initializes a new ListPageObject. The itemType can be a PageObject subclass constructor, a PageObject instance, or undefined.
```APIDOC
## Constructor
```ts
new ListPageObject(itemType?)
```
`itemType` is either:
- a `PageObject` subclass constructor — items are `new ItemType()` then bound via `cloneWithContext`,
- a `PageObject` *instance* — items are cloned via `cloneWithContext`,
- `undefined` — items are typed as `PageObject` (no custom helpers).
Invalid `itemType` throws at construction time:
```ts
new ListPageObject("not a class"); // throws
new ListPageObject(42); // throws
```
```
--------------------------------
### Filter List Items by Role
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Example of filtering a ListPageObject to find items containing an element matching a specific ARIA role. Returns a new, narrowed ListPageObject.
```typescript
list.filterByRole("button", { name: "Remove" });
```
--------------------------------
### Manually Run Code Formatter
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/CONTRIBUTING.md
Execute the Biome formatter manually to fix code style issues. This command ensures code adheres to the project's formatting standards.
```bash
npm run lint:fix
```
--------------------------------
### Custom Controls with Other Selector Decorators
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/custom-controls.mdx
Shows how to use custom control classes with various other selector decorators like `SelectorByRole`, `SelectorByText`, and `SelectorByLabel`.
```typescript
@SelectorByRole("button", { name: "Apply" }, ButtonControl)
accessor ApplyButton!: ButtonControl;
@SelectorByText("Submit", ButtonControl)
accessor SubmitButton!: ButtonControl;
@SelectorByLabel("Email", InputControl)
accessor EmailField!: InputControl;
```
--------------------------------
### Identify Removed and Renamed APIs
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/reference/migration-v1-to-v2.mdx
Use grep to find occurrences of APIs that have been removed or renamed in v2. This is the first step in the migration process.
```bash
ListStrictSelector | waitNoValue | waitProp | waitPropAbsence
filterByTestId | getItemByIdMask
```
--------------------------------
### cloneWithContext for Custom Constructors
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/page-object.mdx
Override `cloneWithContext` if your subclass has constructor arguments beyond the default.
```APIDOC
### `cloneWithContext` for custom constructors
#### Description
If your subclass has constructor arguments beyond the default, override `cloneWithContext(root, selector)`. The library calls this on every access to bind the instance to its parent's resolved scope.
#### Example
```ts
class TimedItem extends PageObject {
constructor(readonly timeout: number) {
super();
}
cloneWithContext(root, selector) {
const next = new TimedItem(this.timeout);
return next.attach(root, selector);
}
}
```
#### Note
Most users do not need this — the default `cloneWithContext` covers the no-arg constructor case.
```
--------------------------------
### Fixture Creation with createFixtures()
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/skills/playwright-page-object/SKILL.md
Use `createFixtures` to support constructors and factory functions for dependency injection in tests. This allows for flexible instantiation of PageObjects with or without additional arguments.
```typescript
export const test = base.extend(
createFixtures({
checkoutPage: CheckoutPage,
authPage: (page) => new AuthPage(page, authConfig),
}),
);
test("...", async ({ checkoutPage }) => {
await checkoutPage.PromoInput.fill("CODE");
});
```
--------------------------------
### Custom Control Initialization with Child Decorators
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/decorators.mdx
Child decorators can accept a constructor or arrow function as the last argument to initialize custom controls. Use a class constructor for `new`-called instances or an arrow function for direct calls with the resolved locator.
```typescript
@Selector("Promo", InputControl)
accessor Promo!: InputControl;
@SelectorByRole("button", { name: "Apply" }, ButtonControl)
accessor Apply!: ButtonControl;
@SelectorByLabel("Email", (locator) => new TimedInput(locator, 5000))
accessor Email!: TimedInput;
```
--------------------------------
### Fix 'Cannot resolve locator' by implementing context protocol
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/reference/troubleshooting.mdx
Ensure the host class has a `@RootSelector`, `locator`, or `page` property to enable context chaining for child decorators.
```typescript
@RootSelector("Page")
class HostA { constructor(readonly page: Page) {} }
```
```typescript
class HostB { constructor(readonly locator: Locator) {} }
```
```typescript
class HostC { constructor(readonly page: Page) {} }
```
--------------------------------
### Type Inference with FixturesFromMap
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/create-fixtures.mdx
Illustrates how `FixturesFromMap` infers the instance types for each page object entry, ensuring correct type narrowing when using Playwright's `extend` method with explicit generics.
```APIDOC
## Type inference
`FixturesFromMap` maps each entry to its instance type so that `test.extend` infers fixture types correctly:
```ts
createFixtures({
homePage: HomePage,
authPage: (page) => new AuthPage(page), // authPage: AuthPage
});
```
You still pass the explicit generic to `base.extend<{ homePage; authPage }>` — Playwright's `extend` requires it for stricter type narrowing.
```
--------------------------------
### Wait Helpers
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/page-object.mdx
Provides methods to wait for specific conditions on the control.
```APIDOC
## Wait Helpers
### Description
Provides methods to wait for specific conditions on the control.
### Available Methods
| Method | Accepts |
|-----------------|-----------------------------|
| `waitVisible()` | — |
| `waitHidden()` | — |
| `waitText(text)`| `string \| RegExp` |
| `waitValue(value)`| `string \| RegExp \| number` |
| `waitCount(count)`| `number` |
| `waitChecked()` | — |
| `waitUnChecked()`| — |
### Usage
```ts
await control.waitVisible();
await control.waitText("Applied");
await control.waitText(/applied/i);
await control.waitValue(42); // coerced to "42"
```
### Note
`waitValue` accepts `number` and coerces with `String(value)` for convenience. Pass a `RegExp` for pattern matching.
```
--------------------------------
### Define and Use a Fragment in Playwright Page Object
Source: https://context7.com/sergeyshmakov/playwright-page-object/llms.txt
Demonstrates how to define a reusable UI fragment (PromoSection) and use it across different page objects (CheckoutPage, CartPage). Fragments are scoped to a given locator and help in organizing complex UIs.
```typescript
import type { Locator, Page } from "@playwright/test";
import { RootSelector, Selector } from "playwright-page-object";
// Fragment — scoped to whatever locator it receives
class PromoSection {
constructor(readonly locator: Locator) {}
@Selector("CodeInput")
accessor CodeInput!: Locator;
@Selector("ApplyButton")
accessor ApplyButton!: Locator;
}
// Reuse the same fragment on multiple pages
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("PromoSection", PromoSection)
accessor promo!: PromoSection;
}
@RootSelector("CartPage")
class CartPage {
constructor(readonly page: Page) {}
@Selector("PromoSection", PromoSection)
accessor promo!: PromoSection;
}
test("fragments", async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.promo.CodeInput.fill("SAVE20");
await checkout.promo.ApplyButton.click();
// Chain: body > [data-testid="CheckoutPage"] > [data-testid="PromoSection"] > [data-testid="CodeInput"]
});
```
--------------------------------
### Using ListSelector with Raw Locator
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Demonstrates how to use @ListSelector with a raw Locator type when item-specific helpers are not needed. This allows direct use of Locator methods like .count() and .nth().
```typescript
@ListSelector("CartItem_")
accessor Rows!: Locator;
const count = await cart.Rows.count();
const second = cart.Rows.nth(1);
```
--------------------------------
### Built-In Page Object Model (POM)
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/skills/playwright-page-object/SKILL.md
Use this pattern when you need built-in helpers like `$`, wait functions (`.waitVisible()`, `.waitText()`), soft assertions, or managing many nested controls. It extends `RootPageObject`.
```typescript
class CheckoutPage extends RootPageObject {
@Selector("Promo")
accessor PromoInput = new PageObject();
async applyPromo(code: string) {
await this.PromoInput.waitVisible();
await this.PromoInput.$.fill(code);
}
}
```
--------------------------------
### Custom Control with Factory Function
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/custom-controls.mdx
Demonstrates using a factory function with the `@Selector` decorator when a custom control requires additional constructor arguments, such as a timeout value.
```typescript
class TimedInputControl {
constructor(readonly locator: Locator, readonly timeout: number) {}
}
@Selector("PromoCodeInput", (locator) => new TimedInputControl(locator, 5000))
accessor PromoCode!: TimedInputControl;
```
--------------------------------
### After: Page Object Method
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/index.mdx
Demonstrates the 'after' approach using a Page Object method, showcasing cleaner, more maintainable code with typed and composable locators.
```typescript
// After — typed, composable, reusable
await checkoutPage.applyPromoCode("SAVE20");
```
--------------------------------
### Page-Only Host
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/skills/playwright-page-object/SKILL.md
A lightweight host pattern suitable for globally unique test IDs with minimal boilerplate. It uses the `page.locator("body")` as its default scope.
```typescript
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("Promo")
accessor PromoInput!: Locator;
}
```
--------------------------------
### Equivalent Scoping Forms
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/page-only-hosts.mdx
Demonstrates three functionally equivalent ways to achieve the same scoping for child selectors: a page-only host, '@RootSelector()' with no argument, and '@RootSelector()' with 'body'. Choose the page-only form when the decorator isn't needed for other purposes.
```typescript
// 1. Page-only host (no decorator)
class A {
constructor(readonly page: Page) {}
}
// 2. @RootSelector() with no argument
@RootSelector()
class B {
constructor(readonly page: Page) {}
}
// 3. @RootSelector() with body
@RootSelector()
class C {
constructor(readonly page: Page) {}
}
```
--------------------------------
### Instantiate RootPageObject
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/root-page-object.mdx
Instantiate a RootPageObject by passing a Playwright Page object as the first argument. The root decorator is essential for correct child accessor resolution.
```typescript
new CheckoutPage(page);
```
--------------------------------
### ListPageObject Constructor Usage
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Demonstrates valid and invalid usages of the ListPageObject constructor. The constructor accepts a PageObject subclass constructor, a PageObject instance, or undefined for item typing.
```typescript
new ListPageObject(CartItemControl);
```
```typescript
new ListPageObject(new CartItemControl());
```
```typescript
new ListPageObject();
```
```typescript
new ListPageObject("not a class"); // throws
```
```typescript
new ListPageObject(42); // throws
```
--------------------------------
### Share Fragments Across Multiple Page Objects
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/fragments.mdx
Demonstrates how to reuse the same fragment class in different page objects. This allows for consistent modeling of UI sections that appear on multiple pages without requiring inheritance.
```typescript
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("PromoSection", PromoSection)
accessor promo!: PromoSection;
}
@RootSelector("CartPage")
class CartPage {
constructor(readonly page: Page) {}
@Selector("PromoSection", PromoSection)
accessor promo!: PromoSection;
}
```
--------------------------------
### Plain Class with Actions
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/plain-classes.mdx
Extends a plain page object class by adding regular methods alongside decorated accessors. This allows for composing actions directly within the page object.
```typescript
@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();
}
}
```
--------------------------------
### Before and After Page Object Usage
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/README.md
Compares traditional Playwright selector usage with the streamlined approach offered by playwright-page-object.
```typescript
// Before — selectors duplicated, structure invisible
await page.getByTestId("CheckoutPage").getByTestId("PromoCodeInput").fill("SAVE20");
await page.getByTestId("CheckoutPage").getByRole("button", { name: "Apply" }).click();
```
```typescript
// After — typed, composable, reusable
await checkoutPage.applyPromoCode("SAVE20");
```
--------------------------------
### Adopt PageObject for Advanced Helpers
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/incremental-adoption.mdx
Shows how to switch an accessor to use the built-in `PageObject` for advanced features like lazy evaluation, visibility waits, and soft assertions. This allows mixing different styles within the same page object.
```typescript
import { Page, Locator, expect } from "@playwright/test";
import { RootSelector, Selector, RootPageObject, PageObject } from "@playwright/test-page-object";
@RootSelector("CheckoutPage")
class CheckoutPage extends RootPageObject {
@Selector("PromoCodeInput")
accessor PromoCode = new PageObject();
}
// Example usage within a test:
test("apply promo with PageObject", async ({ page }) => {
const checkout = new CheckoutPage(page);
// Capture the accessor in a variable — the chain rebuilds lazily on every use.
const promo = checkout.PromoCode;
await promo.waitVisible();
await promo.$.fill("SAVE20");
await promo.expect({ soft: true }).toHaveValue("SAVE20");
});
```
--------------------------------
### Use initializer for PageObject subclasses
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/reference/migration-v1-to-v2.mdx
In v2, `PageObject` subclasses must be instantiated using the initializer form (`= new MyPageObject()`) when used with `@Selector`. Factory arguments are reserved for plain classes.
```diff
- @Selector("PromoCode", PromoPageObject)
- accessor PromoCode!: PromoPageObject;
+ @Selector("PromoCode")
+ accessor PromoCode = new PromoPageObject();
```
--------------------------------
### Manual Page Object Instantiation
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/fixtures.mdx
If your project does not utilize Playwright's fixture system, you can manually instantiate page objects within your tests using the standard `new` keyword, passing the Playwright `page` object directly.
```typescript
test("apply promo code", async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.applyPromoCode("SAVE20");
});
```
--------------------------------
### Lookup methods
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Methods to find a single item within the list based on specific criteria. Returns the first match.
```APIDOC
## Lookup methods (return single item)
Each returns a single item bound to the first match.
| Method | Returns |
|--------|---|
| `getItemByText(text)` | first item containing `text` |
| `getItemByTestId(id)` | first item with `data-testid={id}` (`string \| RegExp`) |
| `getItemByRole(...)` | first item containing the role |
```ts
const apple = list.getItemByText("Apple");
const second = list.getItemByTestId("CartItem_2");
const byPattern = list.getItemByTestId(/^CartItem_/) ;
const removable = list.getItemByRole("button", { name: "Remove" });
```
```
--------------------------------
### Define a Fragment Class
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/fragments.mdx
Define a fragment by creating a class that accepts a `Locator` in its constructor. Use the `@Selector` decorator to define child selectors within the fragment, which will chain from the fragment's locator.
```typescript
import type { Locator } from "@playwright/test";
import { Selector } from "playwright-page-object";
class PromoSection {
constructor(readonly locator: Locator) {}
@Selector("CodeInput")
accessor CodeInput!: Locator;
@Selector("ApplyButton")
accessor ApplyButton!: Locator;
}
```
--------------------------------
### Utilize Wait Helpers
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/page-object.mdx
PageObject provides several built-in wait helpers for common scenarios like waiting for visibility, text, or a specific value. `waitValue` accepts numbers and coerces them to strings, and also accepts RegExp for pattern matching.
```typescript
await control.waitVisible();
await control.waitText("Applied");
await control.waitText(/applied/i);
await control.waitValue(42); // coerced to "42"
```
--------------------------------
### Page-Only Host Resolution
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/page-only-hosts.mdx
Illustrates how a selector defined in a page-only host is resolved. It defaults to scoping under 'page.locator("body")' and then applying the specific test ID.
```plaintext
page.locator("body").getByTestId("PromoCodeInput")
```
--------------------------------
### Type Inference for Page Objects
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/create-fixtures.mdx
Demonstrates how `FixturesFromMap` infers the types of page objects provided to `createFixtures`. This ensures that `test.extend` correctly infers fixture types, such as `homePage: HomePage` and `authPage: AuthPage`.
```typescript
createFixtures({
homePage: HomePage,
// homePage: HomePage
authPage: (page) => new AuthPage(page), // authPage: AuthPage
});
```
--------------------------------
### Accessing List Items via Typed Proxy
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/list-page-object.mdx
Shows how to access individual items within a ListPageObject using numeric indexing, the .at() method, and asynchronous iteration.
```typescript
list.items[0]; // first item, typed as itemType
```
```typescript
list.items.at(-1); // last item
```
```typescript
for await (const item of list.items) { /* ... */ }
```
--------------------------------
### Configure tsconfig.json target to ES2015 or higher
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/reference/troubleshooting.mdx
Update the `target` option in `tsconfig.json` to `ES2015` or a later version to support the `accessor` modifier.
```json
{
"compilerOptions": {
"target": "ES2022"
}
}
```
--------------------------------
### Plain Class with Decorators
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/skills/playwright-page-object/SKILL.md
Use this pattern for incremental adoption of typed selectors without requiring inheritance. It integrates well with existing Playwright patterns.
```typescript
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("Promo")
accessor PromoInput!: Locator;
}
```
--------------------------------
### Fragment (Reusable Sub-tree)
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/skills/playwright-page-object/SKILL.md
Ideal for nested UI components that have their own child selectors and can be shared across different pages. The constructor accepts a `Locator` for its scope.
```typescript
class PromoSection {
constructor(readonly locator: Locator) {}
@Selector("CodeInput")
accessor CodeInput!: Locator;
}
// Used in parent:
@Selector("PromoSection", PromoSection)
accessor promo!: PromoSection;
```
--------------------------------
### External Controls
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/skills/playwright-page-object/SKILL.md
Integrate existing control libraries or custom typed helpers without needing `PageObject` inheritance. The constructor accepts a `Locator` for the control's scope.
```typescript
class InputControl {
constructor(readonly locator: Locator) {}
async fill(value: string) { await this.locator.fill(value); }
}
@Selector("Promo", InputControl)
accessor PromoInput!: InputControl;
```
--------------------------------
### Plain Class Accessor Usage
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/plain-classes.mdx
Demonstrates how to use a plain class accessor in a test. The `accessor` keyword creates a lazy getter, and Playwright `Locator`s are also lazy, allowing reuse of captured accessors.
```typescript
test("retry on flakiness", async ({ page }) => {
const checkout = new CheckoutPage(page);
const input = checkout.PromoCodeInput;
await input.fill("SAVE20");
await expect(input).toHaveValue("SAVE20");
// `input` is a Locator — Playwright re-evaluates it on every action, so it never goes stale.
});
```
--------------------------------
### Detecting Constructor vs. Factory
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/api/create-fixtures.mdx
This code snippet illustrates the logic used by `createFixtures` to determine if an entry is a class constructor or a factory function. ES classes and regular functions are treated as constructors, while arrow functions are detected as factories.
```typescript
const isConstructor =
typeof entry === "function" &&
entry.prototype !== undefined &&
entry.prototype.constructor === entry;
const instance =
isConstructor
? new entry(page)
: entry(page);
```
--------------------------------
### Replace Locator Chain with Accessor
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/guides/incremental-adoption.mdx
Before and after demonstrating how to replace a direct locator chain with a page object accessor. This allows for structured access to elements without altering existing test logic significantly.
```typescript
import { test, expect, Page, Locator } from "@playwright/test";
import { RootSelector, Selector } from "@playwright/test-page-object";
// Before
test("apply promo", async ({ page }) => {
await page.getByTestId("CheckoutPage").getByTestId("PromoCodeInput").fill("SAVE20");
});
// After
@RootSelector("CheckoutPage")
class CheckoutPage {
constructor(readonly page: Page) {}
@Selector("PromoCodeInput")
accessor PromoCodeInput!: Locator;
}
test("apply promo", async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.PromoCodeInput.fill("SAVE20");
});
```
--------------------------------
### Configure TypeScript tsconfig.json
Source: https://github.com/sergeyshmakov/playwright-page-object/blob/main/docs/src/content/docs/getting-started/installation.mdx
Ensure your tsconfig.json targets ES2015 or higher to support the accessor keyword. No experimentalDecorators flag is needed for standard ECMAScript decorators.
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler"
}
}
```