Skip to content
Dev Tools Article

Your Playwright Best Practices Are Selenium Habits in Disguise

The community's testing canon keeps contradicting what the Playwright team actually documents and ships.

Rachel Goldstein
Rachel Goldstein
Dev Tools Editor · Aug 20, 2026 · 5 min read
Your Playwright Best Practices Are Selenium Habits in Disguise

Playwright won the end-to-end testing war. It passed Cypress in npm downloads in late 2024 and the gap has since widened to several multiples, with the framework now pulling tens of millions of weekly downloads. And winning has a predictable side effect: a flood of "best practices" guides, one landing on Dev.to or Medium every week, most of them recommending roughly the same architecture — Page Object Model classes, data-test attributes on everything, a Cucumber BDD layer on top, Allure for reporting.

Here's the problem: that architecture is Selenium's, not Playwright's. The QA industry spent fifteen years building scar tissue around Selenium's weaknesses — no auto-waiting, brittle selectors, no built-in runner — and it's now porting that scar tissue wholesale into a framework designed to make most of it unnecessary. Some of it still earns its place. A surprising amount actively contradicts what the Playwright team ships and documents.

The locator advice is backwards

The folk canon says: put data-test attributes on everything, because they're immune to styling and structural changes. ARIA roles come second, if at all.

Playwright's own best-practices guidance inverts that order. getByRole() is the preferred locator, because it targets what a user (or a screen reader) actually perceives — a button named "Submit order" — rather than an attribute no user can see. Test IDs are the documented fallback, not the default.

This isn't a style debate. A role-based locator fails when your accessibility tree breaks, which means your test suite doubles as a continuous accessibility smoke test for free. A data-test locator happily passes while the button loses its accessible name. And since v1.49, toMatchAriaSnapshot() lets you assert whole regions of the accessibility tree from YAML — a capability that's worthless if your suite never engages with roles in the first place. Sprinkling test IDs everywhere is optimizing for the failure mode of a framework you're no longer using: Selenium's CSS-and-XPath fragility.

The honest trade-off: role locators are slower to write on div-soup legacy frontends, and they force fights with your frontend team about semantics. Have those fights. They're the cheap kind.

Page objects survived; the rules around them shouldn't have

The Playwright docs do recommend page objects for large suites, so this isn't a "POM is dead" take. But the Selenium-era rulebook that travels with POM deserves scrutiny — particularly the commandment that page objects must be assertion-free, exposing only actions while tests do the asserting.

That rule existed because Selenium assertions were manual state checks that belonged near the test's intent. Playwright's web-first assertions are different animals: expect(locator).toBeVisible() retries until it passes or times out, which makes it a synchronization primitive, not just a check. A page-object method that clicks "Save" and doesn't assert the confirmation toast isn't cleaner — it's a race condition exported to every caller. In Playwright, assertions inside page objects are often how you make an action reliably complete.

The better structural tool, which most guides skip entirely, is fixtures. Playwright's fixture system gives you typed, composable, lazily-initialized dependencies — an authenticated page, a seeded test user, an instantiated page object — injected per test with automatic teardown. A test.extend() fixture that yields a logged-in DashboardPage replaces both the POM constructor boilerplate and the beforeEach login ritual, and it's the pattern the framework itself is built around. If your suite has page objects but no custom fixtures, you've adopted the Selenium half of the architecture and skipped the Playwright half.

The Cucumber tax

Then there's the BDD layer. Worth stating plainly: Playwright has no official Cucumber support, and the team has shown no appetite for adding it — BDD integration remains an open community request. The workable path is the community-maintained playwright-bdd package, which compiles Gherkin into native Playwright tests, and it's genuinely well-executed.

But the question isn't whether you can — it's what you're buying. A Gherkin layer adds a translation step (feature file → step definition → page object → Playwright API) that quadruples the places a change can land, in exchange for readability by stakeholders. If your product managers actually read and write feature files, pay the tax. In most shops I've seen, nobody outside QA opens them, and the Gherkin exists because the Selenium-era framework template had it. Meanwhile Playwright's native syntax — test('user can reorder a saved cart', ...) with role-based locators — is already close to executable English.

The framework is telling you where this goes

The strongest argument against deep bespoke abstraction layers is what Playwright is shipping now. Version 1.56 introduced agent definitions — planner, generator, and healer — that guide LLMs through writing and repairing tests, and 1.59 added trace-analysis tooling for agentic test-fixing. Aria snapshots grew options specifically for AI consumption. Whatever you think of AI-generated tests, the direction is unambiguous: the Playwright team is betting that test authoring and maintenance — the entire justification for heavyweight POM hierarchies — gets increasingly automated.

Automated healing works on standard, idiomatic Playwright: role locators, web-first assertions, traces. It does not work on your company's five-layer Cucumber-POM-helper lasagna, because no agent can see through abstractions it's never met. Every layer of custom framework you add today is a layer the tooling can't help you maintain tomorrow. That was always a cost; it's about to become a much bigger one.

What to standardize on instead

If I were writing the internal best-practices doc: getByRole() first, getByTestId() as the documented exception. Shallow page objects for genuinely repeated flows, instantiated through fixtures, allowed to assert. storageState for auth reuse, network mocking for third-party dependencies, zero waitForTimeout() calls tolerated in review. Traces on first retry — npx playwright show-trace beats any reporting dashboard for actual debugging — and cross-browser runs on a schedule rather than every PR. And no Cucumber unless a non-engineer demonstrably reads the feature files.

None of that is exotic. Most of it is just the framework's own documentation, which remains the best Playwright best-practices guide ever written — and the one the genre keeps politely ignoring.

Sources & further reading

  1. Playwright JavaScript Framework Best Practices — dev.to
  2. Best Practices — playwright.dev
  3. Release notes — playwright.dev
  4. Page object models — playwright.dev
  5. Feature: Full support for Cucumber.js or BDD in general — github.com
  6. Testing Framework Adoption Trends: 5 Years of npm Data — testdino.com
Rachel Goldstein
Written by
Rachel Goldstein · Dev Tools Editor

Rachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading