### 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" } } ```