@qaguardian/playwright-score 1.0.0 → 2.0.0
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 +689 -0
- package/METHODOLOGY.md +53 -6
- package/README.md +57 -23
- package/dist/cli.js +8 -2
- package/dist/eslint-runner.js +36 -0
- package/dist/index.js +7 -2
- package/dist/metrics.d.ts +1 -0
- package/dist/metrics.js +32 -1
- package/dist/profiles.d.ts +15 -0
- package/dist/profiles.js +34 -0
- package/dist/rules/base-plugin.d.ts +4 -0
- package/dist/rules/base-plugin.js +657 -0
- package/dist/score-engine.d.ts +9 -1
- package/dist/score-engine.js +9 -1
- package/dist/types.d.ts +13 -1
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. Any change that
|
|
4
|
+
alters what a given suite scores is a new scoring-model version (see
|
|
5
|
+
[METHODOLOGY.md](./METHODOLOGY.md)), not a quiet patch.
|
|
6
|
+
|
|
7
|
+
## 2.0.0 — 2026-09-23
|
|
8
|
+
|
|
9
|
+
**Scoring model v4 — `standard` scores change.** Owner direction: public
|
|
10
|
+
scores should reflect quality, full stop, not be softened to avoid moving
|
|
11
|
+
anyone's number ("I don't care who fails"). This supersedes the 1.1.0
|
|
12
|
+
plan of shipping these same rules as report-only under `standard`; 1.1.0
|
|
13
|
+
was never published. See METHODOLOGY.md's "Why v4" for the full
|
|
14
|
+
reasoning and VALIDATION.md for the complete before/after table across
|
|
15
|
+
all 50 corpus repos, including every pass→fail flip.
|
|
16
|
+
|
|
17
|
+
Corpus expanded from 17 to 50 real public repos (see
|
|
18
|
+
`scripts/validate-corpus.sh`) — every rule below was checked against real
|
|
19
|
+
findings across all 50 before landing, not just the fixtures.
|
|
20
|
+
|
|
21
|
+
### Scores changed
|
|
22
|
+
`standard` scores are **not** unchanged in this release (that was 1.1.0's
|
|
23
|
+
promise; it's void under v4). Against the last actually-published version
|
|
24
|
+
(1.0.0, model v3, none of these rules): 20/50 corpus repos move. Net
|
|
25
|
+
**+22** across the corpus, but that's almost entirely Flagsmith's `.pw.ts`
|
|
26
|
+
discovery fix alone (+62, previously hard-failed at 0 — its 20-spec suite
|
|
27
|
+
wasn't found at all). Excluding Flagsmith, the other 19 movers are **net
|
|
28
|
+
−40** (mean −2.1/repo) — real, quality-driven drops. Biggest: tldraw −9,
|
|
29
|
+
AFFiNE −5, activepieces −5, microsoft/vscode −4, sencho −2, cal.com −2,
|
|
30
|
+
then thirteen repos at −1. **Three pass→fail flips** at the default
|
|
31
|
+
threshold (80): `payloadcms/payload` (80→79), `microsoft/vscode`
|
|
32
|
+
(81→77), `tldraw/tldraw` (81→72). Pass rate: 37/50 (74%) → 34/50 (68%).
|
|
33
|
+
See VALIDATION.md for the full per-repo table and the "Known
|
|
34
|
+
limitations" this release documents rather than papers over. Two
|
|
35
|
+
acceptance bars now guard against these rules over-correcting: Playwright's
|
|
36
|
+
own official examples must still score >= 90/A (see
|
|
37
|
+
`tests/official-examples.test.ts`, vendored from microsoft/playwright),
|
|
38
|
+
and a hand-written "gold" fixture must still reach exactly 100.
|
|
39
|
+
|
|
40
|
+
### Added — four rules narrowing the QAG-196 gameability gap (scored under `standard`)
|
|
41
|
+
Narrowing, not closing: each rule matches specific known patterns (see the
|
|
42
|
+
respellings each one closed in this same release), not the general class
|
|
43
|
+
of "code that fakes stability or coverage" — a determined author can
|
|
44
|
+
likely still find an unmatched spelling. Real signal on real code, not a
|
|
45
|
+
guarantee nothing slips through.
|
|
46
|
+
- **`pwscore/no-timer-sleep`** (error) — flags a hard-coded sleep
|
|
47
|
+
disguised past `playwright/no-wait-for-timeout`: `await new
|
|
48
|
+
Promise((resolve) => setTimeout(resolve, ms))`, its `globalThis.
|
|
49
|
+
setTimeout` and wrapped-callback (`setTimeout(() => resolve(), ms)`)
|
|
50
|
+
respellings, and the unrelated but equally invisible `node:timers/
|
|
51
|
+
promises` sleep (`await setTimeout(ms)`).
|
|
52
|
+
- **`pwscore/no-coordinate-click`** (**warning**, not error — see below)
|
|
53
|
+
— flags coordinate-based mouse interaction (`page.mouse.click/dblclick/
|
|
54
|
+
move/down/up(x, y)`, including a destructured `const { mouse } = page`
|
|
55
|
+
binding) in place of a locator. **Known false positive, mitigated, not
|
|
56
|
+
eliminated**: this is also the *correct* way to interact with
|
|
57
|
+
canvas-based apps (whiteboard/drawing-tool suites with no addressable
|
|
58
|
+
DOM element) — two infinite-canvas editors in the corpus (tldraw,
|
|
59
|
+
AFFiNE) account for 428/459 of this rule's hits. Demoted to a
|
|
60
|
+
warning-level (0.4, not 1.0) demerit rather than skipped for canvas
|
|
61
|
+
apps by heuristic, which would itself become a new gameability surface
|
|
62
|
+
— see METHODOLOGY.md's "Why v4" and VALIDATION.md's "Known
|
|
63
|
+
limitations".
|
|
64
|
+
- **`pwscore/no-trivial-assertion`** (error) — flags an assertion whose
|
|
65
|
+
subject and matcher combination can only ever pass regardless of
|
|
66
|
+
anything the app under test does (`expect(true).toBe(true)`,
|
|
67
|
+
`expect([]).toEqual([])`, `` expect(`x`).toBe(`x`) ``, `expect(1 +
|
|
68
|
+
1).toBe(2)`). Always passes, satisfies `playwright/expect-expect`,
|
|
69
|
+
verifies nothing about the app. Never flags a deliberate force-fail
|
|
70
|
+
assertion that always *fails* (`expect(true).toBe(false)`,
|
|
71
|
+
`expect(true, msg).toBeFalsy()`).
|
|
72
|
+
- **`pwscore/no-soft-assertion-only-test`** (warning) — flags a test
|
|
73
|
+
whose every assertion is `expect.soft(...)`, so a failed check never
|
|
74
|
+
fails the run. `expect.poll(...)` counts as a hard assertion (a failed
|
|
75
|
+
poll still throws). Soft assertions are a legitimate, deliberate choice
|
|
76
|
+
(gather every problem in one pass) in many suites, but a warning-level
|
|
77
|
+
(not error) demerit as the default now that it's scored, not report-
|
|
78
|
+
only.
|
|
79
|
+
|
|
80
|
+
### Added — inline `eslint-disable` comments are now a finding, not invisible
|
|
81
|
+
`pwscore/eslint-disable-comment` (warning) flags every ESLint suppression-
|
|
82
|
+
directive comment (`eslint-disable`, `eslint-disable-line`,
|
|
83
|
+
`eslint-disable-next-line`, block or line form) in a scored file — a real
|
|
84
|
+
demerit, same as everything else in this release. **Known limitation**:
|
|
85
|
+
it reports the presence of the comment, not which specific rule or
|
|
86
|
+
finding it silences — see VALIDATION.md.
|
|
87
|
+
|
|
88
|
+
### Fixed — an inline `eslint-disable` comment could still silence every finding it named
|
|
89
|
+
Found in review, before this ever shipped: adding the finding above was
|
|
90
|
+
not enough on its own — every rule this package enables is an ordinary
|
|
91
|
+
ESLint rule, and ESLint honors an inline disable comment by default, so
|
|
92
|
+
`/* eslint-disable */` on line 1 still silenced everything it named for
|
|
93
|
+
scoring purposes, same as it always had. A 10-test file (a hard wait plus
|
|
94
|
+
a tautological assertion in every test) scored 35/F; the same file with
|
|
95
|
+
that one comment on line 1 scored **99/A** — the new
|
|
96
|
+
`pwscore/eslint-disable-comment` finding was reported, but the four real
|
|
97
|
+
findings it was covering for were simply gone. ESLint 9's flat-config
|
|
98
|
+
`linterOptions.noInlineConfig` (see `eslint-runner.ts`) now makes every
|
|
99
|
+
inline directive comment inert for this package's rule set: the comment
|
|
100
|
+
itself is still scored via `pwscore/eslint-disable-comment`, but it can
|
|
101
|
+
no longer also make the finding it targets disappear. The "Known
|
|
102
|
+
limitation" above is narrower as a result — a disable comment can no
|
|
103
|
+
longer silence anything this package's own rules find, it just isn't
|
|
104
|
+
attributed to which specific rule it named.
|
|
105
|
+
|
|
106
|
+
### Removed — `strict` profile
|
|
107
|
+
1.1.0 (never published) planned a `strict` profile to gate the four rules
|
|
108
|
+
above and the `eslint-disable-comment` finding behind an opt-in, leaving
|
|
109
|
+
`standard` unchanged. With all of them scored under `standard` directly
|
|
110
|
+
as of v4, `strict` would be byte-for-byte identical to `standard` —
|
|
111
|
+
nothing left to gate. `--profile strict` on the CLI now errors with a
|
|
112
|
+
migration message; the library API degrades a legacy `'strict'` string
|
|
113
|
+
to `standard` weights, same fallback as the `guardian` profile's removal
|
|
114
|
+
in 1.0.0.
|
|
115
|
+
|
|
116
|
+
### Fixed — `.pw.ts` spec discovery
|
|
117
|
+
Directory/glob discovery now recognizes `*.pw.ts` (joining `.spec.`,
|
|
118
|
+
`.test.`, `.e2e.`, `.e2e-spec.`, `.e2e-test.`) — Flagsmith's entire
|
|
119
|
+
20-spec `frontend/e2e` suite (`billing-test.pw.ts`, `flag-tests.pw.ts`,
|
|
120
|
+
...) previously hard-failed as "no files matched" despite being a fully
|
|
121
|
+
healthy, populated suite, the same failure mode `.e2e.ts`/`.e2e-spec.ts`
|
|
122
|
+
were fixed for in 1.0.0 and its predecessors.
|
|
123
|
+
|
|
124
|
+
### Added — acceptance bars: official examples score high, 100 is reachable
|
|
125
|
+
`tests/official-examples.test.ts` vendors unmodified (attribution header
|
|
126
|
+
only) specs from microsoft/playwright (Apache-2.0; see
|
|
127
|
+
`fixtures/playwright-official/NOTICE.md`) and asserts each scores >= 90
|
|
128
|
+
and grades A under `standard` — three `examples/todomvc` specs (100/A
|
|
129
|
+
each) and `examples/github-api/tests/test-api.spec.ts` (97/A). Checked
|
|
130
|
+
that the 97, not 100, isn't an unfair penalty: the file's only finding is
|
|
131
|
+
`pwscore/eslint-disable-comment` on its own `/* eslint-disable
|
|
132
|
+
notice/notice */` licence-header lint escape (Playwright's monorepo
|
|
133
|
+
convention, unrelated to Playwright/test quality — see NOTICE.md), −3
|
|
134
|
+
points (structure dimension 100→80, weighted). That's correct, not a
|
|
135
|
+
bug: this project scores every suppression-directive comment as a real,
|
|
136
|
+
unconditional demerit regardless of what it's aimed at (see the "Added —
|
|
137
|
+
inline eslint-disable comments" entry above and VALIDATION.md's Known
|
|
138
|
+
limitations) — a licence-header opt-out is still, mechanically, an
|
|
139
|
+
`eslint-disable` comment. 97/A comfortably clears the >= 90 bar either
|
|
140
|
+
way.
|
|
141
|
+
`examples/svgomg/tests/example.spec.ts` was investigated and excluded
|
|
142
|
+
(79/C, genuine non-idiomatic CSS/text-selector usage predating current
|
|
143
|
+
Playwright locator guidance, not a scorer bug — documented in NOTICE.md).
|
|
144
|
+
A new hand-written `fixtures/gold.spec.ts` scores exactly 100 with zero
|
|
145
|
+
findings, proving 100 is reachable, not just a theoretical ceiling.
|
|
146
|
+
|
|
147
|
+
## 1.0.0 — 2026-08-28
|
|
148
|
+
|
|
149
|
+
First stable release. The scoring model (v3), CLI contract, library API,
|
|
150
|
+
and GitHub Action inputs/outputs are now considered stable — breaking
|
|
151
|
+
changes to any of them mean a major version bump from here on.
|
|
152
|
+
|
|
153
|
+
### Fixed — locator ratio
|
|
154
|
+
- **`getByAltText` and `getByTitle` now count as native locators.** They
|
|
155
|
+
are part of Playwright's own `getBy*` family but were missing from the
|
|
156
|
+
native set, so an image-heavy suite using alt/title locators plus a
|
|
157
|
+
single `.locator()` fallback scored 0/100 on the locators dimension for
|
|
158
|
+
perfectly idiomatic code. Scores can only improve or stay the same under
|
|
159
|
+
this fix (the native count grows; raw counting is unchanged). The
|
|
160
|
+
17-suite validation corpus was re-run — see VALIDATION.md.
|
|
161
|
+
|
|
162
|
+
### Added — public API for composing layers
|
|
163
|
+
- `getTestSpans` and `attributeFindingScopes` are now exported. A caller
|
|
164
|
+
layering its own rules on top of `standard` (an extra private dimension,
|
|
165
|
+
say) can attribute its findings to test/hook/module scope with the exact
|
|
166
|
+
logic the package uses internally, then score the dimension with the
|
|
167
|
+
already-exported `ratioDimensionScore` — reused, not reimplemented, so
|
|
168
|
+
the math can't drift.
|
|
169
|
+
|
|
170
|
+
## 0.5.0 — 2026-08-27
|
|
171
|
+
|
|
172
|
+
### Changed — scoring model v3: "the weighted share of your tests that are clean"
|
|
173
|
+
- **The density model is gone.** v1/v2 scored findings per 25 lines of
|
|
174
|
+
code through an exponential decay with a minimum-slots floor. That was
|
|
175
|
+
unexplainable ("0.5 penalty units per slot"), structurally lenient on
|
|
176
|
+
small suites, and gameable — padding a suite with clean lines diluted
|
|
177
|
+
the penalty. All of it (K, slot divisor, minimum slots, per-rule caps,
|
|
178
|
+
`e^-x`) is removed.
|
|
179
|
+
- **Model v3**: every finding is attributed to the test it sits in via AST
|
|
180
|
+
test spans. A test accumulates demerits per dimension (error 1.0,
|
|
181
|
+
warning 0.4, capped at 1 — a test is at worst fully flawed); findings in
|
|
182
|
+
before/after hooks demerit every test in the file; module-level findings
|
|
183
|
+
count once per file. Each dimension is
|
|
184
|
+
`100 × (1 − demerits/tests)`; locators stays the native/raw usage
|
|
185
|
+
ratio; final score is the weighted sum (40/25/20/15), unchanged grades.
|
|
186
|
+
- The motivating example (3 tests: two hard waits, two assertion-free, one
|
|
187
|
+
skipped) scores **53/F** — it was 78/C under v1 density math and 64/D
|
|
188
|
+
under v2.
|
|
189
|
+
- **In-house acronyms removed**: the score version is now plainly `v3`
|
|
190
|
+
(JSON `scoreVersion: "v3"`); "sqs-*" no longer appears anywhere in
|
|
191
|
+
output, docs, or code.
|
|
192
|
+
- `summary.cleanTests` added (tests with no demerit in any dimension);
|
|
193
|
+
text output now prints `clean tests=N/M`.
|
|
194
|
+
- **Removed exports**: `SQS_V1`, `SQS_V2`, `penaltyDimensionScore`,
|
|
195
|
+
`applyPerRuleCap` (the density machinery). `MODEL` and
|
|
196
|
+
`ratioDimensionScore` replace them. Callers composing house rules on
|
|
197
|
+
top (QA Guardian's `playwright_runner`) should migrate when they bump.
|
|
198
|
+
|
|
199
|
+
## 0.4.0 — 2026-08-27
|
|
200
|
+
|
|
201
|
+
### Changed — score version `sqs-v2`
|
|
202
|
+
- **Assertions dimension is now a per-test coverage ratio** instead of a
|
|
203
|
+
finding-density penalty. sqs-v1's density math misread assertion
|
|
204
|
+
coverage badly on small suites: 2 of 3 tests asserting nothing scored
|
|
205
|
+
82/100 on the dimension (and the overall example suite 78/C) because two
|
|
206
|
+
findings in a tiny file barely register per-SLOC. Whether a test asserts
|
|
207
|
+
anything is a fraction of tests, not a smell rate — so, like the
|
|
208
|
+
locators dimension, the primary signal is now
|
|
209
|
+
`coverage = assertedTests / tests` (an **uncapped** census of
|
|
210
|
+
`playwright/expect-expect` findings, taken after all delegation
|
|
211
|
+
resolution), decayed multiplicatively by the density load of the
|
|
212
|
+
remaining assertion-quality rules (`valid-expect`,
|
|
213
|
+
`no-standalone-expect`, `prefer-web-first-assertions`). The motivating
|
|
214
|
+
example now scores the dimension 33 and the suite 64/D.
|
|
215
|
+
- Everything else — weights, hygiene/structure density math, locator
|
|
216
|
+
ratio, constants, grades — is unchanged. Per METHODOLOGY's change
|
|
217
|
+
policy this is a new score version: results now report `sqs-v2`.
|
|
218
|
+
- `summary.unassertedTests` added to the JSON output; the text formatter
|
|
219
|
+
prints `tests asserting=N/M`.
|
|
220
|
+
- `SQS_V1` export is deprecated and aliases `SQS_V2` (identical
|
|
221
|
+
constants); it will be removed in a future release.
|
|
222
|
+
|
|
223
|
+
## 0.3.0 — 2026-08-27
|
|
224
|
+
|
|
225
|
+
### Fixed
|
|
226
|
+
- **Nine documented sqs-v1 rules never ran.** `buildConfig` spread
|
|
227
|
+
eslint-plugin-playwright's `flat/recommended` config only when it was an
|
|
228
|
+
array, but the plugin exports a single config object — so the whole
|
|
229
|
+
recommended set was silently dropped, and every rule that relied on it
|
|
230
|
+
was inert. Concretely: `no-conditional-in-test`, `no-conditional-expect`,
|
|
231
|
+
`no-element-handle`, `no-eval`, `no-page-pause`, `no-useless-await`,
|
|
232
|
+
`valid-expect`, `no-standalone-expect`, and `max-nested-describe` were
|
|
233
|
+
all mapped to dimensions in `profiles.ts` (and METHODOLOGY.md's hygiene
|
|
234
|
+
dimension explicitly lists "handles, conditionals") yet could never
|
|
235
|
+
produce a finding. A spec whose tests were built entirely out of
|
|
236
|
+
`if (await locator.isVisible())` branches scored a clean 100.
|
|
237
|
+
- The fix enables the intended rule set **explicitly and frozen** in
|
|
238
|
+
`eslint-runner.ts` rather than re-spreading upstream `recommended`:
|
|
239
|
+
eslint-plugin-playwright is a `^` range dependency, and inheriting its
|
|
240
|
+
recommended set at install time would let identical code score
|
|
241
|
+
differently across installs. Severities mirror upstream flat/recommended
|
|
242
|
+
(2.x). `require-top-level-describe` remains deliberately disabled
|
|
243
|
+
(stylistic; not in upstream recommended; would penalize idiomatic
|
|
244
|
+
describe-less suites).
|
|
245
|
+
|
|
246
|
+
### Scoring impact
|
|
247
|
+
- The formula, weights, and constants are unchanged — this restores the
|
|
248
|
+
*documented* sqs-v1 rule inputs, so the score version stays `sqs-v1`.
|
|
249
|
+
Suites that use conditional test logic, element handles, `page.pause()`,
|
|
250
|
+
or malformed/standalone `expect` calls will score **lower** than under
|
|
251
|
+
0.2.0 (which could not see those findings at all). Clean suites are
|
|
252
|
+
unaffected — all `good-*` fixtures score identically.
|
|
253
|
+
|
|
254
|
+
## 0.2.0 — 2026-08-19
|
|
255
|
+
|
|
256
|
+
### Removed
|
|
257
|
+
- **The `guardian` profile.** It was QA Guardian's own internal codegen
|
|
258
|
+
house-rules layer — arbitrary product-specific conventions (e.g.
|
|
259
|
+
"timeouts must be exactly 2000 or 20000ms") never claimed as official
|
|
260
|
+
Playwright doctrine, and not part of this package's actual differentiated
|
|
261
|
+
value (the aggregate score, locator-ratio tracing, assertion-delegation
|
|
262
|
+
resolution — all in `standard`). Shipping one company's house rules
|
|
263
|
+
inside a general-purpose public tool undermined the "this is a neutral
|
|
264
|
+
community linter" positioning the rest of this project has been built
|
|
265
|
+
around. `ProfileName` is now `'standard'` only; `--profile guardian` on
|
|
266
|
+
the CLI now exits 2 with an explicit message pointing here instead of
|
|
267
|
+
silently doing something unexpected. Passing an unrecognized profile
|
|
268
|
+
string programmatically (e.g. a caller still pinned to the old contract)
|
|
269
|
+
falls back to `standard`'s weights rather than throwing — a defensive
|
|
270
|
+
guard, not an invitation to keep using it.
|
|
271
|
+
QA Guardian's own `playwright_runner` now runs its private house-rules
|
|
272
|
+
layer separately, composed with this package's public `standard`
|
|
273
|
+
profile — verified byte-for-byte identical output to the old built-in
|
|
274
|
+
`guardian` profile across every fixture and 15 real production specs
|
|
275
|
+
before cutover. `penaltyDimensionScore`/`applyPerRuleCap` are now
|
|
276
|
+
exported (they always existed internally) specifically so that
|
|
277
|
+
composition — or anyone else building a similar house-rules layer on
|
|
278
|
+
top of `standard` — doesn't have to reimplement the scoring math.
|
|
279
|
+
- `scripts/dogfood.sh` — it reached into a relative sibling-repo path that
|
|
280
|
+
only exists inside this project's private internal monorepo, and
|
|
281
|
+
defaulted to the now-removed `guardian` profile. Internal-only tooling
|
|
282
|
+
that never belonged in a public repo; `scripts/validate-corpus.sh`
|
|
283
|
+
(real public repos, no internal paths) is the tool for this now.
|
|
284
|
+
|
|
285
|
+
## 0.1.14 — 2026-08-19
|
|
286
|
+
|
|
287
|
+
### Fixed
|
|
288
|
+
- **CLI stdout truncation on large output.** `console.log(body)` immediately
|
|
289
|
+
followed by `process.exit()` raced Node's async pipe write against
|
|
290
|
+
process termination — for output over ~64KB (a real suite's 1,215-finding
|
|
291
|
+
JSON report, ~370KB), the write was silently cut at exactly the pipe
|
|
292
|
+
buffer boundary when stdout wasn't a TTY (piped to `jq`, a CI log
|
|
293
|
+
processor, anything). No error, just truncated, invalid output. Found
|
|
294
|
+
while building `scripts/validate-corpus.sh` — reproduced with `--out
|
|
295
|
+
file.json` (full 376,920 bytes) vs. piped (exactly 65,536 bytes) against
|
|
296
|
+
the same input. Fixed by using `process.exitCode` instead of
|
|
297
|
+
`process.exit()` everywhere in the CLI, which lets Node exit naturally
|
|
298
|
+
once pending I/O actually drains — the standard fix for this class of
|
|
299
|
+
bug. 3 new tests spawn the real CLI binary as a child process (the only
|
|
300
|
+
way to reproduce a stdout-pipe bug — the existing suite only ever called
|
|
301
|
+
`scorePaths()` in-process).
|
|
302
|
+
- **`.e2e-spec.`/`.e2e-test.` naming conventions weren't discovered either**
|
|
303
|
+
— the same gap as 0.1.13's `.e2e.` fix, one suffix short. Verified
|
|
304
|
+
against Immich: 13 real Playwright specs named `*.e2e-spec.ts`,
|
|
305
|
+
previously invisible, sitting alongside 31 *other* files using the
|
|
306
|
+
identical suffix for `vitest`/`supertest` backend tests — correctly
|
|
307
|
+
excluded by the existing `looksLikeNonPlaywrightTest` safety net once
|
|
308
|
+
discovery found them. `SPEC_GLOBS` now also matches
|
|
309
|
+
`*.e2e-spec.*`/`*.e2e-test.*`.
|
|
310
|
+
|
|
311
|
+
### Added
|
|
312
|
+
- **`scripts/validate-corpus.sh`** — re-runnable real-world validation:
|
|
313
|
+
shallow/sparse-clones 17 real public repos fresh and scores each with the
|
|
314
|
+
local build. See `VALIDATION.md` for the current results and the story
|
|
315
|
+
behind every fix that came from running it.
|
|
316
|
+
|
|
317
|
+
## 0.1.13 — 2026-08-19
|
|
318
|
+
|
|
319
|
+
### Fixed
|
|
320
|
+
- **Directory/glob discovery only matched `*.spec.*`/`*.test.*` — a repo
|
|
321
|
+
using `.e2e.` to disambiguate end-to-end specs from unit tests scored a
|
|
322
|
+
hard 0/F ("no files matched") no matter how healthy its suite actually
|
|
323
|
+
was.** Verified against a real, large, well-known repo (cal.com:
|
|
324
|
+
`apps/web/playwright/` — 53 real Playwright specs, all named `*.e2e.ts`,
|
|
325
|
+
zero `*.spec.ts`/`*.test.ts`). `SPEC_GLOBS` now also matches
|
|
326
|
+
`*.e2e.{ts,tsx,js,jsx}`. Re-scored: 0/F, no files matched → 85/B, PASS,
|
|
327
|
+
53 files / 278 tests found — and the findings are real (642
|
|
328
|
+
`no-raw-locators`, spot-checked against the actual source), not noise
|
|
329
|
+
introduced by the wider glob.
|
|
330
|
+
`.e2e.` is also Cypress's own scaffolding convention, so
|
|
331
|
+
`looksLikeNonPlaywrightTest` (the existing safety net for a broad glob
|
|
332
|
+
sweeping in a different framework's tests) gained an explicit Cypress
|
|
333
|
+
check — an import from `'cypress'`, or common `cy.*` command usage
|
|
334
|
+
(`cy.visit`, `cy.get`, `cy.contains`, ...) with no import at all, since
|
|
335
|
+
Cypress's globals are injected the same way React Testing Library's
|
|
336
|
+
`screen` often is. Matched on specific command names, not a bare `cy.`
|
|
337
|
+
prefix, so a Playwright spec with an unrelated local variable named `cy`
|
|
338
|
+
is never caught by mistake.
|
|
339
|
+
9 new tests, full real-world corpus re-verified with zero regressions.
|
|
340
|
+
|
|
341
|
+
## 0.1.12 — 2026-08-19
|
|
342
|
+
|
|
343
|
+
### Fixed
|
|
344
|
+
- **Assertion-helper delegation was invisible for class-based Page Object
|
|
345
|
+
Models — `findLocalAssertionHelperNames` only recognized free functions
|
|
346
|
+
and `const`-bound arrows, never a class method.** Since almost every
|
|
347
|
+
real-world POM is written as a class, this meant the cross-file
|
|
348
|
+
assertion-delegation tracing shipped in 0.1.11 didn't actually reach the
|
|
349
|
+
common case. Now also collects `MethodDefinition` (`kind: 'method'`)
|
|
350
|
+
candidates.
|
|
351
|
+
- **`.waitFor({ state })` (state defaults to `'visible'`) is now recognized
|
|
352
|
+
as an assertion equivalent to `expect(...)`.** It exists only to verify
|
|
353
|
+
a state, unlike an action method that merely throws on failure as a
|
|
354
|
+
side effect — functionally the same as `expect(locator).toBeVisible()`.
|
|
355
|
+
Matched on the exact property name `waitFor`, never confused with
|
|
356
|
+
`waitForEvent`/`waitForURL`/`waitForResponse`/`waitForLoadState`/
|
|
357
|
+
`waitForTimeout`, none of which are assertions.
|
|
358
|
+
- **Delegation through more than one call is now resolved within a single
|
|
359
|
+
file** — e.g. a `waitForNotificationAndClose()` method calling
|
|
360
|
+
`this.waitForNotification()`, which itself calls `.waitFor()`, two
|
|
361
|
+
calls removed from anything expect-expect could previously see. A
|
|
362
|
+
bounded fixed-point pass: any candidate calling (by name, any receiver —
|
|
363
|
+
same matching eslint-plugin-playwright's own `assertFunctionNames`
|
|
364
|
+
option already uses) something already recognized is added too, until
|
|
365
|
+
no new names are found.
|
|
366
|
+
Verified against the real suite this was found in (n8n): its
|
|
367
|
+
`NotificationsPage.waitForNotificationAndClose` is exactly this shape,
|
|
368
|
+
two calls deep, defined in the same file as `waitForNotification`. Files
|
|
369
|
+
with an `expect-expect` finding dropped from ~15 to 5 across n8n's 256
|
|
370
|
+
specs; the remaining ones are a different, disclosed pattern (some tests
|
|
371
|
+
signal failure by throwing directly rather than asserting a state —
|
|
372
|
+
still open, not attempted here). Re-verified against the full real-world
|
|
373
|
+
corpus (Playwright, Supabase, Storybook, freeCodeCamp, OpenMRS,
|
|
374
|
+
patient-chart, 15 real Guardian specs) with zero regressions.
|
|
375
|
+
|
|
376
|
+
## 0.1.11 — 2026-08-17
|
|
377
|
+
|
|
378
|
+
### Fixed
|
|
379
|
+
- **Page Object Model suites read as 0/100 on the locators dimension even
|
|
380
|
+
when the suite uses native locators throughout.** A spec file that routes
|
|
381
|
+
all interaction through an imported class (`canvas.addNode(...)` instead
|
|
382
|
+
of `page.getByRole(...)` directly) has zero locator calls of its own —
|
|
383
|
+
the real `getByRole`/`getByTestId`/`.locator()` calls live in a separate
|
|
384
|
+
page-object file the scorer never looked at. Verified against a real,
|
|
385
|
+
large suite (n8n's own e2e tests: 256 spec files, 1,046 tests, every one
|
|
386
|
+
of them at 0 direct locator calls) — it scored 79/C and *failed* the
|
|
387
|
+
default threshold for this reason alone; the suite's actual page objects
|
|
388
|
+
use native locators almost exclusively.
|
|
389
|
+
`src/import-graph.ts` now follows a spec file's *relative* imports
|
|
390
|
+
(`./`, `../` only — never a bare package specifier, npm dependency, or
|
|
391
|
+
path alias, which would need a real module resolver to resolve safely)
|
|
392
|
+
to locate local dependency files within the scanned directory, and folds
|
|
393
|
+
their locator counts into the suite-level ratio. Dependency files are
|
|
394
|
+
never linted and never count toward SLOC/tests/findings — only their
|
|
395
|
+
locator calls are attributed to the suite. Bounded deliberately (depth
|
|
396
|
+
and file-count capped, confined to the scanned directory) so a large or
|
|
397
|
+
circular import graph can't blow up cost. Re-scoring n8n's suite with
|
|
398
|
+
this fix: 79/C (FAIL) → 95/A (PASS).
|
|
399
|
+
As a byproduct of the same infrastructure, assertion delegated to an
|
|
400
|
+
*imported* helper function (not just a same-file one, which already
|
|
401
|
+
worked — see 0.1.7) is now also recognized by `expect-expect`.
|
|
402
|
+
- Fixed a related bug found while building the above: the import-tracing
|
|
403
|
+
boundary was initially set to the scored spec files' own common
|
|
404
|
+
ancestor, which is *narrower* than the actual project directory whenever
|
|
405
|
+
specs live in a dedicated `tests/` folder sibling to `pages/`/
|
|
406
|
+
`fixtures/` (the normal shape for this pattern) — every import climbing
|
|
407
|
+
back out of `tests/` was rejected as "outside the boundary." Fixed by
|
|
408
|
+
widening the boundary to the directory the caller actually pointed the
|
|
409
|
+
scanner at. Does not apply when every input is an explicit file path
|
|
410
|
+
with no directory to widen to — see the new regression test documenting
|
|
411
|
+
that limitation.
|
|
412
|
+
|
|
413
|
+
## 0.1.10 — 2026-08-15
|
|
414
|
+
|
|
415
|
+
### Added
|
|
416
|
+
- **Reusable GitHub Action** (`action.yml` at repo root, usable as
|
|
417
|
+
`qa-guardian/playwright-score@v1`). Runs the scorer, writes a job summary
|
|
418
|
+
(always — no extra permissions needed), optionally posts/updates a sticky
|
|
419
|
+
PR comment with the full findings, and gates or warns on the threshold
|
|
420
|
+
via `mode: gate|warn` (mirrors the existing `SPEC_SCORE_MODE` convention
|
|
421
|
+
from the QA Guardian integration). Exposes `score`/`grade`/`pass` as step
|
|
422
|
+
outputs. Not yet published to the Marketplace or exercised in a live
|
|
423
|
+
workflow — validated locally by running the same commands/JSON parsing
|
|
424
|
+
the action performs against the built CLI, not by an actual GitHub
|
|
425
|
+
Actions run.
|
|
426
|
+
- **CI auto-publish** (`.github/workflows/publish.yml`): on push to `main`
|
|
427
|
+
touching `package.json`, builds, tests, and publishes to npm only if the
|
|
428
|
+
local version isn't already on the registry. Authenticates via npm's
|
|
429
|
+
**Trusted Publisher (OIDC)** flow (`id-token: write`, no stored token) —
|
|
430
|
+
chosen over a classic automation token specifically because npm is
|
|
431
|
+
restricting 2FA-bypass tokens for direct publishing starting January 2027;
|
|
432
|
+
OIDC isn't affected and has nothing to rotate or expire. Requires the
|
|
433
|
+
trusted publisher configured once on npmjs.com (package settings →
|
|
434
|
+
Trusted Publisher → GitHub Actions → repo `qa-guardian/playwright-score`,
|
|
435
|
+
workflow `publish.yml`) and npm CLI ≥11.5.1 / Node ≥22.14.0 in the runner
|
|
436
|
+
(both satisfied by `actions/setup-node@v4` with `node-version: '22'`).
|
|
437
|
+
|
|
438
|
+
### Chore
|
|
439
|
+
- No functional change beyond the above. Version bump to verify the new
|
|
440
|
+
CI auto-publish pipeline (`.github/workflows/publish.yml`,
|
|
441
|
+
Trusted Publisher/OIDC) works end-to-end against the real npm registry.
|
|
442
|
+
|
|
443
|
+
## 0.1.9 — 2026-08-15
|
|
444
|
+
|
|
445
|
+
### Fixed
|
|
446
|
+
- **`playwright/prefer-locator` and `playwright/prefer-native-locators` were
|
|
447
|
+
mapped in `profiles.ts` but never actually enabled — completely inert.**
|
|
448
|
+
`prefer-locator` specifically catches Playwright's older direct-action API
|
|
449
|
+
(`page.click('#foo')` instead of a locator or native locator), arguably
|
|
450
|
+
the single most common raw-selector anti-pattern in naive/AI-generated
|
|
451
|
+
code. Also extended `countLocators`'s raw/native ratio to recognize this
|
|
452
|
+
pattern (`click`/`dblclick`/`hover`/`check`/`uncheck`/`tap`/`focus` called
|
|
453
|
+
with a string selector as the first argument — methods a real `Locator`
|
|
454
|
+
never accepts a string for, so this is unambiguous regardless of receiver
|
|
455
|
+
name; `fill`/`type`/`press`/`selectOption` are deliberately excluded since
|
|
456
|
+
their first argument is legitimately a string on a `Locator` too).
|
|
457
|
+
Verified manually: a realistic "naive AI-generated" test sample using
|
|
458
|
+
nothing but `page.click(selector)`/`page.type(selector, ...)` went from a
|
|
459
|
+
misleading 85/B (PASS) to a correct 65/D (FAIL). Re-validated against the
|
|
460
|
+
full real-world corpus (~130 files across freeCodeCamp, OpenMRS, patient
|
|
461
|
+
chart, and Storybook e2e suites, plus 15 real Guardian specs) — no new
|
|
462
|
+
false positives.
|
|
463
|
+
|
|
464
|
+
## 0.1.8 — 2026-08-15
|
|
465
|
+
|
|
466
|
+
### Fixed
|
|
467
|
+
- **Directory/glob scanning did not exclude `node_modules` (or `dist`,
|
|
468
|
+
`build`, `.next`, `coverage`, and similar vendored/generated
|
|
469
|
+
directories).** Verified as a real bug: running the CLI against a
|
|
470
|
+
project root that has any dependency shipping its own `*.spec.ts`/
|
|
471
|
+
`*.test.ts` files silently swept those in alongside real specs. Every
|
|
472
|
+
comparable tool in this space (ESLint, Prettier, Jest) default-excludes
|
|
473
|
+
these directories for the same reason. Re-verified against a real
|
|
474
|
+
823MB `node_modules` containing 385 spec-like files: now correctly
|
|
475
|
+
excluded, and fast (glob itself skips traversing into the ignored
|
|
476
|
+
directories, not just filtering results afterward). An explicit file
|
|
477
|
+
path always bypasses this, same as the non-Playwright-test filter.
|
|
478
|
+
|
|
479
|
+
## 0.1.7 — 2026-08-15
|
|
480
|
+
|
|
481
|
+
Deeper real-world validation: ~10 more single-file pulls (API-only specs,
|
|
482
|
+
mobile device emulation, deeply-chained fixtures, Japanese-language
|
|
483
|
+
content) plus a full clone of freeCodeCamp's e2e suite (89 spec files)
|
|
484
|
+
scored as one directory. No crashes; UTF-8/multi-byte content (Japanese,
|
|
485
|
+
Chinese, emoji seen across this and the previous round) parses and scores
|
|
486
|
+
correctly throughout.
|
|
487
|
+
|
|
488
|
+
### Fixed
|
|
489
|
+
- **The `assertFunctionPatterns` name-guessing from 0.1.6 was still
|
|
490
|
+
fundamentally unbounded.** A fourth real production file used yet
|
|
491
|
+
another naming convention (`alertToBeVisible`, imported from another
|
|
492
|
+
module) for the exact same assertion-delegation shape, and a fifth
|
|
493
|
+
(`checkFlashMessageVisibility`) didn't match any reasonable prefix/suffix
|
|
494
|
+
pattern at all — real codebases name these helpers however they want.
|
|
495
|
+
Replaced pure name-guessing with `findLocalAssertionHelperNames`: a
|
|
496
|
+
same-file call-graph pass that discovers any function (declared or
|
|
497
|
+
const-bound to an arrow/function expression) whose own body actually
|
|
498
|
+
contains an `expect(...)`-shaped call, regardless of what it's named,
|
|
499
|
+
and feeds the result into `playwright/expect-expect`'s own
|
|
500
|
+
`assertFunctionNames` option per run. This is fully name-agnostic for
|
|
501
|
+
the common (same-file helper) case; the name-pattern list from 0.1.6
|
|
502
|
+
remains only as a fallback for helpers imported from another file, which
|
|
503
|
+
can't be resolved without following module specifiers — a narrower,
|
|
504
|
+
clearly-scoped residual limitation.
|
|
505
|
+
|
|
506
|
+
## 0.1.6 — 2026-08-15
|
|
507
|
+
|
|
508
|
+
Further real-world validation: ~45 more spec files across single-file
|
|
509
|
+
pulls (visual regression, accessibility, file upload, iframes, auth
|
|
510
|
+
setup, Storybook) and three fully-cloned real e2e suites (openmrs-esm-core,
|
|
511
|
+
openmrs-esm-patient-chart, storybook) scored as whole directories. No
|
|
512
|
+
crashes across ~100+ real files total (this round plus 0.1.5's); all
|
|
513
|
+
formatters (text/json/markdown/sarif) and `--out` file writing checked at
|
|
514
|
+
that scale too.
|
|
515
|
+
|
|
516
|
+
### Fixed
|
|
517
|
+
- **`playwright/expect-expect` can only see `expect(...)` calls written
|
|
518
|
+
directly in a test body — it has no way to trace an assertion made
|
|
519
|
+
inside a helper function the test calls.** Delegating assertions to a
|
|
520
|
+
shared helper (e.g. an `audit(page, path)` a11y check reused across many
|
|
521
|
+
near-identical tests, or an `expectXToBeVisible(page)` helper) is a very
|
|
522
|
+
common way to dedupe similar specs, and was flagged as "no assertions"
|
|
523
|
+
at error severity — verified against two real production files (one
|
|
524
|
+
losing 2 tests' worth of real signal to this). Configured
|
|
525
|
+
`assertFunctionPatterns` (eslint-plugin-playwright's own documented
|
|
526
|
+
escape hatch for this) with a conservative, camelCase/exact-anchored
|
|
527
|
+
pattern set (`assert*`, `verify*`, `validate*`, `audit*`, `expect*`,
|
|
528
|
+
`checkA11y`, `checkAccessibility`) — anchored so e.g. `checkoutFlow`
|
|
529
|
+
doesn't collide with a bare `check*` prefix.
|
|
530
|
+
|
|
531
|
+
## 0.1.5 — 2026-08-15
|
|
532
|
+
|
|
533
|
+
Validated against ~16 real-world Playwright spec files pulled from public
|
|
534
|
+
GitHub repos (react-joyride, openmrs, rancher-desktop, stylelint-demo,
|
|
535
|
+
Microsoft's own Playwright test suite, and others) plus the sample suites
|
|
536
|
+
already dogfooded internally, in addition to unit/integration coverage.
|
|
537
|
+
Found and fixed three more real false positives this surfaced:
|
|
538
|
+
|
|
539
|
+
### Fixed
|
|
540
|
+
- **`expect.poll()`/`expect.soft()` assertions weren't recognized as
|
|
541
|
+
assertions at all.** A source-text-regex check (`metrics/no-empty-test`)
|
|
542
|
+
only matched a bare `expect(` call, so any file whose only assertion
|
|
543
|
+
used the poll/soft chained form was flagged at **error** severity as
|
|
544
|
+
having no assertions — on a real file that unambiguously polls until a
|
|
545
|
+
condition is true. The AST-based community `playwright/expect-expect`
|
|
546
|
+
rule (already active) handles this correctly on its own, so the
|
|
547
|
+
redundant, buggy regex check has been removed rather than patched
|
|
548
|
+
further.
|
|
549
|
+
- **Playwright's own documented conditional `test.skip(condition, reason)`
|
|
550
|
+
was flagged identically to an always-skipped test declaration.**
|
|
551
|
+
`eslint-plugin-playwright`'s `no-skipped-test` doesn't distinguish
|
|
552
|
+
`test.skip('name', fn)` (a test that never runs — worth flagging) from
|
|
553
|
+
`test.skip(browserName === 'webkit', 'reason')` (Playwright's documented
|
|
554
|
+
API for conditionally skipping at runtime — playwright.dev/docs/test-annotations#conditionally-skip-a-test).
|
|
555
|
+
Verified against a real cross-browser test file where this generated
|
|
556
|
+
five warnings on entirely idiomatic code. Replaced with
|
|
557
|
+
`pwscore/no-skipped-test-declaration`, which tells the two apart by
|
|
558
|
+
whether the trailing argument is a function (a declaration always
|
|
559
|
+
supplies one; a conditional skip's second argument, if any, is the
|
|
560
|
+
reason string) — same severity for the real anti-pattern, none for the
|
|
561
|
+
documented one.
|
|
562
|
+
- **`// eslint-disable-next-line @typescript-eslint/no-unused-vars` (and
|
|
563
|
+
any other disable comment referencing a rule we don't bundle) produced a
|
|
564
|
+
confusing non-finding.** We only embed `@typescript-eslint`'s *parser*,
|
|
565
|
+
not its rule set, so ESLint reports "Definition for rule '...' was not
|
|
566
|
+
found" for any disable directive targeting one of its rules — a
|
|
567
|
+
diagnostic about our own rule coverage, not the spec's quality. This
|
|
568
|
+
pattern is extremely common in real TypeScript code (any repo that also
|
|
569
|
+
lints with `@typescript-eslint`, react-hooks, import, etc.). These
|
|
570
|
+
messages are now dropped entirely instead of appearing as a report-only
|
|
571
|
+
finding.
|
|
572
|
+
|
|
573
|
+
## 0.1.4 — 2026-08-15
|
|
574
|
+
|
|
575
|
+
Comprehensive correctness/hardening pass ahead of public launch.
|
|
576
|
+
|
|
577
|
+
### Fixed
|
|
578
|
+
- **A spec file with a syntax error scored a perfect 100/A.** ESLint
|
|
579
|
+
reports parse errors as a message with `fatal:true` and `ruleId:null`;
|
|
580
|
+
those were silently dropped by the same filter that skips harmless
|
|
581
|
+
"file ignored" notices. A file that isn't even valid JS/TS is now a hard
|
|
582
|
+
fail (score 0 / grade F) with an explicit `playwright-score/parse-error`
|
|
583
|
+
finding, instead of the worst possible outcome for a tool whose job is
|
|
584
|
+
catching bad — including AI-generated — code.
|
|
585
|
+
- **Directory/glob scanning could sweep in unrelated Jest/Vitest/RTL unit
|
|
586
|
+
tests** sitting next to real Playwright specs (a very common repo
|
|
587
|
+
layout, e.g. `src/Button.test.tsx` next to `e2e/login.spec.ts`, both
|
|
588
|
+
matching a broad `*.test.ts`-style glob), silently polluting the
|
|
589
|
+
aggregate score. Files matched via directory/glob expansion are now
|
|
590
|
+
skipped when they show positive evidence of a non-Playwright framework
|
|
591
|
+
(an import from vitest/jest/mocha/jasmine/@testing-library, or RTL's
|
|
592
|
+
`screen.*`/`toBeInTheDocument()`); reported explicitly via the new
|
|
593
|
+
`skippedFiles` result field, not silently. Paths passed explicitly are
|
|
594
|
+
always scored regardless. An earlier version of this check tried to
|
|
595
|
+
*positively* require Playwright evidence instead and was reverted after
|
|
596
|
+
it silently skipped 18 of 19 genuine specs in a real production suite —
|
|
597
|
+
custom Playwright fixtures (`test/expect` re-exported from a project
|
|
598
|
+
fixtures module) can destructure arbitrarily-named fixtures, not just
|
|
599
|
+
`page`/`context`/`browser`, so there's no reliable positive signal.
|
|
600
|
+
- **The new AST-based locator counter (added in the previous release)
|
|
601
|
+
silently returned zero for any `.tsx`/`.jsx` file containing real JSX**
|
|
602
|
+
(e.g. Playwright component tests), because `@typescript-eslint/parser`
|
|
603
|
+
doesn't enable JSX parsing by default and the failure was swallowed by
|
|
604
|
+
the same try/catch that handles genuinely unparseable source. JSX is now
|
|
605
|
+
enabled based on the file extension.
|
|
606
|
+
- `test.fix(...)` isn't a real Playwright API — the actual modifier is
|
|
607
|
+
`test.fixme(...)`. Several guardian rules (`one-describe-one-test`,
|
|
608
|
+
`require-test-step`) and metric heuristics (`countTests`,
|
|
609
|
+
`findEmptyExpectTests`) checked for the nonexistent `.fix` spelling and
|
|
610
|
+
so never recognized real `test.fixme(...)` declarations.
|
|
611
|
+
- `guardian/require-expect` only recognized bare `test(...)` calls, so
|
|
612
|
+
`test.only/skip/fixme/slow(...)` declarations with zero assertions went
|
|
613
|
+
unflagged by this rule specifically (the community `expect-expect` rule
|
|
614
|
+
still caught them as a redundant safety net, so real-world impact was
|
|
615
|
+
partial).
|
|
616
|
+
- `guardian/one-describe-one-test` didn't recognize
|
|
617
|
+
`test.describe.serial/parallel/only/fixme(...)` as a describe form at
|
|
618
|
+
all (only bare `test.describe(...)` matched), and flagged
|
|
619
|
+
`test.describe.serial(...)`'s multiple ordered `test()` calls as
|
|
620
|
+
"sprawl" — but `describe.serial` is Playwright's own documented
|
|
621
|
+
mechanism for exactly that one-journey-multiple-ordered-tests pattern.
|
|
622
|
+
- `guardian/no-hardcoded-secrets` matched "auth" as a bare substring, so
|
|
623
|
+
`authorName`, `isAuthenticated`, and similar common identifiers were
|
|
624
|
+
flagged as possible secrets. Matching is now whole-word (after
|
|
625
|
+
camelCase/snake_case splitting), so `authorName` no longer matches while
|
|
626
|
+
`authToken`/`apiKey`/`Authorization` still do.
|
|
627
|
+
- `guardian/no-generic-long-timeout` flagged *any* object property named
|
|
628
|
+
`timeout` anywhere in the file — including unrelated business/fixture
|
|
629
|
+
data like `{ timeout: 1800 }` describing a session-length field — not
|
|
630
|
+
just `timeout` options passed to a Playwright call. Now scoped to
|
|
631
|
+
`timeout` properties passed inline as a call argument.
|
|
632
|
+
- `prefer-web-first-assertions` was downgraded to `warn` with no stated
|
|
633
|
+
reason, softer than eslint-plugin-playwright's own recommended `error`
|
|
634
|
+
severity for a rule it treats as important. Aligned to match.
|
|
635
|
+
|
|
636
|
+
### Added
|
|
637
|
+
- `ScoreResult.skippedFiles`, and a summary of skipped files in the text
|
|
638
|
+
and markdown formatters.
|
|
639
|
+
|
|
640
|
+
## 0.1.3 — 2026-08-14
|
|
641
|
+
|
|
642
|
+
### Fixed
|
|
643
|
+
- The `locators` dimension counted native/raw locator calls with a regex
|
|
644
|
+
anchored to literal `page`/`frame` receivers, undercounting raw locators
|
|
645
|
+
on any other receiver (e.g. `this.container.locator(...)` in a Page
|
|
646
|
+
Object Model, or a second `.locator()` chained off a prior locator call).
|
|
647
|
+
Counting now walks the same AST ESLint already parses, so it can't
|
|
648
|
+
silently disagree with `no-raw-locators` findings.
|
|
649
|
+
- SARIF output pointed `informationUri` at a URL missing a hyphen
|
|
650
|
+
(`github.com/qaguardian/...` instead of `qa-guardian`), and
|
|
651
|
+
`artifactLocation.uri` used absolute filesystem paths, which GitHub code
|
|
652
|
+
scanning cannot map back to files in a PR diff. `Finding.file` (and so
|
|
653
|
+
every formatter, not just SARIF) is now relative to the files' common
|
|
654
|
+
ancestor directory instead of an absolute path.
|
|
655
|
+
- Removed `guardian/prefer-uuid` and `guardian/fixtures-import` from the
|
|
656
|
+
rule→dimension map: they were referenced but never implemented or
|
|
657
|
+
enabled, so they could never fire.
|
|
658
|
+
|
|
659
|
+
### Added
|
|
660
|
+
- Fixture and integration test coverage for the 5 guardian rules that
|
|
661
|
+
previously had none: `no-hardcoded-secrets`, `no-generic-long-timeout`,
|
|
662
|
+
`one-describe-one-test`, `require-test-step`, `no-wait-for-load-state`.
|
|
663
|
+
|
|
664
|
+
## 0.1.2 — 2026-08-14
|
|
665
|
+
|
|
666
|
+
### Fixed
|
|
667
|
+
- ESLint 9 flat config resolves a `basePath` from the `cwd` passed to it and
|
|
668
|
+
silently drops (no error, no `ruleId`) any file outside that path.
|
|
669
|
+
`scorePaths`/`runEslint` now derive the basePath from the common ancestor
|
|
670
|
+
of the files actually being linted instead of trusting the caller's `cwd`,
|
|
671
|
+
which is not guaranteed to contain them. Previously, any caller running
|
|
672
|
+
from an unrelated working directory got zero ESLint-based findings on
|
|
673
|
+
every file regardless of content.
|
|
674
|
+
|
|
675
|
+
## 0.1.1 — 2026-08-03
|
|
676
|
+
|
|
677
|
+
### Fixed
|
|
678
|
+
- Empty file matches now hard-fail with score 0 / grade F instead of
|
|
679
|
+
reporting a misleadingly healthy ~99 pass.
|
|
680
|
+
|
|
681
|
+
### Docs
|
|
682
|
+
- Scoped-package CLI invocation docs (`npx -p @qaguardian/playwright-score`).
|
|
683
|
+
- `--version` flag.
|
|
684
|
+
|
|
685
|
+
## 0.1.0 — 2026-07-30
|
|
686
|
+
|
|
687
|
+
Initial release. `sqs-v1` scoring: `standard` (community `eslint-plugin-playwright`
|
|
688
|
+
+ suite metrics) and `guardian` (house conventions) profiles. CLI with text,
|
|
689
|
+
json, markdown, and sarif output. Library export via `scorePaths`.
|