{"id":46510397,"url":"https://github.com/sergeyshmakov/playwright-page-object","last_synced_at":"2026-04-01T17:25:50.339Z","repository":{"id":342146286,"uuid":"1173008658","full_name":"sergeyshmakov/playwright-page-object","owner":"sergeyshmakov","description":"TypeScript Page Object Model for Playwright — decorator-driven, typed, composable controls instead of raw locator strings.","archived":false,"fork":false,"pushed_at":"2026-03-21T10:54:51.000Z","size":591,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-22T00:16:20.394Z","etag":null,"topics":["decorators","e2e","page-object-framework","page-object-model","page-object-pattern","playwright","playwright-test","playwright-typescript","pom","testing"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/playwright-page-object","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sergeyshmakov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-04T23:15:20.000Z","updated_at":"2026-03-21T12:23:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sergeyshmakov/playwright-page-object","commit_stats":null,"previous_names":["sergeyshmakov/playwright-page-object"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/sergeyshmakov/playwright-page-object","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergeyshmakov%2Fplaywright-page-object","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergeyshmakov%2Fplaywright-page-object/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergeyshmakov%2Fplaywright-page-object/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergeyshmakov%2Fplaywright-page-object/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sergeyshmakov","download_url":"https://codeload.github.com/sergeyshmakov/playwright-page-object/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sergeyshmakov%2Fplaywright-page-object/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290537,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["decorators","e2e","page-object-framework","page-object-model","page-object-pattern","playwright","playwright-test","playwright-typescript","pom","testing"],"created_at":"2026-03-06T16:04:38.557Z","updated_at":"2026-04-01T17:25:50.332Z","avatar_url":"https://github.com/sergeyshmakov.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# playwright-page-object\n\nTyped, decorator-driven selector composition for Playwright. Keep selectors close to your page objects without forcing a single Page Object Model style.\n\n[![npm version](https://badge.fury.io/js/playwright-page-object.svg)](https://badge.fury.io/js/playwright-page-object)\n[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)\n\n## Problem\n\nPlaywright locators are powerful, but selector logic often leaks into tests:\n\n- Selector strings get duplicated across files\n- Long locator chains obscure the actual UI structure\n- Reusable UI parts become scattered ad-hoc helpers\n- Adopting a structured Page Object Model feels like an all-or-nothing rewrite\n\n## Solution\n\n`playwright-page-object` provides an incremental path forward:\n\n- **Root decorators** scope a class to a top-level locator\n- **Child decorators** resolve selectors relative to that scope\n- **Lazy chains** rebuild only when accessed\n- **Multiple output styles** support your existing patterns\n\nUse it with plain classes, custom controls, or built-in `PageObject` helpers—no breaking changes required.\n\n## Installation\n\n```bash\nnpm install -D playwright-page-object\n```\n\n**Requirements:**\n- Node `\u003e=20`\n- `@playwright/test \u003e=1.35.0`\n- TypeScript `\u003e=5.0` (when using decorators + accessors)\n\nEnsure your `tsconfig.json` targets `\"ES2015\"` or higher for ECMAScript accessor support.\n\n## Quick Start\n\nNo need to extend built-in classes—start with plain classes:\n\n```ts\nimport type { Locator, Page } from \"@playwright/test\";\nimport { RootSelector, Selector, SelectorByRole } from \"playwright-page-object\";\n\nclass ButtonControl {\n\tconstructor(readonly locator: Locator) {}\n}\n\n@RootSelector(\"CheckoutPage\")\nclass CheckoutPage {\n\tconstructor(readonly page: Page) {}\n\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoCodeInput!: Locator;\n\n\t@SelectorByRole(\"button\", { name: \"Apply\" }, ButtonControl)\n\taccessor ApplyPromoButton!: ButtonControl;\n\n\tasync applyPromoCode(code: string) {\n\t\tawait this.PromoCodeInput.fill(code);\n\t\tawait this.ApplyPromoButton.locator.click();\n\t}\n}\n```\n\n```ts\nimport { test } from \"@playwright/test\";\n\ntest(\"apply promo code\", async ({ page }) =\u003e {\n\tconst checkout = new CheckoutPage(page);\n\tawait checkout.applyPromoCode(\"SAVE20\");\n});\n```\n\n### Using page-only hosts (no root decorator)\n\nSkip `@RootSelector` when your `data-testid` values are globally unique:\n\n```ts\nimport type { Locator, Page } from \"@playwright/test\";\nimport { Selector, SelectorByRole } from \"playwright-page-object\";\n\nclass ButtonControl {\n\tconstructor(readonly locator: Locator) {}\n}\n\nclass CheckoutPage {\n\tconstructor(readonly page: Page) {}\n\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoCodeInput!: Locator;\n\n\t@SelectorByRole(\"button\", { name: \"Apply\" }, ButtonControl)\n\taccessor ApplyPromoButton!: ButtonControl;\n}\n```\n\nSee [PlainHostCheckoutPage.ts](example/e2e/page-objects/PlainHostCheckoutPage.ts) and [PromoSectionFragment.ts](example/e2e/page-objects/PromoSectionFragment.ts) for a fuller example.\n\n### Composing fragments with `@Selector` + `this.locator`\n\nPass a class to `@Selector(...)` to create reusable fragments. If that class exposes a `locator` property, nested decorators chain under that element:\n\n```ts\nimport type { Locator, Page } from \"@playwright/test\";\nimport { Selector } from \"playwright-page-object\";\n\nclass PromoSection {\n\tconstructor(readonly locator: Locator) {}\n\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoInput!: Locator;\n}\n\nclass CheckoutPage {\n\tconstructor(readonly page: Page) {}\n\n\t@Selector(\"PromoSection\", PromoSection)\n\taccessor promo!: PromoSection;\n}\n```\n\n## How It Works\n\n### Root decorators set locator scope\n\n**Scoped root** — establishes a container-level context:\n\n```ts\n@RootSelector(\"CheckoutPage\")\nclass CheckoutPage {\n\tconstructor(readonly page: Page) {}\n\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoCodeInput!: Locator;\n}\n\n// Resolves to: page.locator(\"body\").getByTestId(\"CheckoutPage\").getByTestId(\"PromoCodeInput\")\n```\n\n**Page-only host** — chains from body without a scoped container:\n\n```ts\nclass CheckoutPage {\n\tconstructor(readonly page: Page) {}\n\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoCodeInput!: Locator;\n}\n\n// Resolves to: page.locator(\"body\").getByTestId(\"PromoCodeInput\")\n```\n\nUse a scoped root when relying on a container test id; use page-only when globally unique ids suffice.\n\n### Child decorators resolve from context\n\nChild decorators resolve in this order:\n\n1. Decorator-managed locator context (from `@RootSelector`, `PageObject`, etc.)\n2. `Locator`-like `locator` property (fragments)\n3. Playwright `page` property → `page.locator(\"body\")`\n4. Error if none match\n\nIf both `locator` and `page` exist, `locator` wins (element scope \u003e page scope).\n\n### Accessor type determines output shape\n\n- `Locator` — raw Playwright locator\n- **Custom class** — decorator passes resolved locator to that constructor\n- `PageObject` / `ListPageObject` — use built-in helpers\n\n## Output Styles\n\n### 1. Raw Locator (minimal abstraction)\n\n```ts\n@RootSelector(\"CheckoutPage\")\nclass CheckoutPage {\n\tconstructor(readonly page: Page) {}\n\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoCodeInput!: Locator;\n}\n\nawait checkoutPage.PromoCodeInput.fill(\"SAVE20\");\n```\n\nDrop `@RootSelector` if body scope suffices.\n\n### 2. Custom Controls (reusable abstractions)\n\n```ts\nclass ExternalInputControl {\n\tconstructor(readonly locator: Locator) {}\n\tfill(value: string) {\n\t\treturn this.locator.fill(value);\n\t}\n}\n\n@RootSelector(\"CheckoutPage\")\nclass CheckoutPage {\n\tconstructor(readonly page: Page) {}\n\n\t@Selector(\"PromoCodeInput\", ExternalInputControl)\n\taccessor PromoCode!: ExternalInputControl;\n}\n\nawait checkoutPage.PromoCode.fill(\"SAVE20\");\n```\n\n### 3. Built-in POM Layer (batteries included)\n\n```ts\nimport {\n\tListPageObject,\n\tListSelector,\n\tPageObject,\n\tRootPageObject,\n\tRootSelector,\n\tSelector,\n\tSelectorByRole,\n} from \"playwright-page-object\";\n\nclass CartItemControl extends PageObject {\n\t@SelectorByRole(\"button\", { name: \"Remove\" })\n\taccessor RemoveButton = new PageObject();\n}\n\n@RootSelector(\"CheckoutPage\")\nclass CheckoutPage extends RootPageObject {\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoCode = new PageObject();\n\n\t@SelectorByRole(\"button\", { name: \"Apply\" })\n\taccessor ApplyPromoButton = new PageObject();\n\n\t@ListSelector(\"CartItem_\")\n\taccessor CartItems = new ListPageObject(CartItemControl);\n}\n\nawait checkoutPage.PromoCode.$.fill(\"SAVE20\");\nawait checkoutPage.expect().toBeVisible();\nawait checkoutPage.CartItems.waitCount(3);\n```\n\nStyles coexist in the same class—mix as needed.\n\n## Built-In Classes\n\n### `RootPageObject`\n\nUse for root-decorated classes instantiated directly from `page`:\n\n```ts\n@RootSelector(\"CheckoutPage\")\nclass CheckoutPage extends RootPageObject {}\n\nconst checkout = new CheckoutPage(page);\n```\n\n### `PageObject`\n\nUse for nested controls. Provides:\n\n| Feature | Example |\n|---------|---------|\n| Raw locator | `await control.$.click()` |\n| Assertions | `await control.expect().toBeVisible()` |\n| Wait helpers | `await control.waitVisible()` |\n| Page access | `control.page()` |\n\n**Wait methods:**\n\n| Method | Purpose |\n|--------|---------|\n| `.waitVisible()` / `.waitHidden()` | Wait for visibility |\n| `.waitText(text)` | Wait for exact text |\n| `.waitValue(value)` / `.waitNoValue()` | Wait for input value |\n| `.waitCount(count)` | Wait for element count |\n| `.waitChecked()` / `.waitUnChecked()` | Wait for checkbox state |\n| `.waitProp(name, value)` | Wait for React/Vue data prop |\n| `.waitPropAbsence(name)` | Wait for data prop absence |\n\n**Assertion methods:**\n\n```ts\nawait control.expect().toBeVisible();\nawait control.expect({ soft: true }).toHaveText(\"Click me\");\nawait control.expect({ message: \"Button is enabled\" }).toBeEnabled();\n```\n\n### `ListPageObject`\n\nUse for repeated child controls:\n\n```ts\n@ListSelector(\"CartItem_\")\naccessor CartItems = new ListPageObject(CartItemControl);\n```\n\n**Common APIs:**\n\n```ts\nlist.items[0]                          // First item\nlist.items.at(-1)                      // Last item\nfor await (const item of list.items) {} // Iterate\nawait list.count()                     // Item count\nlist.first()                           // First item (PageObject)\nlist.last()                            // Last item (PageObject)\nlist.filterByText(\"Apple\")             // Narrowed ListPageObject\nlist.filterByTestId(\"CartItem_2\")      // Narrow by item test id\nlist.getItemByText(\"Apple\")            // First matching item\nlist.getItemByTestId(\"CartItem_2\")     // First item by item test id\nlist.getItemByRole(\"button\", { name: \"Remove\" }) // First item containing that role\n```\n\n`filter...` methods return a narrowed `ListPageObject`, so you can continue with `.first()`, `.count()`, `.getAll()`, or `for await...of`. `getItemBy...` methods return a single item. Use `filterByHasTestId()` when you want Playwright `has`-style matching for rows containing a child with a given test id instead of matching the row's own test id.\n\n**For Locator-based lists** (multi-element without helpers):\n\n```ts\n@ListSelector(\"CartItem_\")\naccessor CartItemRows!: Locator;\n\n// Use: cartPage.CartItemRows.nth(0), expect().toHaveCount()\n```\n\nPrefer declarative row ids (`CartItem_1`, `CartItem_2`) to keep selectors readable.\n\n## Fixtures\n\n`createFixtures()` works with classes constructible via `new Class(page)`:\n\n```ts\nimport { test as base } from \"@playwright/test\";\nimport { createFixtures } from \"playwright-page-object\";\nimport { CheckoutPage } from \"./page-objects/CheckoutPage\";\n\nexport const test = base.extend\u003c{ checkoutPage: CheckoutPage }\u003e(\n\tcreateFixtures({\n\t\tcheckoutPage: CheckoutPage,\n\t}),\n);\n```\n\nFor classes with custom constructor arguments, create them in your own fixture.\n\n## Decorator Reference\n\n### Root Decorators\n\n| Decorator | Playwright API |\n|-----------|----------------|\n| `@RootSelector(id)` | `getByTestId(id)` |\n| `@RootSelector()` | `page.locator(\"body\")` |\n| `@ListRootSelector(id)` | `getByTestId(new RegExp(id))` |\n| `@RootSelectorByRole(...)` | `getByRole(...)` |\n| `@RootSelectorByText(text)` | `getByText(text)` |\n| `@RootSelectorByLabel(...)` | `getByLabel(...)` |\n| `@RootSelectorByPlaceholder(...)` | `getByPlaceholder(...)` |\n| `@RootSelectorByAltText(...)` | `getByAltText(...)` |\n| `@RootSelectorByTitle(...)` | `getByTitle(...)` |\n\n### Child Decorators\n\n| Decorator | Playwright API |\n|-----------|----------------|\n| `@Selector(id)` | `getByTestId(id)` |\n| `@Selector()` | Identity (no chaining) |\n| `@ListSelector(id)` | `getByTestId(new RegExp(id))` |\n| `@ListStrictSelector(id)` | `getByTestId(id)` (strict) |\n| `@SelectorByRole(...)` | `getByRole(...)` |\n| `@SelectorByText(text)` | `getByText(text)` |\n| `@SelectorByLabel(...)` | `getByLabel(...)` |\n| `@SelectorByPlaceholder(...)` | `getByPlaceholder(...)` |\n| `@SelectorByAltText(...)` | `getByAltText(...)` |\n| `@SelectorByTitle(...)` | `getByTitle(...)` |\n| `@SelectorBy(fn)` | Custom locator logic |\n\nEach can return raw `Locator`, custom controls, `PageObject`, or `ListPageObject`.\n\n## Incremental Adoption\n\n### Step 1: Add decorators, keep Locator output\n\n```ts\n@RootSelector(\"CheckoutPage\")\nclass CheckoutPage {\n\tconstructor(readonly page: Page) {}\n\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoCodeInput!: Locator;\n}\n```\n\n### Step 2: Extract reusable controls\n\n```ts\n@Selector(\"PromoCodeInput\", ExternalInputControl)\naccessor PromoCode!: ExternalInputControl;\n```\n\n### Step 3: Adopt PageObject where helpful\n\n```ts\nclass CheckoutPage extends RootPageObject {\n\t@Selector(\"PromoCodeInput\")\n\taccessor PromoCode = new PageObject();\n}\n```\n\nMix all three approaches in the same suite.\n\n## AI \u0026 Assistant Support\n\nThis package is available in [Context7](https://context7.com/) MCP, so AI assistants can load it directly into context when working with your object property paths.\n\nA [Cubic wiki](https://www.cubic.dev/wikis/sergeyshmakov/playwright-page-object) provides AI-ready documentation for this project.\n\nIt also ships an [Agent Skills](https://agentskills.io/) – compatible skill. Install it so your AI assistant loads data-path guidance:\n\n```bash\nnpx ctx7 skills install /sergeyshmakov/playwright-page-object playwright-page-object\n```\n\nThe skill lives in [skills/playwright-page-object/SKILL.md](skills/playwright-page-object/SKILL.md).\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.\n\n## License\n\n[ISC License](LICENSE)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsergeyshmakov%2Fplaywright-page-object","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsergeyshmakov%2Fplaywright-page-object","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsergeyshmakov%2Fplaywright-page-object/lists"}