@qaguardian/playwright-score 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +239 -3
- package/README.md +50 -63
- package/dist/eslint-runner.d.ts +2 -0
- package/dist/eslint-runner.js +22 -4
- package/dist/formatters/markdown.js +8 -0
- package/dist/formatters/text.js +7 -0
- package/dist/import-graph.d.ts +1 -0
- package/dist/import-graph.js +1 -1
- package/dist/index.js +304 -14
- package/dist/metrics.d.ts +17 -4
- package/dist/metrics.js +27 -8
- package/dist/playwright-config.d.ts +140 -0
- package/dist/playwright-config.js +543 -0
- package/dist/rules/base-plugin.js +46 -12
- package/dist/test-aliases.d.ts +18 -0
- package/dist/test-aliases.js +218 -0
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,244 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. Any change that
|
|
4
|
-
alters what a given suite scores
|
|
5
|
-
|
|
4
|
+
alters what a given suite scores *by changing a rule's weight or
|
|
5
|
+
severity* is a new scoring-model version (see
|
|
6
|
+
[METHODOLOGY.md](./METHODOLOGY.md)), not a quiet patch. A release that
|
|
7
|
+
leaves every rule's weight and severity untouched but changes which files
|
|
8
|
+
or which test names get recognized in the first place — discovery and
|
|
9
|
+
parsing accuracy, not grading policy — can still move a suite's score
|
|
10
|
+
without itself being a new model version; 2.1.0 below is exactly that
|
|
11
|
+
case, and says so explicitly.
|
|
12
|
+
|
|
13
|
+
## 2.1.1 — 2026-09-26
|
|
14
|
+
|
|
15
|
+
Scoring model stays v4. Reviewer follow-ups from the 2.1.0 release review
|
|
16
|
+
(QAG-230) — none of these blocked that release. They close symlink-escape
|
|
17
|
+
gaps a crafted repo could otherwise use, quiet a false warning, and fix a
|
|
18
|
+
false-positive drop the first round of these same fixes introduced. None
|
|
19
|
+
of that changes what an ordinary suite with no symlinked spec files
|
|
20
|
+
scores; the realpath dedupe fix below is the one exception — it can
|
|
21
|
+
change the file/test count for a suite that has a symlinked spec file
|
|
22
|
+
matching discovery alongside its own real target, and says so.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **A match escaping the *repository* through a symlink is now dropped —
|
|
26
|
+
scoped to the repo root, not the narrower directory the caller pointed
|
|
27
|
+
at.** `testDirEscapesRepo` (added in 2.1.0) only clamps a config's
|
|
28
|
+
`testDir` itself before it becomes a glob root — it couldn't see a
|
|
29
|
+
symlinked directory the glob then walked *through* on the way to an
|
|
30
|
+
individual match. A `testMatch` entry that names a symlinked directory
|
|
31
|
+
by a literal path segment (not a `**` wildcard) is still followed even
|
|
32
|
+
with glob's `follow: false` (that option only stops `**` from
|
|
33
|
+
*expanding into* a symlinked directory, not a pattern from naming one
|
|
34
|
+
explicitly), so a crafted repo (`tests/evil -> /`, `testMatch:
|
|
35
|
+
['evil/**/*.ts']`) could make discovery walk arbitrary parts of the
|
|
36
|
+
filesystem while every match still lexically looked like it lived
|
|
37
|
+
under the scanned directory. The same gap let an individually
|
|
38
|
+
symlinked spec file sitting directly in the repo (no `testMatch`
|
|
39
|
+
trickery needed) resolve to, and get scored from, a file outside it.
|
|
40
|
+
`src/index.ts`'s match loop realpath's every candidate match and drops
|
|
41
|
+
it unless it's still contained within the realpath of the **repository
|
|
42
|
+
root** (`safeRealpath(findRepoBoundary(abs))`) — not, as this package
|
|
43
|
+
first shipped it, the narrower directory the caller actually scanned.
|
|
44
|
+
That first version silently dropped a real, common monorepo shape too:
|
|
45
|
+
a spec reachable through an in-repo symlink that resolves to a sibling
|
|
46
|
+
directory *outside* the caller's scanned subpath (one package's specs
|
|
47
|
+
shared with another via a symlink) is legitimate and is kept; only a
|
|
48
|
+
match whose realpath lands outside the repository itself is dropped.
|
|
49
|
+
The same repo-root containment now also applies to a bare glob-pattern
|
|
50
|
+
input (e.g. `tests/*.spec.ts` passed directly to `scorePaths`, not a
|
|
51
|
+
directory it walks itself) — the first version of this fix only
|
|
52
|
+
covered the directory-scan branch, so switching an otherwise-identical
|
|
53
|
+
call from a directory argument to an equivalent glob argument silently
|
|
54
|
+
bypassed the guard entirely.
|
|
55
|
+
- **The walk itself is now pruned at a symlink that escapes the repo
|
|
56
|
+
root, not just filtered after the fact.** The realpath containment
|
|
57
|
+
check above still has to pay to walk all the way through a symlinked
|
|
58
|
+
directory before dropping every match inside it one by one — for a
|
|
59
|
+
large or slow target (`tests/evil -> /`) that's a real cost, not just
|
|
60
|
+
a theoretical one (measured: ~6.4s to fully walk a large symlinked
|
|
61
|
+
directory before this fix). `RepoBoundaryIgnore` (`src/index.ts`) wraps
|
|
62
|
+
glob's own `ignore` matching with a `childrenIgnored` check that stops
|
|
63
|
+
the walk from ever descending into a symlinked directory whose
|
|
64
|
+
realpath lands outside the repo root, only paying the realpath cost at
|
|
65
|
+
an actual symlink (a plain nested directory can't walk outside a root
|
|
66
|
+
its own parent is already inside, so it's never checked).
|
|
67
|
+
- **An in-repo symlinked spec and its own real target are no longer both
|
|
68
|
+
scored, when both independently match discovery under the same scan.**
|
|
69
|
+
Pre-existing, not new to the fixes above: `expandPaths` deduped a
|
|
70
|
+
literal path only against `explicit` inputs, never against another
|
|
71
|
+
`expanded` match that happens to resolve to the same realpath. A
|
|
72
|
+
symlink sitting alongside its target inside the same scanned directory
|
|
73
|
+
(or reachable via two different scan roots in one `scorePaths` call)
|
|
74
|
+
was counted, and scored, twice. Now deduped by realpath, keeping the
|
|
75
|
+
first lexical path in sorted order. **This can change file/test counts
|
|
76
|
+
for a repo that has such a link** — said plainly here rather than
|
|
77
|
+
folded into "doesn't change what a correctly-shaped suite scores",
|
|
78
|
+
since a suite with this exact shape is exactly what changes.
|
|
79
|
+
- **False "testDir resolves outside the repository" warning on a
|
|
80
|
+
nonexistent `testDir` under a symlinked repo path.** `safeRealpath`
|
|
81
|
+
only resolves the portion of a path that exists on disk, so a
|
|
82
|
+
nonexistent `testDir` kept its literal, unresolved form while
|
|
83
|
+
`repoRoot` (which does exist) got fully resolved — on a host where the
|
|
84
|
+
repo itself sits under a symlinked prefix (macOS: `/tmp` ->
|
|
85
|
+
`/private/tmp`), that asymmetry alone made the two sides disagree and
|
|
86
|
+
fired a false escape warning for an ordinary missing `testDir`.
|
|
87
|
+
`testDirEscapesRepo` now checks existence first and returns "does not
|
|
88
|
+
escape" immediately for a `testDir` that isn't on disk — consistent
|
|
89
|
+
with `resolveConfigScopedRoot`'s own existing "nothing to score there"
|
|
90
|
+
handling for the same case.
|
|
91
|
+
- **Escape warning now prints cwd-relative paths**, matching every other
|
|
92
|
+
`configWarning` in this package, instead of the runner's own absolute
|
|
93
|
+
filesystem paths.
|
|
94
|
+
- **A raw glob-pattern input pointing at a different repo than `cwd` no
|
|
95
|
+
longer hard-fails with 0 matches.** The repo-root containment fix above
|
|
96
|
+
used `cwd`'s own repo boundary for a bare glob pattern regardless of
|
|
97
|
+
where the pattern actually pointed — an absolute pattern into a sibling
|
|
98
|
+
repo (`['/abs/repoB/e2e/*.spec.ts']`), or an equivalent `../`-relative
|
|
99
|
+
one, had every match rejected as an "escape" of `cwd`'s repo, where the
|
|
100
|
+
same target passed as a directory already scored fine. `src/index.ts`'s
|
|
101
|
+
glob-input branch now derives its containment boundary from *the
|
|
102
|
+
pattern's own* literal (non-magic) leading-directory prefix, resolved
|
|
103
|
+
against `cwd`, not from `cwd` itself — a literal prefix that lands
|
|
104
|
+
inside another repo entirely is scored against that repo's own
|
|
105
|
+
boundary, while a symlink an in-bounds prefix still reaches that escapes
|
|
106
|
+
*that* repo is still dropped.
|
|
107
|
+
- **An explicit target and a directory scan that reaches it again through
|
|
108
|
+
a symlink are no longer scored twice.** The realpath dedupe added above
|
|
109
|
+
only ever populated its map from `expanded` matches; an `explicit` file
|
|
110
|
+
and a same-realpath `expanded` symlink are two different literal paths,
|
|
111
|
+
so the separate "explicit wins" step (an exact literal-path delete)
|
|
112
|
+
never caught that pairing. The dedupe map is now seeded with
|
|
113
|
+
`explicit`'s own realpaths first, so a same-realpath `expanded` match is
|
|
114
|
+
recognized as a duplicate instead of surviving as a second, independent
|
|
115
|
+
file.
|
|
116
|
+
|
|
117
|
+
Corpus rerun (100 public repos, directory inputs, run on the round-2 build;
|
|
118
|
+
the round-3 fixes only touch glob-pattern inputs): 96/100 repos score
|
|
119
|
+
identically, 4 moved upstream, 0 changed.
|
|
120
|
+
|
|
121
|
+
## 2.1.0 — 2026-09-24
|
|
122
|
+
|
|
123
|
+
Scoring model stays v4 — nothing here changes a rule's weight or
|
|
124
|
+
severity, only whether a suite is *recognized* correctly in the first
|
|
125
|
+
place (see the note at the top of this file reconciling that with "any
|
|
126
|
+
score-changing release is a new model version"). Every fix below made at
|
|
127
|
+
least one real corpus repo's score more accurate — not uniformly higher,
|
|
128
|
+
and not less strict or more lenient by policy; see "Behaviour change"
|
|
129
|
+
below and VALIDATION.md's "Bugs found this way, and fixed" for the full
|
|
130
|
+
story (corpus repo, exact before/after) behind each one.
|
|
131
|
+
|
|
132
|
+
### Fixed
|
|
133
|
+
- **Custom `test.extend()` fixture names.** A suite that always declares
|
|
134
|
+
its tests via a custom-named fixture (`const loggedTest =
|
|
135
|
+
test.extend(...)`, imported into every spec file — sometimes through a
|
|
136
|
+
re-export barrel) had every `expect()` inside a completely normal test
|
|
137
|
+
flagged by upstream `eslint-plugin-playwright`'s `no-standalone-expect`
|
|
138
|
+
and sibling rules (which key off the literal identifier `test`, and
|
|
139
|
+
can only dereference a same-file extend chain), and undercounted by
|
|
140
|
+
this package's own test-span/`countTests` logic too.
|
|
141
|
+
`src/test-aliases.ts` resolves these aliases — same-file and
|
|
142
|
+
cross-file, through imports and re-export barrels — and feeds both
|
|
143
|
+
`eslint-plugin-playwright`'s `globalAliases.test` setting and this
|
|
144
|
+
package's own AST rules. Scores rise for affected suites:
|
|
145
|
+
`argos-ci/argos` 57/F → 95/A, `apache/superset` 70/C → 77/C.
|
|
146
|
+
- **Discovery now honours the target's own `playwright.config.*`
|
|
147
|
+
`testDir`/`testMatch`/`testIgnore`.** Spec discovery previously matched
|
|
148
|
+
by filename suffix only, so a directory holding a different test
|
|
149
|
+
runner's files under a similar naming convention got swept in
|
|
150
|
+
alongside the real suite. `src/playwright-config.ts` statically parses
|
|
151
|
+
(never executes — a scored repo's config is untrusted input) the
|
|
152
|
+
nearest `playwright.config.{ts,js,mjs,cjs}` at or above the scanned
|
|
153
|
+
directory and scopes discovery to what the target itself declares.
|
|
154
|
+
When no config is found at all, this falls back to the previous
|
|
155
|
+
filename-based behavior silently, exactly as before this release; when
|
|
156
|
+
a config *is* found but can't be read statically (a dynamic value or
|
|
157
|
+
an unsupported shape, e.g. a `RegExp` `testMatch`), it falls back the
|
|
158
|
+
same way but now also adds an entry to the new, additive, optional
|
|
159
|
+
`ScoreResult.configWarnings` field so the caller can see why. Includes
|
|
160
|
+
three fixes found making this work against real configs: matching
|
|
161
|
+
Playwright's own `createFileMatcher` auto-`**/`-prefix behavior for a
|
|
162
|
+
`testMatch`/`testIgnore` pattern with no leading `**/`; globbing from
|
|
163
|
+
the config's `testDir` rather than the caller's own (possibly narrower
|
|
164
|
+
or differently-rooted) scanned directory; and failing *open* (keeping a
|
|
165
|
+
file) rather than treating an unresolvable relative import as proof a
|
|
166
|
+
file isn't a real spec, since a sparse/partial checkout can legitimately
|
|
167
|
+
be missing a fixtures module the real project has. This moves a
|
|
168
|
+
suite's score in *either* direction depending on whether the old
|
|
169
|
+
filename-only discovery had been over- or under-matching relative to
|
|
170
|
+
what the config itself declares — see "Behaviour change" below for a
|
|
171
|
+
concrete case where it lowers a score.
|
|
172
|
+
- **Discovery no longer misses a plain CommonJS suite.** A follow-on to
|
|
173
|
+
the config-scoped discovery fix above: the safety net that confirms a
|
|
174
|
+
config-matched file transitively imports `@playwright/test` only
|
|
175
|
+
walked ES `import`/`export ... from` syntax, so a suite written
|
|
176
|
+
entirely in CommonJS (`require()`, no `import` anywhere) read as
|
|
177
|
+
*definitive negative evidence* of not being a Playwright spec — the
|
|
178
|
+
opposite of the truth — and hard-failed to 0 files the moment
|
|
179
|
+
config-scoped discovery activated for it. Fixed by also recognizing a
|
|
180
|
+
top-level `require('x')` call the same way as an ES import.
|
|
181
|
+
- **Config discovery and a config's `testDir` can no longer escape the
|
|
182
|
+
scanned repo.** Hardening fixes for a public tool that reads an
|
|
183
|
+
untrusted repo's own config. `findPlaywrightConfig` stops walking
|
|
184
|
+
upward at the first ancestor directory containing `.git` (a directory
|
|
185
|
+
for a normal clone, a file for a worktree/submodule), so a coincidental
|
|
186
|
+
`playwright.config.*` sitting above the actual repo root — a shared CI
|
|
187
|
+
runner, a monorepo checkout nested inside another one — is never picked
|
|
188
|
+
up (this guarantee needs a `.git` somewhere in the ancestor chain).
|
|
189
|
+
The bug this closes: when no `.git` existed anywhere in the ancestor
|
|
190
|
+
chain, the boundary used to clamp a config's `testDir` fell back to the
|
|
191
|
+
filesystem root (or 20 levels up), which is effectively no clamp at
|
|
192
|
+
all, so a `testDir: '/'` could make the glob walk the whole filesystem
|
|
193
|
+
(in the worst case, a hang). For the `testDir` clamp the fallback is
|
|
194
|
+
now the directory actually passed to the scorer, or the config file's
|
|
195
|
+
own directory when that's the wider of the two, never the filesystem
|
|
196
|
+
root. The config search itself (a bounded series of existence checks)
|
|
197
|
+
keeps its wider fallback so monorepo configs above the scanned
|
|
198
|
+
directory are still found.
|
|
199
|
+
Symlinks got the same treatment: a `testDir` is now checked *after
|
|
200
|
+
resolving symlinks* on both sides of the ancestor check, not just
|
|
201
|
+
lexically, so a `testDir` that's a symlink to somewhere outside the
|
|
202
|
+
repo can't launder an escape the plain path comparison would have
|
|
203
|
+
missed (the glob's own `follow` option, already `false` by default for
|
|
204
|
+
`**` patterns, is also now pinned explicitly). An escaping `testDir` —
|
|
205
|
+
absolute (`'/'`), climbing out (`'../../..'`), or via a symlink — gets a
|
|
206
|
+
`configWarnings` entry and falls back to filename-based discovery
|
|
207
|
+
rather than globbing the clamped repo root with testMatch/testIgnore
|
|
208
|
+
patterns written for a different directory. To be clear about the
|
|
209
|
+
actual exposure this closes: a scored *result* was never able to
|
|
210
|
+
include a file whose path lies outside the scan root either way (a
|
|
211
|
+
symlinked file inside the repo is a separate, tracked follow-up) — index.ts always
|
|
212
|
+
filters expanded matches back down to the caller's own requested
|
|
213
|
+
directory — the risk was the walk/glob itself reading (or hanging on)
|
|
214
|
+
parts of the filesystem well outside the repo, not a leaked finding in
|
|
215
|
+
the output. Neither case is exercised by any corpus repo (a legitimate
|
|
216
|
+
config never does this); found and fixed during this release's own
|
|
217
|
+
review, not from a corpus regression.
|
|
218
|
+
|
|
219
|
+
### Behaviour change
|
|
220
|
+
- **Config-scoped discovery can lower a suite's score, not just raise
|
|
221
|
+
it.** Scoping to what a target's own `playwright.config.*` actually
|
|
222
|
+
declares is strictly more accurate than the old filename-suffix match,
|
|
223
|
+
but "more accurate" isn't the same as "friendlier": `mattermost/
|
|
224
|
+
mattermost` moves **84/B → 79/C (a pass→fail flip)** because its
|
|
225
|
+
top-level config's `testDir` doesn't cover the real, correctly-written
|
|
226
|
+
`upgrade-specs/` suite it runs only through per-project `testDir`
|
|
227
|
+
overrides — `projects[].testDir` isn't read yet (see VALIDATION.md's
|
|
228
|
+
"Known limitations"). A future config parser that reads
|
|
229
|
+
`projects[].testDir` would recover this repo's full suite and its
|
|
230
|
+
higher score.
|
|
231
|
+
|
|
232
|
+
### Corpus
|
|
233
|
+
Grown from 50 (2.0.0) to 100 public suites: 34 added in an interim,
|
|
234
|
+
unpublished pass (84 total — see VALIDATION.md for the full accounting of
|
|
235
|
+
that pass), then 16 more here for brand
|
|
236
|
+
recognition (Nextcloud, Ghost, Pinterest, Microsoft, The Guardian,
|
|
237
|
+
Shopify ×2, Adobe, Automattic, BBC, Google, Datadog, Twilio) plus three
|
|
238
|
+
named competitors (Checkly, Currents, LambdaTest), scored under the
|
|
239
|
+
exact same rules as everyone else. Every one of the 100 entries in
|
|
240
|
+
VALIDATION.md now carries the exact commit SHA scored and the date it
|
|
241
|
+
was tested, not just an aggregate "last run" date.
|
|
6
242
|
|
|
7
243
|
## 2.0.0 — 2026-09-23
|
|
8
244
|
|
|
@@ -37,7 +273,7 @@ own official examples must still score >= 90/A (see
|
|
|
37
273
|
`tests/official-examples.test.ts`, vendored from microsoft/playwright),
|
|
38
274
|
and a hand-written "gold" fixture must still reach exactly 100.
|
|
39
275
|
|
|
40
|
-
### Added — four rules narrowing the
|
|
276
|
+
### Added — four rules narrowing the gameability gap (scored under `standard`)
|
|
41
277
|
Narrowing, not closing: each rule matches specific known patterns (see the
|
|
42
278
|
respellings each one closed in this same release), not the general class
|
|
43
279
|
of "code that fakes stability or coverage" — a determined author can
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/qa-guardian/playwright-score/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@qaguardian/playwright-score)
|
|
5
|
-
[](
|
|
5
|
+
[](https://github.com/qa-guardian/playwright-score/blob/main/LICENSE)
|
|
6
6
|
|
|
7
7
|
**Deterministic, AI-free quality score for Playwright specs** — the
|
|
8
8
|
weighted share of your tests that are clean.
|
|
@@ -19,76 +19,63 @@ The score never calls an LLM. AI may *generate* or *repair* code using findings;
|
|
|
19
19
|
|
|
20
20
|
## Real-world results
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
100 public Playwright suites, scored with the published package against each
|
|
23
23
|
project's actual source — chosen to include both well-known,
|
|
24
|
-
heavily-engineered platforms and smaller, less mature projects
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
heavily-engineered platforms and smaller, less mature projects, plus three
|
|
25
|
+
named testing-infra competitors scored under the exact same rules as
|
|
26
|
+
everyone else. Not curated to look good: 35 of 100 fail the default
|
|
27
|
+
threshold, for real, verifiable reasons (see
|
|
28
|
+
[VALIDATION.md](https://github.com/qa-guardian/playwright-score/blob/main/VALIDATION.md),
|
|
29
|
+
which also has the full before/after against the last published version
|
|
30
|
+
and every one of the 100 entries, not just the 15 shown below).
|
|
28
31
|
|
|
29
|
-
**
|
|
32
|
+
**100 suites · 7,281 files · 39,059 tests · 65/100 pass (80% threshold) ·
|
|
33
|
+
tested on 2026-09-24**
|
|
34
|
+
|
|
35
|
+
Top scorers (everyone at 97+; the next tier down is a 6-way tie at 96 — see
|
|
36
|
+
VALIDATION.md for the full 100):
|
|
37
|
+
|
|
38
|
+
| Repo (source scanned) | Score | Grade | Result |
|
|
39
|
+
|---|---:|:-:|:-:|
|
|
40
|
+
| [pinterest/gestalt](https://github.com/pinterest/gestalt/tree/22874a7522d1/playwright/accessibility) | 100/100 | A | PASS |
|
|
41
|
+
| [TryGhost/Ghost](https://github.com/TryGhost/Ghost/tree/9d7248b56d53/e2e/tests) | 99/100 | A | PASS |
|
|
42
|
+
| [hashicorp/vault](https://github.com/hashicorp/vault/tree/f03da019aba5/ui/e2e) | 98/100 | A | PASS |
|
|
43
|
+
| [microsoft/playwright (TodoMVC example)](https://github.com/microsoft/playwright/tree/7563c5a67eab/examples/todomvc) | 98/100 | A | PASS |
|
|
44
|
+
| [plausible/analytics](https://github.com/plausible/analytics/tree/8f3970104aa7/e2e) | 98/100 | A | PASS |
|
|
45
|
+
| [segmentio/analytics-next](https://github.com/segmentio/analytics-next/tree/2d6d55301804/packages/browser-integration-tests/src) | 98/100 | A | PASS |
|
|
46
|
+
| [cloudflare/templates](https://github.com/cloudflare/templates/tree/a0bb6ef9a990/playwright-tests) | 97/100 | A | PASS |
|
|
47
|
+
| [glpi-project/glpi](https://github.com/glpi-project/glpi/tree/9e8a9570a90b/tests/e2e/specs) | 97/100 | A | PASS |
|
|
48
|
+
| [shopify/hydrogen](https://github.com/shopify/hydrogen/tree/3ef7ec23096a/e2e/specs) | 97/100 | A | PASS |
|
|
49
|
+
|
|
50
|
+
Bottom 5 scorers:
|
|
30
51
|
|
|
31
52
|
| Repo (source scanned) | Score | Grade | Result |
|
|
32
53
|
|---|---:|:-:|:-:|
|
|
33
|
-
| [
|
|
34
|
-
| [
|
|
35
|
-
| [
|
|
36
|
-
| [
|
|
37
|
-
| [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| [Element (Matrix client)](https://github.com/element-hq/element-web/tree/3153cc6ae123/apps/web/playwright) | 89/100 | B | PASS |
|
|
54
|
-
| [Clerk](https://github.com/clerk/javascript/tree/81a641348101/integration/tests) | 87/100 | B | PASS |
|
|
55
|
-
| [Grafana](https://github.com/grafana/grafana/tree/1fd1d6398053/e2e-playwright) | 87/100 | B | PASS |
|
|
56
|
-
| [PostHog](https://github.com/PostHog/posthog/tree/854a4262ec8b/playwright) | 87/100 | B | PASS |
|
|
57
|
-
| [Formbricks](https://github.com/formbricks/formbricks/tree/55ade3bc2a5a/apps/web/playwright) | 85/100 | B | PASS |
|
|
58
|
-
| [Bruno](https://github.com/usebruno/bruno/tree/f1b433f7b06f/tests) | 84/100 | B | PASS |
|
|
59
|
-
| [Mattermost](https://github.com/mattermost/mattermost/tree/f0b63f8ed11e/e2e-tests/playwright) | 84/100 | B | PASS |
|
|
60
|
-
| [Saleor Dashboard](https://github.com/saleor/saleor-dashboard/tree/3508127f50dc/playwright) | 84/100 | B | PASS |
|
|
61
|
-
| [WordPress.com Calypso](https://github.com/Automattic/wp-calypso/tree/af6245bcd266/test/e2e) | 84/100 | B | PASS |
|
|
62
|
-
| [sencho](https://github.com/Studio-Saelix/sencho/tree/489e1a9a9a90/e2e) | 84/100 | B | PASS |
|
|
63
|
-
| [Handsontable](https://github.com/handsontable/handsontable/tree/40c2738b2e87/tests/e2e) | 83/100 | B | PASS |
|
|
64
|
-
| [Rocket.Chat](https://github.com/RocketChat/Rocket.Chat/tree/828fa06f72d5/apps/meteor/tests/e2e) | 83/100 | B | PASS |
|
|
65
|
-
| [novu](https://github.com/novuhq/novu/tree/6fc9d02ea001/apps/dashboard/tests) | 82/100 | B | PASS |
|
|
66
|
-
| [Appsmith](https://github.com/appsmithorg/appsmith/tree/a72a95b73a99/app/client/playwright/tests) | 80/100 | B | PASS |
|
|
67
|
-
| [Payload CMS](https://github.com/payloadcms/payload/tree/5f9bcac2f07b/test/admin/e2e) | 79/100 | C | FAIL |
|
|
68
|
-
| [Immich](https://github.com/immich-app/immich/tree/f48d4b332127/e2e/src/specs) | 78/100 | C | FAIL |
|
|
69
|
-
| [VS Code (component fixtures)](https://github.com/microsoft/vscode/tree/7b24b6303075/test/componentFixtures/playwright) | 77/100 | C | FAIL |
|
|
70
|
-
| [Astro](https://github.com/withastro/astro/tree/40896acb7449/packages/astro/e2e) | 75/100 | C | FAIL |
|
|
71
|
-
| [AFFiNE](https://github.com/toeverything/AFFiNE/tree/d897bb3d8409/tests/blocksuite/e2e) | 72/100 | C | FAIL |
|
|
72
|
-
| [Builder.io SDKs](https://github.com/BuilderIO/builder/tree/51cd0521bbf1/packages/sdks-tests/src/e2e-tests) | 72/100 | C | FAIL |
|
|
73
|
-
| [openplayerjs](https://github.com/openplayerjs/openplayerjs/tree/ee1673106237/e2e) | 72/100 | C | FAIL |
|
|
74
|
-
| [tldraw](https://github.com/tldraw/tldraw/tree/a6cfe996e1df/apps/examples/e2e) | 72/100 | C | FAIL |
|
|
75
|
-
| [cal.com](https://github.com/calcom/cal.com/tree/54343aa685ae/apps/web/playwright) | 71/100 | C | FAIL |
|
|
76
|
-
| [Qwik](https://github.com/QwikDev/qwik/tree/02b5c6b18fe7/e2e) | 70/100 | C | FAIL |
|
|
77
|
-
| [Wekan](https://github.com/wekan/wekan/tree/84414711b7ae/tests/playwright/specs) | 70/100 | C | FAIL |
|
|
78
|
-
| [livecodes](https://github.com/live-codes/livecodes/tree/747054828256/e2e) | 70/100 | C | FAIL |
|
|
79
|
-
| [OpenReplay](https://github.com/openreplay/openreplay/tree/3fce37d89113/frontend/tests/playwright) | 66/100 | D | FAIL |
|
|
80
|
-
| [Flagsmith](https://github.com/Flagsmith/flagsmith/tree/66dc2e9ed00b/frontend/e2e/tests) | 62/100 | D | FAIL |
|
|
81
|
-
| [Activepieces](https://github.com/activepieces/activepieces/tree/ab8cd5c9069e/packages/tests-e2e) | 58/100 | F | FAIL |
|
|
82
|
-
| [TheCyberHub](https://github.com/th3cyb3rhub/TheCyberHub/tree/1cf418654f16/e2e) | 56/100 | F | FAIL |
|
|
83
|
-
|
|
84
|
-
Repo names link to the exact source scanned. Full methodology, findings
|
|
85
|
-
breakdown, and `scripts/validate-corpus.sh` to reproduce every number here
|
|
86
|
-
yourself live in [VALIDATION.md](./VALIDATION.md). A self-contained visual
|
|
54
|
+
| [Flagsmith/flagsmith](https://github.com/Flagsmith/flagsmith/tree/ea5ad826e85b/frontend/e2e/tests) | 62/100 | D | FAIL |
|
|
55
|
+
| [activepieces/activepieces](https://github.com/activepieces/activepieces/tree/9e493a87f471/packages/tests-e2e) | 58/100 | F | FAIL |
|
|
56
|
+
| [th3cyb3rhub/TheCyberHub](https://github.com/th3cyb3rhub/TheCyberHub/tree/1cf418654f16/e2e) | 56/100 | F | FAIL |
|
|
57
|
+
| [DataDog/documentation](https://github.com/DataDog/documentation/tree/6cd10f40cf46/hugo/e2e) | 50/100 | F | FAIL |
|
|
58
|
+
| [getsentry/spotlight](https://github.com/getsentry/spotlight/tree/3dd5eb5b315f/packages/spotlight/tests) | 33/100 | F | FAIL |
|
|
59
|
+
|
|
60
|
+
Repo names link to the exact commit scanned. Selected by a mix of
|
|
61
|
+
mechanical GitHub search (real `@playwright/test` usage, hand-verified)
|
|
62
|
+
and, for the most recent 16, hand-picked brand recognition — three of the
|
|
63
|
+
100 are named competitors' own example suites (Checkly, Currents,
|
|
64
|
+
LambdaTest), scored with no separate bar. Full methodology, the complete
|
|
65
|
+
100-repo table, findings breakdown, and
|
|
66
|
+
`scripts/validate-corpus.sh` to reproduce every number here yourself live
|
|
67
|
+
in
|
|
68
|
+
[VALIDATION.md](https://github.com/qa-guardian/playwright-score/blob/main/VALIDATION.md).
|
|
69
|
+
A machine-readable
|
|
70
|
+
[`validation.json`](https://github.com/qa-guardian/playwright-score/blob/main/validation.json)
|
|
71
|
+
carries the same per-repo data for tooling. See
|
|
72
|
+
[CHANGELOG.md](https://github.com/qa-guardian/playwright-score/blob/main/CHANGELOG.md)
|
|
73
|
+
for what changed release to release. A self-contained visual
|
|
87
74
|
report of an earlier five-suite audit lives at
|
|
88
|
-
[`docs/scorecard.html`](
|
|
75
|
+
[`docs/scorecard.html`](https://github.com/qa-guardian/playwright-score/blob/main/docs/scorecard.html) — host it wherever
|
|
89
76
|
(GitHub Pages, qaguardian.com, ...).
|
|
90
77
|
|
|
91
|
-
See [METHODOLOGY.md](
|
|
78
|
+
See [METHODOLOGY.md](https://github.com/qa-guardian/playwright-score/blob/main/METHODOLOGY.md) for the full scoring model, or the product write-up on the [landing page](https://qaguardian.com/open-source/playwright-score).
|
|
92
79
|
|
|
93
80
|
## Install
|
|
94
81
|
|
package/dist/eslint-runner.d.ts
CHANGED
|
@@ -9,4 +9,6 @@ export declare function runEslint(options: {
|
|
|
9
9
|
cwd?: string;
|
|
10
10
|
/** See findLocalAssertionHelperNames in metrics.ts. */
|
|
11
11
|
assertFunctionNames?: string[];
|
|
12
|
+
/** See detectExtendedTestAliases in test-aliases.ts. */
|
|
13
|
+
testAliasNames?: string[];
|
|
12
14
|
}): Promise<Finding[]>;
|
package/dist/eslint-runner.js
CHANGED
|
@@ -4,10 +4,28 @@ import { commonAncestorDir } from './fs-util.js';
|
|
|
4
4
|
import playwright from 'eslint-plugin-playwright';
|
|
5
5
|
import { basePlugin } from './rules/base-plugin.js';
|
|
6
6
|
import { mapRule } from './profiles.js';
|
|
7
|
-
function buildConfig(assertFunctionNames) {
|
|
7
|
+
function buildConfig(assertFunctionNames, testAliasNames) {
|
|
8
8
|
const base = [
|
|
9
9
|
{
|
|
10
10
|
files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
|
|
11
|
+
// Identifiers bound to a `test.extend(...)`-derived fixture under a
|
|
12
|
+
// custom name (test-aliases.ts) — upstream eslint-plugin-playwright
|
|
13
|
+
// already dereferences a *same-file* extend chain internally, but
|
|
14
|
+
// can't follow one across files (its own `dereference` is scoped to
|
|
15
|
+
// the current file); this setting is what makes a cross-file alias
|
|
16
|
+
// (`import { loggedTest } from './fixtures'`) resolve to `test` for
|
|
17
|
+
// every rule that keys off the literal identifier — no-standalone-
|
|
18
|
+
// expect, no-conditional-expect, no-conditional-in-test, valid-expect,
|
|
19
|
+
// no-focused-test, and others built on the same parseFnCall/
|
|
20
|
+
// resolveToPlaywrightFn helper. Verified against two real corpus
|
|
21
|
+
// repos (argos-ci/argos's `loggedTest`, apache/superset's
|
|
22
|
+
// `testWithAssets`) where this exact cross-file shape was the actual
|
|
23
|
+
// false-positive cause.
|
|
24
|
+
settings: {
|
|
25
|
+
playwright: {
|
|
26
|
+
globalAliases: { test: testAliasNames },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
11
29
|
// A pre-2.0.0 review found `/* eslint-disable */` on line 1 of a
|
|
12
30
|
// 10-test file (waitForTimeout + expect(true).toBe(true) in every
|
|
13
31
|
// test) took it from 35/F to 99/A — every rule this package enables
|
|
@@ -83,7 +101,7 @@ function buildConfig(assertFunctionNames) {
|
|
|
83
101
|
// with the same severity for the actual anti-pattern (an
|
|
84
102
|
// always-skipped test declaration) only.
|
|
85
103
|
'playwright/no-skipped-test': 'off',
|
|
86
|
-
'pwscore/no-skipped-test-declaration': 'warn',
|
|
104
|
+
'pwscore/no-skipped-test-declaration': ['warn', { testAliasNames }],
|
|
87
105
|
'playwright/no-raw-locators': 'warn',
|
|
88
106
|
// Both were mapped in profiles.ts (report-only, so they don't
|
|
89
107
|
// double-penalize the ratio-based locators dimension) but never
|
|
@@ -135,7 +153,7 @@ function buildConfig(assertFunctionNames) {
|
|
|
135
153
|
'pwscore/no-timer-sleep': 'error',
|
|
136
154
|
'pwscore/no-coordinate-click': 'warn',
|
|
137
155
|
'pwscore/no-trivial-assertion': 'error',
|
|
138
|
-
'pwscore/no-soft-assertion-only-test': 'warn',
|
|
156
|
+
'pwscore/no-soft-assertion-only-test': ['warn', { testAliasNames }],
|
|
139
157
|
},
|
|
140
158
|
},
|
|
141
159
|
];
|
|
@@ -166,7 +184,7 @@ export async function runEslint(options) {
|
|
|
166
184
|
const cwd = options.files.length > 0
|
|
167
185
|
? commonAncestorDir(options.files)
|
|
168
186
|
: (options.cwd ?? process.cwd());
|
|
169
|
-
const overrideConfig = buildConfig(options.assertFunctionNames ?? []);
|
|
187
|
+
const overrideConfig = buildConfig(options.assertFunctionNames ?? [], options.testAliasNames ?? []);
|
|
170
188
|
// Try to attach typescript-eslint parser for .ts files
|
|
171
189
|
try {
|
|
172
190
|
const tsParser = await import('@typescript-eslint/parser');
|
|
@@ -32,5 +32,13 @@ export function formatMarkdown(result) {
|
|
|
32
32
|
lines.push('');
|
|
33
33
|
lines.push('</details>');
|
|
34
34
|
}
|
|
35
|
+
if (result.configWarnings?.length) {
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push('## Config warnings');
|
|
38
|
+
lines.push('');
|
|
39
|
+
for (const w of result.configWarnings) {
|
|
40
|
+
lines.push(`- ${w}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
35
43
|
return lines.join('\n');
|
|
36
44
|
}
|
package/dist/formatters/text.js
CHANGED
|
@@ -36,6 +36,13 @@ export function formatText(result) {
|
|
|
36
36
|
lines.push(` … and ${result.skippedFiles.length - 20} more`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
if (result.configWarnings?.length) {
|
|
40
|
+
lines.push('');
|
|
41
|
+
lines.push('Config warnings:');
|
|
42
|
+
for (const w of result.configWarnings) {
|
|
43
|
+
lines.push(` ${w}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
39
46
|
return lines.join('\n');
|
|
40
47
|
}
|
|
41
48
|
function pathBasename(p) {
|
package/dist/import-graph.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function resolveRelativeImport(fromFile: string, specifier: string, boundaryDir: string): string | undefined;
|
|
1
2
|
/**
|
|
2
3
|
* A Page Object Model is common in real Playwright suites: spec files
|
|
3
4
|
* interact with the page entirely through methods on an imported class
|
package/dist/import-graph.js
CHANGED
|
@@ -35,7 +35,7 @@ function extractRelativeImportSpecifiers(source, file) {
|
|
|
35
35
|
}
|
|
36
36
|
return specifiers;
|
|
37
37
|
}
|
|
38
|
-
function resolveRelativeImport(fromFile, specifier, boundaryDir) {
|
|
38
|
+
export function resolveRelativeImport(fromFile, specifier, boundaryDir) {
|
|
39
39
|
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
40
40
|
const candidates = [];
|
|
41
41
|
for (const ext of RESOLVE_EXTENSIONS)
|