Playwright makes it easy to get a test running quickly. It's harder to keep a whole suite of Playwright tests reliable six months later, once your app has changed, your team has grown, and nobody quite remembers why a particular test is flaky three days a week. If you're new to Playwright, our guide to what Playwright is and how it works is a good place to start first. This guide is for the next step: the habits that keep Playwright tests fast, stable, and worth trusting as your suite grows, organized around why each one matters and closely following Playwright's own official best practices guide. If you're still weighing Playwright against Selenium, our comparison breaks down the tradeoffs.
THE SHORT ANSWER
Playwright best practices, at a glance:
• Test what users actually see and do, not internal implementation details
• Use Playwright's built-in, user-facing locators instead of brittle CSS or XPath selectors
• Keep every test isolated, with its own data and browser context
• Use web-first assertions that retry automatically instead of one-shot checks
• Run tests across all three engines, in parallel, and debug failures with the trace viewer
Each of these is worth understanding in more depth, since the reasoning behind them is what actually keeps a suite maintainable.
The single most common source of fragile Playwright suites is testing the wrong layer. It's tempting to assert against a component's internal state, a specific CSS class, or a DOM structure that only exists because of how the frontend happens to be built this month. Playwright's own guidance is direct about this: test user-visible behavior, the same things an actual user would see and interact with, rather than implementation details that change every time a developer refactors a component.
This extends to how you scope each test. Every test should run in isolation, with its own data, cookies, and storage, rather than depending on state left behind by a previous test. When tests share state, one failure cascades into several unrelated ones, and debugging turns into archaeology.
It's also worth being deliberate about what you don't test directly. If your app depends on a third-party service, a payment provider, an external API, a widget you don't control, mock that dependency rather than testing the live third-party site itself. You're responsible for your own app's behavior, not for whether someone else's service is up.
This is where most of Playwright's specific tooling earns its keep, and it's also where most flakiness gets introduced or eliminated.
Start with locators. Playwright's built-in locators come with auto-waiting and retry-ability already handled, so you're not writing your own polling logic. The practice that matters most here is preferring user-facing attributes, like a button's visible text or an accessible role, over CSS selectors or XPath. A CSS class can change the moment a developer touches the stylesheet; the fact that a button says "Submit" tends to survive a lot more refactors. When a locator needs to be narrower, chain and filter it, for example scoping a search to a specific section of the page, or filtering by visible text, rather than writing one long, brittle selector.
If you're not sure what a good locator looks like for a given element, use Playwright's codegen tool or the VS Code extension. Both generate resilient, uniquely identified locators automatically by recording your actions in a real browser, which is often a faster and more reliable starting point than writing selectors by hand.
Assertions matter just as much as locators. Playwright's web-first assertions, methods like toBeVisible() or toHaveText(), wait and retry automatically until the condition is met or a timeout is hit, instead of checking once and immediately failing. That single behavior removes a large share of the "it just needed one more second" flakiness that plagues tests written with older, immediate-assertion patterns.
Writing a good test is half the job. Running it reliably in CI, and being able to actually figure out what went wrong when it fails, is the other half.
A few habits make the biggest difference here. Test across all three browser engines, Chromium, Firefox, and WebKit, by configuring separate projects for each rather than assuming Chromium-only coverage is representative. When something does fail in CI, the trace viewer is the tool worth learning first: it captures a full timeline of the test, including DOM snapshots and network activity, so you can see exactly what the browser was doing at the moment of failure without needing to reproduce it locally.
For speed, lean on parallelism and sharding, running independent tests concurrently and splitting a large suite across multiple machines, rather than running everything sequentially on one runner. On the CI side, install only the browsers your suite actually needs rather than the full set, which saves meaningful time and disk space on every run. It's also worth linting your tests with TypeScript and ESLint, specifically the no-floating-promises rule, which catches a surprisingly common bug: a missing await that lets a test move on before an action actually finishes.
Finally, keep your Playwright dependency current. New releases typically ship updated browser binaries and bug fixes, and falling behind tends to surface compatibility issues all at once rather than gradually.
Two smaller practices are worth calling out on their own, since they tend to get skipped early on and missed later. Soft assertions let a test collect multiple failures into one report instead of stopping at the first one, which is useful when you want full visibility into everything wrong with a page rather than fixing one issue at a time across several test runs. And once your suite grows past a handful of files, a consistent structure, like the page object model, for organizing locators and actions makes it far easier for a new team member to find and fix a broken test without reading the entire suite first. We cover that pattern in more detail in Page Object Model.
Every practice above makes an individual test more reliable. None of them solve a different problem that shows up once a Playwright suite grows: knowing, across dozens or hundreds of tests, what's actually failing, why, and whether a release is ready to ship. That's typically the point where teams start looking for a way to bring Playwright's results into a shared reporting and test management layer, alongside manual testing and other frameworks, instead of reading CI logs one run at a time. We've covered how to do that without rewriting your existing Playwright scripts in how to add test management to Playwright, without a rewrite.
Some teams are taking this further, using AI agents to handle not just the running of tests but the decision-making around what to test and how, routing a task to the right execution method instead of a person deciding that manually every time. We've covered a real implementation of that pattern in building a Playwright AI agent with Claude Code.
None of these practices are complicated on their own, testing visible behavior, preferring resilient locators, isolating tests, and using the trace viewer when something breaks. What makes them worth adopting deliberately is that they compound: a suite built on these habits from the start stays maintainable as it grows, while one that skips them tends to accumulate flaky tests faster than anyone can fix them. If you haven't yet, it's worth revisiting what Playwright is and how it works for the fundamentals these practices build on.