The Problem
Three ways a growing Playwright suite falls apart.
A Playwright project that grows past a hundred tests usually starts to fail in three distinct ways. The tests themselves get fragile, the team writing them drifts into incompatible conventions, the framework becomes accessible only to whoever built it… Each of these has different symptoms and a different fix, but they share a root cause that we'll try address and prevent with this series.
§ 1Selector rot, URL rot, schema rot
Most Playwright projects start with the example from the docs :
There is nothing wrong with this test. No really, that's not the prettiest but it does work. Sometimes it's all you expect from it : It runs, it passes, it verifies the right behavior. Multiplied across thirty test files, it stays green for as long as the application doesn't move. The problem is that applications move.
A data-testid (when you have the luxury to have test-ids)
gets renamed during a UI refactor. The change is invisible to whoever
made it, because they don't grep for testids when they rename a
component. The CI turns red across however many spec files reference
that testid. The fix is a find-and-replace pass, plus a manual sweep
for any spec where the testid appears as part of a longer string.
A staging URL changes. It's hardcoded in every spec file plus a handful of setup files. Patching it is mechanical but slow, and at least one place gets missed on the first pass.
A required field gets added to a form. Every test that submits the form starts failing because none of them fill it. The fix is to add the field everywhere, except half the patches use the form field name and half use the database column name, and the next CI run is half-red for entirely silly reasons.
A login flow gets MFA, or SSO, or an email confirmation step. Every test breaks at once. Not because the tests are wrong, but because the assumption baked into all of them, that you can log in with two fields and a click, is no longer true.
Click any of the events below and watch the same 17 lines of test code give way. The damage in this single file is what every test in the suite is taking, in parallel, on the same day.
All-in-all, things starts to go wrong and whenever you open the codebase you start having the silly idea of "starting over", just to reassure yourself that you can do better. But let's face it : refactoring is a luxury most teams cannot afford…
None of these breakages are unusual. They are what happens when a frontend evolves at the pace the business needs it to. The procedural test absorbs the cost of every UI change as a sweep through every spec file that referenced the changed thing. That cost compounds as the suite grows.
At 30 tests it's annoying. At 300 it's the reason the team stops trusting your regression suite…
§ 2Convention drift inside a single stack
Even when the tests are technically resilient, the codebase that produces them tends to drift. This isn't about people bringing incompatible stacks to the project (the hiring filter usually handles that), it's about people bringing different opinions about how to write tests in the same stack, and nothing in the project forcing those opinions to converge.
One person writes flat spec files with inline locators. Another
extracts locators into helper functions at the top of each spec. A
third writes proto-Page-Objects as plain classes in a helpers/ folder. A fourth adds a pages/ folder a month later, not
realizing helpers/ already exists for similar reasons, and
starts moving things in there. Each of these is a defensible choice in
isolation but none of them combine.
The same login flow ends up implemented three different ways across the repo. None of them share locators. When the login button's testid changes, the dev who fixes one version doesn't know the other two exist. The regression suite goes half-red, gets half-fixed, and the team learns to live with a 75% pass rate on things that are not really broken, but hey…
This drift doesn't get caught in review because tests rarely get the review rigor that production code does. A PR that adds a new test in a fourth structure gets approved in two minutes by someone who saw the CI go green and assumed the rest of the codebase looked similar. By the time the divergence becomes visible, there are four parallel folders and no shared abstractions between them.
The root issue is that the project never declared a convention. Nobody
wrote pages/ is where Page Objects live and they extend BasePage and locators are private. Without a declared convention, people fall
back on whatever they did last. The result isn't bad code, it's
incoherent code, which is harder to fix because there's no single
thing to fix.
§ 3The framework as private property
The third failure shows up later, usually when the original author of the test setup stops being available to it. They take a vacation, switch projects, leave the company. The framework is still there, but the team's relationship with it completely changes.
Custom fixtures that nobody else wired keep working but nobody dares modify them. The custom HTML reporter generates output that the team has informally agreed not to look at because last time someone opened it the page hung. New tests get written in a parallel style next to the existing framework, drawing on the Playwright getting-started docs instead of the project's actual setup, because the docs are documented and the framework isn't.
This isn't a code quality problem, the framework was good. It just had no public interface. No documented entry points, no convention statement, no examples of how to extend it. Without those, the only person who could maintain it was the person who built it, and once that person isn't around, the team works around the framework instead of through it. Eventually you end up with two test suites in the same repo, written years apart, with no shared abstractions and no plan to consolidate. And that angers me for some reason.
§ 4What all three have in common
The selector rot is a symptom of selectors not being encapsulated. The convention drift is a symptom of no declared architecture. The private-property problem is a symptom of no public interface.
Different symptoms, same disease : there is no shared model of what a test is, what a Page Object is, what a fixture does, and who is allowed to know what about what.
The architecture that survives all three has four layers and one rule :
- Tests describe behavior
- Fixtures hand them what they need
- Page Objects and Components encapsulate the UI
- A thin Core layer holds the abstract base classes that everything inherits from.
The rule is that each layer can only depend on layers below it. Hover any layer to see what lives there, or toggle violation mode to test what happens when the rule gets broken.
The four layers and the dependency rule are what stop the three failures from compounding. Selectors are encapsulated because they have nowhere else to go. Conventions hold because the layers are the convention. The framework has a public interface because the layers and the rule are the interface. None of this is enforced by tooling, it's enforced by where you can and can't import from.
§ 5Where this leaves us
Three failure modes, one architectural pattern, one rule about dependency direction. The rule sounds simple until you build the thing that enforces it. That's the next three parts.
Part 2 walks through the build, layer by layer, with code that runs. Part 3 measures what the architecture actually buys you against the three failures from this article. Part 4 packages the whole thing into a CLI so you never have to type it out again.