| applyTo | **/pages/**/*.ts |
|---|---|
| description | Page Object Model conventions for Playwright + TypeScript: structure, locators, actions, waits, and composition rules. |
Create stable, readable, and reusable automation primitives:
- tests express user intent
- page objects hide UI details
- failures are diagnostic (good errors, good waits, good locators)
- Page Objects expose intent-level methods (e.g.
loginAs(user),addProductToCart(name)), not low-levelclick()chains. - Keep locators private. Expose actions + key domain assertions only.
- Prefer a thin Page Object layer + optional Component Objects for reusable widgets (nav, modal, table).
- Do NOT put test data generation, API clients, or environment config inside Page Objects (use fixtures/helpers).
- Accept
Page(and optionallybaseURL) via constructor. - Store Locator objects, not ElementHandles (Locators are resilient to re-renders).
- Avoid storing state that can go stale across navigations (urls, text snapshots, counts). Read it when needed.
- Prefer user-facing locators:
getByRole,getByLabel,getByPlaceholder,getByText(carefully),getByTestId(for stable hooks).
- Avoid brittle selectors: long CSS chains, nth-child, layout-driven selectors, random classes.
- Use locator narrowing/chaining instead of complex CSS (e.g.
page.getByRole(...).getByText(...)). - Locators must be strict and unambiguous (1 target). If not, narrow by role/name/filter.
- NEVER use
page.waitForTimeout()as synchronization. - Use Playwright’s auto-wait + explicit
expect(...)waits for readiness/visibility. - Any method that triggers navigation must wait for completion:
- prefer sequential
await locator.click(); await page.waitForURL(...);-waitForURLaccounts for navigations that already started, so noPromise.allrace is needed - or assert the new page’s stable element.
- (the legacy
Promise.all([...])idiom is only needed forwaitForEvent('popup')-style races.)
- prefer sequential
- Prefer assertions that validate user-visible outcomes (URL, heading, toast, table row).
- Keep minimal, meaningful assertions inside Page Objects (e.g.
expectLoggedInAs(name)). - Avoid dumping "test assertions" everywhere; tests still own the scenario expectations.
- Locators are
private readonly, named in camelCase after the element they target (e.g.submitButton); no mandatory suffix. - Intent methods are verbs:
open(),loginAs(),save(),deleteProject(). - Keep methods small and composable; avoid
doEverything().
- When asserting, add meaningful messages (what user expected vs what was observed).
- Prefer returning useful objects for chaining (e.g.
submit(): Promise<DashboardPage>).
open()navigates and verifies the page is ready.assertLoaded()checks a stable landmark (heading/main container).
Example style:
private readonly signInButton = this.page.getByRole('button', { name: 'Sign in' });async open(): Promise<this> { await this.page.goto('/login'); await this.assertLoaded(); return this; }async loginAs(user): Promise<DashboardPage> { ...; await expect(dashboardHeading).toBeVisible(); return new DashboardPage(this.page); }
- Login/setup flows that are shared across tests should be implemented as Playwright fixtures (test/worker scope as appropriate), and Page Objects should be created by fixtures/factories.