We Scored 17 Real Playwright Test Suites: The State of Playwright Quality in 2026
TL;DR
We scored 17 real, public Playwright suites (not our own fixtures) with playwright-score, our open-source, deterministic, AI-free scorer — re-run 2026-08-27 under its stricter per-test scoring model, where your score is the share of your tests that are clean. Across 1,228 spec files and 5,664 tests we found 6,415 rule violations; 80.9% of tests are clean, meaning roughly 1 in 5 carries a provable defect. The most common: raw CSS/XPath locators instead of role-based ones — 16 of 17 repos, 3,280 occurrences, 51% of every finding — followed by conditional logic inside tests (877 occurrences). 30.1% of every locator call in the corpus is a raw selector, and two repos use zero role-based locators at all. 12 of 17 repos have at least one test that passes without asserting anything. Five repos now fail the default threshold, including cal.com. Full methodology, every repo, and a script to reproduce every number are public on GitHub.
Every managed QA vendor, including us, claims to write "good" Playwright tests. Almost none of that claim is measurable. So we built a scorer that doesn't take anyone's word for it — an open-source, deterministic, AI-free tool called playwright-score — and pointed it at 17 real, public Playwright suites we don't own, didn't write, and didn't curate for a good outcome.
Updated 2026-08-27: every number in this post was re-scored with playwright-score 0.5.0 and its new scoring model — a stricter, per-test model where your score is simply the weighted share of your tests that are clean. Two suites that passed under the original model now fail. The original 2026-08-19 run used package 0.1.x; the deltas and why we changed the model are covered below.
The result: 1,228 spec files, 5,664 tests, 171,602 lines of test code, and 6,415 individual rule violations, scored on 2026-08-27 against each project's live main/master branch. Every number in this post is reproducible from a single script against the public repos linked throughout — no surveys, no self-reported data, nothing from our own customers.
The headline numbers
17
public repos scored
6,415
rule violations found
30.1%
of all locators are raw CSS/XPath
1 in 5
tests has at least one real defect
The corpus
We didn't pick repos to make the tool — or the industry — look good or bad. The corpus mixes heavily-engineered platforms (Supabase, Grafana, Mattermost, n8n) with smaller, less mature projects found by searching for real @playwright/test usage. Every repo is scored on the exact subdirectory that holds its Playwright suite, via a fresh, shallow, sparse clone — nothing is vendored or cached.
| Repo | Score | Grade | Files | Tests | Findings |
|---|---|---|---|---|---|
| Playwright (own TodoMVC example) | 98/100 | A | 24 | 24 | 4 |
| Storybook | 95/100 | A | 7 | 24 | 13 |
| n8n | 95/100 | A | 260 | 1002 | 42 |
| freeCodeCamp | 94/100 | A | 86 | 326 | 207 |
| dub | 94/100 | A | 21 | 213 | 41 |
| Documenso | 93/100 | A | 126 | 1109 | 693 |
| Supabase | 91/100 | A | 31 | 254 | 220 |
| PostHog | 88/100 | B | 43 | 119 | 273 |
| Grafana | 87/100 | B | 213 | 668 | 869 |
| sencho | 85/100 | B | 26 | 148 | 278 |
| Mattermost | 83/100 | B | 290 | 1030 | 1866 |
| novu | 82/100 | B | 2 | 2 | 3 |
| Immich | 78/100 | C | 13 | 43 | 40 |
| cal.com | 73/100 | C | 53 | 253 | 1258 |
| openplayerjs | 73/100 | C | 8 | 77 | 39 |
| livecodes | 70/100 | C | 14 | 234 | 140 |
| TheCyberHub | 56/100 | F | 11 | 138 | 429 |
Run on 2026-08-27 with playwright-score 0.5.0 (scoring model v3), standard profile, threshold 80. Twelve of seventeen pass. Reproduce it yourself with scripts/validate-corpus.sh in the GitHub repo.
What's actually wrong with real Playwright suites
Grades hide the interesting part. We also kept every individual finding — 6,415 of them — and ranked which specific anti-patterns show up most often across the corpus. This is the part that isn't on our own tool's landing page.
| Rule | Occurrences | Repos affected | What it means |
|---|---|---|---|
| no-raw-locators | 3,280 | 16 / 17 | Raw page.locator()/frame.locator() CSS or XPath instead of role-based locators |
| no-conditional-in-test | 677 | 12 / 17 | if/else branching inside a test — behavior depends on runtime page state |
| prefer-native-locators | 572 | 8 / 17 | Legacy page.click(selector) style instead of Locator API |
| no-wait-for-timeout | 450 | 11 / 17 | Hard-coded sleeps (waitForTimeout) instead of condition-based waits |
| prefer-web-first-assertions | 219 | 12 / 17 | Assertions that don't auto-retry against the live DOM |
| prefer-locator | 208 | 8 / 17 | Direct page actions on selector strings instead of locator objects |
| no-conditional-expect | 200 | 9 / 17 | expect() calls that only run on one branch — assertions that can silently skip |
| no-networkidle | 189 | 8 / 17 | waitForLoadState('networkidle') — deprecated, unreliable in Playwright |
| no-force-option | 144 | 9 / 17 | { force: true } bypassing actionability checks |
| no-wait-for-selector | 103 | 8 / 17 | Manual waitForSelector instead of auto-waiting locators |
| expect-expect | 102 | 12 / 17 | A test block that runs and passes without asserting anything |
Two patterns dominate everything else. Locator anti-patterns account for 4,060 of the 6,415 total findings — 63% of every violation in the corpus — and no-raw-locators alone is 51% of all findings, present in 16 of the 17 repos. This is not a niche mistake. It's the default way most teams still write Playwright selectors, three years after Playwright's own docs started recommending role-based locators over raw CSS.
Zoomed out across every locator call in the corpus — not just findings, every single getByRole/getByTestId/getByText vs. page.locator()/frame.locator() call, 17,145 of them — 69.9% are role-based and 30.1% are raw selectors. Two repos in the corpus, livecodes and openplayerjs, use zero role-based locators: every single locator in both suites is a raw CSS or XPath selector. Both fail the default threshold.
The second-biggest category is one we're publishing with some humility: conditional logic inside tests — 877 occurrences (no-conditional-in-test, 677 in 12 repos, plus no-conditional-expect, 200 in 9). An if (await locator.isVisible()) branch means the test does different things on different runs — and an assertion inside that branch can silently never execute. Until late August, a config bug in our own scorer left these rules silently disabled: a suite built entirely out of conditional branches scored a perfect 100. We found it, fixed it, published the fix the same day, and the second-most-common anti-pattern in real-world Playwright appeared out of nowhere. The full account is in the changelog.
When we made the scorer stricter, two more suites failed
The original version of this study used a finding-density model: violations per line of test code, smoothed so one repeated mistake couldn't sink a big suite. It had a flaw we couldn't defend once we saw it clearly — a tiny suite where two of three tests had hard waits and no assertions still scored 78/C, because a handful of findings barely registers per-line. Density is also not how anyone reviews a test suite. Reviewers say "two of your three tests are broken," not "0.5 penalty units per 25 lines."
Scoring model v3 says exactly what a reviewer says: every finding is attributed to the test it sits in, a test with an error is a flawed test, and your score is the weighted share of your tests that are clean. A problem in a beforeEach counts against every test that runs through it. Under that model, this corpus has 4,580 clean tests out of 5,664 — 80.9%. Roughly one in five real-world Playwright tests carries at least one defect the scorer can prove.
The strictness has consequences: cal.com (73) and Immich (78) now fail the default threshold, joining the three suites that always did, and TheCyberHub drops to an F. cal.com is instructive: 1,258 findings across 253 tests, only 60% of its tests clean, and a locator ratio of 41.2% role-based (613 native vs. 875 raw) against a corpus average of 69.9%. The old density model forgave that concentration; the per-test model doesn't. Meanwhile the suites that were engineered carefully barely moved — n8n held its 95 exactly, and Playwright's own TodoMVC example its 98 — which is what you want from a stricter model: pressure on real problems, not noise on good suites.
Tests that assert nothing
playwright/expect-expect — a test block that runs, exits without an error, and never calls expect() — appeared 102 times across 12 of the 17 repos (71%). These aren't failing tests. They're passing tests that prove nothing, the exact failure mode we've written about before: a green suite that doesn't mean the product works. A scorer that only checked pass/fail would never catch a single one of these 102 cases — they all pass. Under model v3 each of them is, by definition, a fully flawed test in the assertions dimension: a test that asserts nothing can't be clean.
Methodology
Every score in this post comes from @qaguardian/playwright-score 0.5.0, scoring model v3, standard profile, threshold 80 — the same package anyone can npm install -D today. The scorer is fully deterministic and AI-free: it wraps eslint-plugin-playwright for community best practices, attributes every finding to the test it sits in via AST test spans, and adds a locator-ratio metric and assertion-delegation tracing through local imports. No LLM is in the scoring path.
Each repo was scored via a fresh, shallow, sparse git clone of the exact subdirectory holding its Playwright suite — nothing from any of these repos is vendored into ours. The rule-frequency breakdown in this post comes from the raw JSON findings output of that same run, aggregated by rule across all 17 reports. The full scoring model, weights, and a worked example are documented in METHODOLOGY.md. Scores will drift slightly over time as these projects' own suites change — that's expected, and the whole point of scoring live code instead of a frozen snapshot.
What to do with this if you maintain a Playwright suite
Run npx -p @qaguardian/playwright-score playwright-score ./tests --format text against your own suite before you assume it's fine. The output now tells you directly how many of your tests are clean. Given that 63% of every finding in this corpus was a locator anti-pattern, that's the highest-value place to look first — a raw page.locator('.btn-primary') breaks the moment a class name changes; getByRole('button', { name: 'Submit' }) survives a restyle. If your suite is CI-gated, the tool exits non-zero below your threshold, so this is a five-minute add to an existing pipeline, not a rewrite.
If you'd rather have engineers own that quality bar for you — the same bar we hold AI-generated Playwright to internally — book a demo and we'll score your actual suite live, on the call.
Tags
See QA Guardian in action
Everything we write about is what we build and run every day. Book a demo and we'll show you on your own codebase.