# Pw Page Object Builder

> Builds a Playwright Page Object Model class from a page or URL. Use when an SDET says "make a page object for the login page", "build a POM for the dashboard", "extract locators into a page class", or wants to refactor inline selectors into a reusable class. Produces locators-as-methods (getByTestId/ getByRole), action methods, and a static PATH — a draft to review.

- Skill: `pramoddutta/pw-page-object-builder-3` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pramoddutta/pw-page-object-builder-3`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pramoddutta/pw-page-object-builder-3/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: PramodDutta (https://skillmd.com/u/pramoddutta)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/pramoddutta/pw-page-object-builder-3

---


# PW Page Object Builder

Builds a Page Object for **this** framework. Every POM here extends `BasePage`; none of them
build locators in the base class.

## Read first
`src/pages/LoginPage.ts` is the reference implementation. `src/pages/BasePage.ts` is the contract.
Match them rather than a generic Playwright POM.

## Contract
1. `extends BasePage`, constructor calls `super(page, '<ClassName>')` so the logger is scoped.
2. `static readonly PATH` for the route. Navigate with `this.goto(PATH)`, never `page.goto` with an absolute URL, so `baseURL` from `playwright.config.ts` still applies.
3. Locators are `private readonly` fields assigned in the constructor. This app is `data-test` driven, so prefer `[data-test="..."]`. Expose behaviour, never the Locator.
4. All interaction goes through `this.el.*` (`UtilElementLocator`), never `locator.click()` directly. That wrapper is what puts every action in the log.
5. Dynamic locators become private helper methods, e.g. `addBtn(id)`, not public fields.
6. Provide `assertLoaded()` using web-first assertions.

## Output shape
```typescript
import { expect, Locator, Page } from '@playwright/test';
import { BasePage } from './BasePage';

export class InventoryPage extends BasePage {
    static readonly PATH = '/playwright/ttacart/inventory.html';

    private readonly title: Locator;
    private readonly cartLink: Locator;

    constructor(page: Page) {
        super(page, 'InventoryPage');
        this.title = page.locator('[data-test="title"]');
        this.cartLink = page.locator('[data-test="shopping-cart-link"]');
    }

    async open(): Promise<void> {
        await this.goto(InventoryPage.PATH);
        await this.assertLoaded();
    }

    async assertLoaded(): Promise<void> {
        await expect(this.title).toHaveText('Products');
    }

    private addBtn(id: string): Locator {
        return this.page.locator(`[data-test="add-to-cart-${id}"]`);
    }

    async addToCart(id: string): Promise<void> {
        await this.el.click(this.addBtn(id));
    }
}
```

## Then wire it up
A new POM is only half done. Add a fixture for it in `src/fixtures/test-base.ts`: a field on the
`TestFixture` type and a `base.extend` entry that constructs it against `page`. Specs must get
page objects from the fixture, never with `new`.

## Verify
```bash
npx tsc --noEmit -p tsconfig.json
```

