@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 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`.