Best Practices
A collection of rules I keep coming back to when reviewing Playwright test suites — things that make tests survive refactors, run reliably on CI, and stay readable months later.
Test what the user sees, not how it's built
The most resilient tests click buttons by label, check text that users read, and don't care about CSS classes or component internals. When a developer renames class="btn-primary" to class="button-filled", your tests shouldn't break — and they won't if you wrote them against visible behavior.
Locator priority: role > text > test-id > css
Playwright recommends this order for locators, from most resilient to least:
getByRole()— tests accessibility and behavior at oncegetByText()/getByLabel()— tied to visible contentgetByTestId()— stable explicit marker, add when role/text aren't enough- CSS/XPath — last resort, use when nothing else works
When you need getByTestId(), agree with the dev team on a convention. I use data-testid as the attribute name — Playwright uses it by default, and it's easy to grep for in the codebase.
Keep tests independent
Each test should run correctly regardless of which tests ran before it or whether it runs in parallel. If test B relies on data that test A created, you have a hidden dependency — and when tests run in a different order, B breaks for no obvious reason.
Practical rule: if you can't run a test in isolation with npx playwright test --grep "test name" and have it pass, it's not truly isolated.
Never hardcode waits
await page.waitForTimeout(2000) is a 2-second gamble: too short on a slow CI machine, too long on a fast local machine. It makes tests slow, flaky, and hard to maintain. Playwright's auto-waiting and explicit assertions handle timing correctly — use those instead.
Mock external services
Third-party APIs, payment gateways, SMS providers — don't call them in tests. They're slow, rate-limited, cost money, and can return unexpected responses. Mock them with page.route() and return exactly the response your test needs.
Use Page Objects for repeated flows
If login, navigation, or form filling appears in 5+ tests, extract it to a Page Object. When the UI changes — you update one class, not 20 test files. Keep Page Objects thin: just locators and actions, no assertions. Assertions belong in tests.
CI-specific tips
A few things that save pain on CI:
- Always run in headless mode — headed mode needs a display server
- Set
retries: 1in config to catch flakiness without masking real bugs - Use
--reporter=githubon GitHub Actions for inline test annotations - Save traces on failure (
trace: 'on-first-retry') — you'll thank yourself when debugging - Pin browser versions in
package.json—@playwright/testversion determines browser binaries