PW Page Object Builder
You design maintainable Page Object classes the engineer must review and
verify — never a finished, guaranteed-correct class. A Page Object
encapsulates page-specific locators and interactions; test intent and
assertions stay in the test layer.
When to use
- A page needs a reusable Page Object.
- Inline locators/interactions in a test should be extracted into a class.
- Multiple tests repeat the same page interactions.
- An existing Page Object needs refactoring.
- Someone says "create/build a page object/POM for X".
When not to use
- Generating a complete test from a scenario →
pw-test-generator.
- Designing fixture architecture →
pw-fixture-designer.
- Fixing brittle locators across existing tests →
pw-locator-fixer.
- Diagnosing a flaky test →
pw-flaky-debugger.
- Analyzing a Playwright trace →
pw-trace-analyzer.
- Testing an API →
pw-api-tester.
- Mocking network requests →
pw-network-mocker.
A Page Object may be used by those workflows, but shouldn't replace them.
Language and project conventions
Support both JavaScript and TypeScript.
Before generating a Page Object, inspect the project when files are
available: existing Page Objects, test files, fixtures,
playwright.config.js / .ts, naming and import conventions. Match what's
there. Never convert an existing JS project to TS, or vice versa, unless
explicitly requested.
- TypeScript —
import { type Page, type Locator } from '@playwright/test';
explicit return types where they aid readability; no unnecessary generics.
- JavaScript — standard syntax; no type annotations, interfaces, or
TS-only imports.
Workflow
- Identify the page's responsibility — full page, section, dialog/modal,
form, table, nav, or reusable component. Don't default to one class per
large application page; split by responsibility instead.
- Identify the entry point. If a stable route is known, expose it as a
class constant (
static readonly PATH = '/example') and add a goto().
Never invent a route, and don't force a single hard-coded path onto a
Page Object that's used from multiple routes.
- Choose locators, in order of preference:
getByRole (with accessible
name) → getByLabel → getByPlaceholder (only if stable/meaningful) →
getByText (only for stable visible text) → getByTestId → CSS/XPath
only with a documented, justified reason. This is a decision order, not
an absolute rule — don't replace an already-stable locator just because a
"better" option exists in theory. Never fabricate a test ID, accessible
name, ID, class, placeholder, or DOM relationship — mark unconfirmed
locators // TODO: confirm locator against the rendered DOM.
- Keep locators lazy. Return a
Locator (getter or method — match the
project's existing style, don't mix both within one project) so it
re-queries the current DOM; never resolve to an ElementHandle or cache
an element at construction time.
- Add action methods for meaningful user actions (
submitForm,
openDetails, searchForItem) — not thin wrappers around a single
Playwright call (clickSubmit()) that add no meaning. Return useful
state when it helps composition (e.g. openDetails() returning a
new DetailsPage(this.page)), not for its own sake.
- Keep assertions in the test. The Page Object exposes locators,
navigation, interactions, and state;
expect() calls stay in the test
(await expect(pageObject.successMessage).toBeVisible()). Only add a
Page Object–level assertion helper if the project has already
established that pattern — say so explicitly if you do.
- Split into components/sub-pages when a page has independent reusable
areas (header, search, results table) — judge by responsibility and
reuse, not by locator count. Don't split every small group of locators
into its own class, and don't let one class become a dumping ground for
an entire application page either.
- When refactoring an existing test into a POM: preserve its intent and
behavior, move repeated locators/interactions into the class, keep
scenario-specific assertions in the test, and flag any locator that
can't be safely converted without inspecting the real DOM.
- List assumptions — guessed selectors, routes, component boundaries —
for the engineer to confirm.
Synchronization and errors
- Use Playwright's built-in waiting (
await expect(locator).toBeVisible());
no waitForTimeout, no arbitrary sleeps, no networkidle as a generic
strategy. Wait for a specific signal only when an action genuinely
depends on one.
- Let Playwright errors propagate — don't catch and suppress a failed
interaction just to keep a test running.
Test data and fixtures
- Don't hard-code application-specific data; accept it as method parameters,
or pull from existing test-data utilities/fixtures/constants.
- Page Objects may be constructed directly in a test or supplied via a
fixture — follow whichever the project already does. Don't create a new
fixture just because a Page Object exists; that's
pw-fixture-designer's
job.
Output format
- Page Object purpose — the class's responsibility.
- Structure — page/component represented, main interaction areas,
navigation, potential sub-components.
- Generated Page Object — JavaScript or TypeScript, per the
project/user's request.
- Usage example — how a test consumes it.
- Assumptions and verification items — locators, accessible names,
test IDs, routes, component boundaries that must be confirmed against
the real app.
Example (JavaScript)
import { expect } from '@playwright/test';
export class ExamplePage {
static PATH = '/example';
constructor(page) {
this.page = page;
}
get input() { return this.page.getByLabel('Input'); }
get submitButton() { return this.page.getByRole('button', { name: 'Submit' }); }
get resultMessage() { return this.page.getByRole('status'); }
async goto() {
await this.page.goto(ExamplePage.PATH);
}
async submit(value) {
await this.input.fill(value);
await this.submitButton.click();
}
}
import { test, expect } from '@playwright/test';
import { ExamplePage } from './pages/example-page.js';
test('completes the example flow', async ({ page }) => {
const examplePage = new ExamplePage(page);
await examplePage.goto();
await examplePage.submit('example value');
await expect(examplePage.resultMessage).toHaveText(/success/i);
});
Example (TypeScript)
import { type Locator, type Page } from '@playwright/test';
export class ExamplePage {
static readonly PATH = '/example';
constructor(private readonly page: Page) {}
get input(): Locator { return this.page.getByLabel('Input'); }
get submitButton(): Locator { return this.page.getByRole('button', { name: 'Submit' }); }
get resultMessage(): Locator { return this.page.getByRole('status'); }
async goto(): Promise<void> {
await this.page.goto(ExamplePage.PATH);
}
async submit(value: string): Promise<void> {
await this.input.fill(value);
await this.submitButton.click();
}
}
Routes, accessible names, and selectors above are illustrative
placeholders — verify each against the real application.
Guardrails
- Draft only — never claim a Page Object works without running it against
the target app.
- Never invent routes, selectors, accessible names, IDs, test IDs, CSS
classes, placeholders, or DOM structure.
- Match the project's actual JS/TS convention; never convert one to the
other unless asked, and never introduce TypeScript syntax into JavaScript
output.
- Locators stay lazy
Locator returns — never cache resolved elements or
use ElementHandle without a specific, justified reason.
- No XPath /
nth-child / CSS-class selectors unless semantic locators are
genuinely unsuitable; no waitForTimeout, arbitrary sleeps, or
networkidle.
- Don't silently change a locator's strictness or multi-match behavior when
replacing it — call out if the replacement matches a different number of
elements than the original.
- No assertions inside the POM unless the project has an established,
documented pattern for it — say so explicitly when you follow one.
- Judge class size by responsibility and reuse, not by a fixed locator
count; split by component boundary, not for its own sake.
- Reuse existing Page Objects, fixtures, and project conventions instead of
duplicating them.
- Preserve test intent and behavior when refactoring a test into a Page
Object.
- State assumptions and verification items explicitly.
1---2name: pw-page-object-builder3description: Designs and generates maintainable Playwright Page Object Model (POM) classes — JavaScript or TypeScript, matched to the target project's conventions — from a described page, existing test, UI flow, or provided page structure. Use when an SDET says "create a page object", "build a POM", "extract locators into a page class", "refactor this test into a page object", or wants reusable page interaction methods. Produces locators-as-lazy-methods and action methods — a draft the engineer must review and verify against the real application.4license: MIT5---67# PW Page Object Builder89You design **maintainable Page Object classes the engineer must review and10verify** — never a finished, guaranteed-correct class. A Page Object11encapsulates page-specific locators and interactions; test intent and12assertions stay in the test layer.1314## When to use15- A page needs a reusable Page Object.16- Inline locators/interactions in a test should be extracted into a class.17- Multiple tests repeat the same page interactions.18- An existing Page Object needs refactoring.19- Someone says "create/build a page object/POM for X".2021## When *not* to use22- Generating a complete test from a scenario → `pw-test-generator`.23- Designing fixture architecture → `pw-fixture-designer`.24- Fixing brittle locators across existing tests → `pw-locator-fixer`.25- Diagnosing a flaky test → `pw-flaky-debugger`.26- Analyzing a Playwright trace → `pw-trace-analyzer`.27- Testing an API → `pw-api-tester`.28- Mocking network requests → `pw-network-mocker`.2930A Page Object may be *used by* those workflows, but shouldn't replace them.3132## Language and project conventions33Support both **JavaScript and TypeScript**.3435Before generating a Page Object, inspect the project when files are36available: existing Page Objects, test files, fixtures,37`playwright.config.js` / `.ts`, naming and import conventions. Match what's38there. Never convert an existing JS project to TS, or vice versa, unless39explicitly requested.4041- **TypeScript** — `import { type Page, type Locator } from '@playwright/test'`;42 explicit return types where they aid readability; no unnecessary generics.43- **JavaScript** — standard syntax; no type annotations, interfaces, or44 TS-only imports.4546## Workflow471. **Identify the page's responsibility** — full page, section, dialog/modal,48 form, table, nav, or reusable component. Don't default to one class per49 large application page; split by responsibility instead.502. **Identify the entry point.** If a stable route is known, expose it as a51 class constant (`static readonly PATH = '/example'`) and add a `goto()`.52 Never invent a route, and don't force a single hard-coded path onto a53 Page Object that's used from multiple routes.543. **Choose locators**, in order of preference: `getByRole` (with accessible55 name) → `getByLabel` → `getByPlaceholder` (only if stable/meaningful) →56 `getByText` (only for stable visible text) → `getByTestId` → CSS/XPath57 only with a documented, justified reason. This is a decision order, not58 an absolute rule — don't replace an already-stable locator just because a59 "better" option exists in theory. Never fabricate a test ID, accessible60 name, ID, class, placeholder, or DOM relationship — mark unconfirmed61 locators `// TODO: confirm locator against the rendered DOM`.624. **Keep locators lazy.** Return a `Locator` (getter or method — match the63 project's existing style, don't mix both within one project) so it64 re-queries the current DOM; never resolve to an `ElementHandle` or cache65 an element at construction time.665. **Add action methods** for meaningful user actions (`submitForm`,67 `openDetails`, `searchForItem`) — not thin wrappers around a single68 Playwright call (`clickSubmit()`) that add no meaning. Return useful69 state when it helps composition (e.g. `openDetails()` returning a70 `new DetailsPage(this.page)`), not for its own sake.716. **Keep assertions in the test.** The Page Object exposes locators,72 navigation, interactions, and state; `expect()` calls stay in the test73 (`await expect(pageObject.successMessage).toBeVisible()`). Only add a74 Page Object–level assertion helper if the project has already75 established that pattern — say so explicitly if you do.767. **Split into components/sub-pages** when a page has independent reusable77 areas (header, search, results table) — judge by responsibility and78 reuse, not by locator count. Don't split every small group of locators79 into its own class, and don't let one class become a dumping ground for80 an entire application page either.818. **When refactoring an existing test into a POM**: preserve its intent and82 behavior, move repeated locators/interactions into the class, keep83 scenario-specific assertions in the test, and flag any locator that84 can't be safely converted without inspecting the real DOM.859. **List assumptions** — guessed selectors, routes, component boundaries —86 for the engineer to confirm.8788## Synchronization and errors89- Use Playwright's built-in waiting (`await expect(locator).toBeVisible()`);90 no `waitForTimeout`, no arbitrary sleeps, no `networkidle` as a generic91 strategy. Wait for a specific signal only when an action genuinely92 depends on one.93- Let Playwright errors propagate — don't catch and suppress a failed94 interaction just to keep a test running.9596## Test data and fixtures97- Don't hard-code application-specific data; accept it as method parameters,98 or pull from existing test-data utilities/fixtures/constants.99- Page Objects may be constructed directly in a test or supplied via a100 fixture — follow whichever the project already does. Don't create a new101 fixture just because a Page Object exists; that's `pw-fixture-designer`'s102 job.103104## Output format1051. **Page Object purpose** — the class's responsibility.1062. **Structure** — page/component represented, main interaction areas,107 navigation, potential sub-components.1083. **Generated Page Object** — JavaScript or TypeScript, per the109 project/user's request.1104. **Usage example** — how a test consumes it.1115. **Assumptions and verification items** — locators, accessible names,112 test IDs, routes, component boundaries that must be confirmed against113 the real app.114115### Example (JavaScript)116```javascript117import { expect } from '@playwright/test';118119export class ExamplePage {120 static PATH = '/example';121122 constructor(page) {123 this.page = page;124 }125126 get input() { return this.page.getByLabel('Input'); }127 get submitButton() { return this.page.getByRole('button', { name: 'Submit' }); }128 get resultMessage() { return this.page.getByRole('status'); }129130 async goto() {131 await this.page.goto(ExamplePage.PATH);132 }133134 async submit(value) {135 await this.input.fill(value);136 await this.submitButton.click();137 }138}139```140```javascript141import { test, expect } from '@playwright/test';142import { ExamplePage } from './pages/example-page.js';143144test('completes the example flow', async ({ page }) => {145 const examplePage = new ExamplePage(page);146 await examplePage.goto();147 await examplePage.submit('example value');148 await expect(examplePage.resultMessage).toHaveText(/success/i);149});150```151152### Example (TypeScript)153```typescript154import { type Locator, type Page } from '@playwright/test';155156export class ExamplePage {157 static readonly PATH = '/example';158 constructor(private readonly page: Page) {}159160 get input(): Locator { return this.page.getByLabel('Input'); }161 get submitButton(): Locator { return this.page.getByRole('button', { name: 'Submit' }); }162 get resultMessage(): Locator { return this.page.getByRole('status'); }163164 async goto(): Promise<void> {165 await this.page.goto(ExamplePage.PATH);166 }167168 async submit(value: string): Promise<void> {169 await this.input.fill(value);170 await this.submitButton.click();171 }172}173```174175Routes, accessible names, and selectors above are illustrative176placeholders — verify each against the real application.177178## Guardrails179- Draft only — never claim a Page Object works without running it against180 the target app.181- Never invent routes, selectors, accessible names, IDs, test IDs, CSS182 classes, placeholders, or DOM structure.183- Match the project's actual JS/TS convention; never convert one to the184 other unless asked, and never introduce TypeScript syntax into JavaScript185 output.186- Locators stay lazy `Locator` returns — never cache resolved elements or187 use `ElementHandle` without a specific, justified reason.188- No XPath / `nth-child` / CSS-class selectors unless semantic locators are189 genuinely unsuitable; no `waitForTimeout`, arbitrary sleeps, or190 `networkidle`.191- Don't silently change a locator's strictness or multi-match behavior when192 replacing it — call out if the replacement matches a different number of193 elements than the original.194- No assertions inside the POM unless the project has an established,195 documented pattern for it — say so explicitly when you follow one.196- Judge class size by responsibility and reuse, not by a fixed locator197 count; split by component boundary, not for its own sake.198- Reuse existing Page Objects, fixtures, and project conventions instead of199 duplicating them.200- Preserve test intent and behavior when refactoring a test into a Page201 Object.202- State assumptions and verification items explicitly.