playwright-best-practices
Playwright Best Practices
Use When
- Writing new Playwright tests
- Reviewing Playwright test changes
- Fixing flaky Playwright tests
- Choosing locators, assertions, fixtures, projects, or debug settings
- Refactoring Playwright tests toward user-visible behavior
Do Not Use When
- Testing non-browser unit logic better covered by unit tests
- Writing tests for third-party sites or services you do not control
- Adding sleeps, retries, or larger timeouts to hide nondeterministic behavior
- Building Page Object Models before duplication or intent problems exist
Workflow
- Start from user behavior: what user sees, clicks, types, or expects.
- Pick resilient locators:
getByRole,getByLabel,getByText,getByPlaceholder,getByTestId; avoid DOM/CSS/XPath unless no user-facing option exists. - Assert with web-first assertions:
await expect(locator).toBeVisible()over manual boolean checks. - Rely on Playwright auto-waiting. Do not add
waitForTimeout; wait for user-visible state, URL, request, or response. - Keep tests isolated: each test owns page state, storage, cookies, data, and setup.
- Use
beforeEachand fixtures for repeated setup, not shared mutable state between tests. - Mock third-party dependencies and control database/test data.
- Run across browser/device projects when the behavior must work for broad users.
- Use traces, screenshots, videos, UI mode, and debug mode to diagnose failures; fix root cause, not symptoms.
- Keep tests focused. One meaningful user flow per test; prefer clear duplication over clever abstraction.
Review Checklist
- Test validates user-visible behavior, not implementation details.
- Locators use roles, labels, text, placeholders, alt text, titles, or test ids.
- No brittle selectors such as CSS classes, DOM paths, or XPath without reason.
- Assertions are awaited web-first assertions.
- No fixed sleeps or broad hard-coded timeouts unless justified.
- Tests can run independently and in any order.
-
beforeEach/fixtures create fresh state or authenticated storage safely. - Test data is controlled and repeatable.
- External services are mocked or contract-controlled.
- Retries/traces/screenshots/videos support debugging only; test is deterministic without them.
- POMs expose user actions and page concepts without hiding important assertions.
- Cross-browser projects exist when compatibility matters.
Locator Rules
Prefer, in order:
page.getByRole('button', { name: 'Submit' })page.getByLabel('Email')page.getByPlaceholder('Search')page.getByText('Welcome')page.getByTestId('checkout-submit')for explicit testing contracts- Chained/filtered locators for scope:
page.getByRole('listitem').filter({ hasText: 'Product 2' })
Avoid:
- CSS classes tied to styling
- DOM position selectors
- XPath
- Component names, function names, internal state, framework internals
Good vs Bad Patterns
User-visible locators
// Bad: style/DOM implementation detail
await page.locator('button.buttonIcon.episode-actions-later').click();
// Good: user-facing role and name
await page.getByRole('button', { name: 'Save for later' }).click();
Web-first assertions
// Bad: immediate boolean, no retry
expect(await page.getByText('Welcome').isVisible()).toBe(true);
// Good: retries until condition or timeout
await expect(page.getByText('Welcome')).toBeVisible();
Auto-waiting over sleeps
// Bad: slow and flaky
await page.waitForTimeout(2000);
await expect(page.getByRole('alert')).toContainText('Saved');
// Good: waits for meaningful state
await expect(page.getByRole('alert')).toHaveText('Saved');
Isolated setup
// Bad: second test depends on first test's login/cart state
test('logs in', async ({ page }) => { /* ... */ });
test('checks out', async ({ page }) => { /* assumes logged in */ });
// Good: each test starts from known state
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Passphrase').fill('valid-test-credential');
await page.getByRole('button', { name: 'Sign in' }).click();
});
Fixtures without shared mutable state
// Bad: mutable module-level state leaks between tests
let createdUserId: string;
// Good: fixture creates and cleans isolated data per test
const test = base.extend<{ user: User }>({
user: async ({}, use) => {
const user = await createTestUser();
await use(user);
await deleteTestUser(user.id);
},
});
POM only when useful
// Bad: hides intent and assertion details
await checkoutPage.doEverything();
// Good: user-level action, assertion remains readable in test
await checkoutPage.submitOrder();
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
Config Guidance
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Use browser projects when product risk justifies cross-browser coverage. Keep Playwright current so tests run against current browser engines.
Debugging Guidance
- Prefer trace viewer for CI failures; configure
trace: 'on-first-retry', not always-on traces. - Use UI mode, VS Code extension, or
npx playwright test --debugfor local diagnosis. - Use codegen to discover locators, then edit for readability and user intent.
- Use retries to expose flakiness signal, not to accept flaky tests.
- If a timeout feels necessary, first look for missing assertions, uncontrolled data, unmocked third-party calls, or hidden shared state.
References
More from flpbalada/fb-skills
progressive-disclosure
Reduce complexity by revealing information progressively. Use when designing
7discuss-task
Clarify ambiguous tasks before action. Use when goal, scope, success criteria, constraints, or risks are unclear.
4cognitive-fluency-psychology
Apply cognitive fluency principles to improve clarity, trust, and conversion.
4react-useeffect-avoid
Guides when NOT to use useEffect and suggests better alternatives. Use when reviewing React code, troubleshooting performance, or considering useEffect for derived state or form resets.
4discuss-code
Critically discuss code issues with compact findings. Use when code needs review for logic, simplicity, structure, naming, or maintainability.
4learn
Extract reusable patterns from the current session. Use when errors, debugging techniques, workarounds, or project conventions should become skills.
3