Skip to main content

playwright-page-objects

Why It Exists

playwright-page-objects is one of four Playwright skills (alongside playwright-testid-catalog, playwright-testid-attributes, and playwright-attribute-waits) added in commit 306857c to generalize "a Playwright instrumentation convention found undocumented-but-pervasive in a client engagement's codebase: typed data-testid catalogs, reactive data-*/aria-* state-marker attributes (load-state, epoch-fencing, debounced-search), the canonical wait helpers that consume them, and Page Object structure/selector priority." The same commit redacted client-identifying references (envmgr-ui, @ad-infrastructure) from README.md and the docs/*-RUNBOOK.md files, replacing them with a neutral "a client engagement" phrasing — the skill's structure and invariants were lifted from real production code, not invented from a style guide.

The class-structure/selector-discipline split this skill owns (as opposed to the wait-helper implementations or the testid catalog mechanics) later became the reference pattern other skill families were told to mirror exactly. .omc/plans/appium-rn-testing-skills.md (local research artifact, not tracked in this repo) opens its principles with "mirror the existing playwright-testid-attributes / playwright-testid-catalog / playwright-attribute-waits / playwright-page-objects family's producer/catalog/consumer/structure split exactly," and its appium-page-objects acceptance criteria explicitly copy this skill's own carve-out — that element discovery ("browser-exploration" here, screen-exploration there) is a tool-agnostic concern owned by neither skill — once an Appium-capable MCP server (@wdio/mcp) was confirmed to exist, "matching playwright-page-objects' own carve-out for its discovery step."

What It Does

playwright-page-objects defines the output shape of a Playwright Page Object: a typed façade over one route that owns the locators and the small actions/assertions that touch them, so specs read as intent rather than raw selector plumbing. It fixes the class contract (a shared BasePage with route, constructor(page: Page), and navigate()), the selector priority order, the wait discipline around interactions, the one-file-per-route naming rule, and the boundary between page structure and business/domain flow.

It deliberately does not define how you discover which elements belong in the class in the first place — that stays a tool-agnostic concern — nor the readiness-wait helper implementations a page object's action methods call into (that's playwright-attribute-waits) or the typed data-testid catalog those selectors pull from (that's playwright-testid-catalog).

How To Use It

Triggers on: "new page object", "Page Object Model class", "extend BasePage", "page object structure", "expand a stub page object", "locator selector priority", "role vs testid selector", "verify method pattern", "one page object per route", or "business logic leaking into a page object".

skills add git@github.com:catesandrew/next-starters.git --skill skills/playwright-page-objects -g
npm install @next-starters/skill-playwright-page-objects
/plugin marketplace add catesandrew/next-starters
/plugin install playwright-page-objects@next-starters

Gotchas & Invariants

  • Every page object extends a shared BasePage exposing route, a constructor(page: Page), and navigate(). Locators are get accessors (page.searchInput, not page.searchInput()), and verify* assertion methods assert internally with expect and return void rather than handing a boolean back to the caller.
  • Selector priority ladder: role-based first (getByRole() with an accessible name), then getByLabel()/getByText(), then getByTestId() pulled from the typed catalog, then a CSS class selector last. A role/label-based selector doubles as a live accessibility check — if it can't find the element by role and accessible name, that's often a real accessibility bug, not just test flakiness. data-testid finds an element regardless of whether it's exposed to assistive tech, so it papers over a missing aria-label if used as the first choice.
  • Action methods call a named readiness-wait helper from playwright-attribute-waits around every interaction, never a bare waitForTimeout(). If a raw waitForTimeout() is genuinely unavoidable, it must carry an inline comment justifying the exact millisecond budget (e.g. // 400ms matches the search debounce window).
  • One page-object file per route, with the file path and class name derived mechanically from the route — /items/list becomes ItemsListPage in a file that mirrors the segment path — and grouped to match the target app's own routing/feature directory structure.
  • Page objects encode structure, not business/domain flow. Multi-step flows ("sign up, then verify, then onboard") compose one or more page objects at a higher-level test/flow layer instead of living inside a single page object's methods.
  • A stub page object containing only route and a title/heading locator is a legitimate first commit. Expanding it with real locators, actions, and assertions must be purely additive — it must never break or remove the stub's original public route/title contract, so specs already written against the stub keep passing unmodified.
  • playwright-attribute-waits — supplies the readiness-wait helpers this skill's action methods call into but never redefine.
  • playwright-testid-attributes — instruments components with the state-marker attributes this skill's selectors and waits ultimately depend on.
  • playwright-testid-catalog — owns the typed data-testid registry this skill's selector-priority ladder falls back to when no reliable role/label exists.

Sourced from: skills/playwright-page-objects/metadata.json, skills/playwright-page-objects/SKILL.md, .omc/plans/appium-rn-testing-skills.md (local research artifact, not tracked in this repo), git log --follow -- skills/playwright-page-objects (commit 306857c)