# Playwright Best Practices

Use this reference when writing or reviewing locator, assertion, isolation, or synchronization logic.

Official sources:

- https://playwright.dev/docs/best-practices
- https://playwright.dev/docs/locators
- https://playwright.dev/docs/test-assertions
- https://playwright.dev/docs/browser-contexts

## What Matters Most

### 1. Keep scenarios isolated

Playwright's model is built around clean browser contexts so one test does not leak into another.

Apply it like this:

- do not depend on another scenario having run first
- keep scenario state in the runner's scenario-owned context rather than module globals
- model special authentication or session setup through explicit per-scenario fixtures rather than shared mutable state

### 2. Prefer user-facing locators

Playwright recommends built-in locators that reflect what users perceive on the page.

Preferred order:

1. `getByRole`
2. `getByLabel`
3. `getByPlaceholder`
4. `getByText`
5. `getByTestId` when an explicit test contract is the most stable option

Avoid raw CSS/XPath selectors unless no stable user-facing contract exists and adding one is not practical.

Also remember:

- repeated content usually needs scoping to a stable container
- exact text matching is often too brittle when role/name or label already exists
- `getByTestId` is acceptable when semantics are weak but the contract is intentional
- when a real UI region, card, status, or icon lacks an accessible name, prefer adding that semantic contract in product code before falling back to `getByTestId`

### 3. Use web-first assertions

Playwright assertions auto-wait and retry. Prefer them over manual state inspection.

Prefer:

- `await expect(page).toHaveURL(...)`
- `await expect(locator).toBeVisible()`
- `await expect(locator).toBeHidden()`
- `await expect(locator).toBeEnabled()`
- `await expect(locator).toHaveText(...)`

Avoid:

- `expect(await locator.isVisible()).toBe(true)`
- custom polling loops for DOM state
- `waitForTimeout` as synchronization

If a condition genuinely needs custom retry logic, use Playwright's polling/assertion tools deliberately and keep that choice local and explicit.

Use `expect.poll` for non-DOM truth such as API state, backend eventual consistency, generated resources, or captured browser events. For DOM state, use locator assertions so Playwright can apply actionability and web-first retry semantics.

### 4. Let actions wait for actionability

Locator actions already wait for the element to be actionable. Do not preface every click/fill with extra timing logic unless the action needs a specific visible/ready assertion for clarity.

Good pattern:

- assert a meaningful visible state when that is part of the behavior
- then click/fill/select via locator APIs

Bad pattern:

- stack arbitrary waits before every action
- wait on unstable implementation details instead of the visible state the user cares about

### 5. Match debugging to the active harness

Playwright supports traces, screenshots, page snapshots, and browser logs. Configure artifact capture at the runner boundary instead of adding parallel diagnostics to individual scenarios.

## Review Questions

- Would this locator survive DOM refactors that do not change user-visible behavior?
- Is this assertion using Playwright's retrying semantics?
- Is any explicit wait masking a real readiness problem?
- Does this code preserve per-scenario isolation?
- Is a new abstraction really needed, or does it bypass the runner's scenario-owned context and lifecycle?
