@qaguardian/playwright-score 0.5.0 → 1.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/METHODOLOGY.md CHANGED
@@ -65,7 +65,7 @@ The **locators** dimension is independent of demerits — it is the usage
65
65
  ratio it always was:
66
66
 
67
67
  ```
68
- native = getByRole|getByLabel|getByTestId|getByText|getByPlaceholder calls
68
+ native = getByRole|getByLabel|getByTestId|getByText|getByPlaceholder|getByAltText|getByTitle calls
69
69
  raw = .locator(...) calls + legacy string-selector actions (page.click('#x'), …)
70
70
 
71
71
  locatorsScore = total === 0 ? 100 : round(100 × native / total)
package/README.md CHANGED
@@ -25,7 +25,7 @@ heavily-engineered platforms and smaller, less mature projects. Not curated
25
25
  to look good: 5 of 17 fail the default threshold, for real, verifiable
26
26
  reasons (see [VALIDATION.md](./VALIDATION.md)).
27
27
 
28
- **17 suites · 1,228 files · 5,664 tests · 12/17 passed (80% threshold)**
28
+ **17 suites · 1,230 files · 5,654 tests · 12/17 passed (80% threshold)**
29
29
 
30
30
  | Repo (source scanned) | Score | Grade | Result |
31
31
  |---|---:|:-:|:-:|
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { ScoreOptions, ScoreResult } from './types.js';
2
2
  export type { ScoreResult, ScoreOptions, Finding, ProfileName } from './types.js';
3
3
  export { computeScore, MODEL, locatorsScore, gradeFromScore, ratioDimensionScore, } from './score-engine.js';
4
4
  export { countSloc } from './sloc.js';
5
- export { countLocators, analyzeSource } from './metrics.js';
5
+ export { countLocators, analyzeSource, attributeFindingScopes, getTestSpans, } from './metrics.js';
6
6
  export { formatJson } from './formatters/json.js';
7
7
  export { formatText } from './formatters/text.js';
8
8
  export { formatMarkdown } from './formatters/markdown.js';
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@ import { globSync } from 'glob';
4
4
  import { runEslint } from './eslint-runner.js';
5
5
  import { commonAncestorDir } from './fs-util.js';
6
6
  import { collectLocallyImportedFiles } from './import-graph.js';
7
- import { analyzeSource, countLocators, findLocalAssertionHelperNames, getTestSpans, looksLikeNonPlaywrightTest, } from './metrics.js';
7
+ import { analyzeSource, attributeFindingScopes, countLocators, findLocalAssertionHelperNames, getTestSpans, looksLikeNonPlaywrightTest, } from './metrics.js';
8
8
  import { DEFAULT_THRESHOLDS } from './profiles.js';
9
9
  import { computeScore } from './score-engine.js';
10
10
  export { computeScore, MODEL, locatorsScore, gradeFromScore, ratioDimensionScore, } from './score-engine.js';
11
11
  export { countSloc } from './sloc.js';
12
- export { countLocators, analyzeSource } from './metrics.js';
12
+ export { countLocators, analyzeSource, attributeFindingScopes, getTestSpans, } from './metrics.js';
13
13
  export { formatJson } from './formatters/json.js';
14
14
  export { formatText } from './formatters/text.js';
15
15
  export { formatMarkdown } from './formatters/markdown.js';
@@ -260,29 +260,9 @@ export async function scorePaths(options) {
260
260
  raw += counts.raw;
261
261
  }
262
262
  const findings = [...eslintFindings, ...metricFindings];
263
- // Attribute each finding to the test it sits in (model v3): a finding
264
- // inside a test span belongs to that test; inside a hook span it affects
265
- // every test in the file; anything else — imports, describe bodies,
266
- // file-level metrics like oversized-file — is module scope, counted once
267
- // per file.
268
- for (const f of findings) {
269
- const spans = fileSpans.get(f.file);
270
- if (!spans || f.line === undefined) {
271
- f.scope = 'module';
272
- continue;
273
- }
274
- const idx = spans.tests.findIndex((t) => f.line !== undefined && f.line >= t.startLine && f.line <= t.endLine);
275
- if (idx >= 0) {
276
- f.scope = 'test';
277
- f.testKey = `${f.file}#${idx}`;
278
- }
279
- else if (spans.hooks.some((h) => f.line !== undefined && f.line >= h.startLine && f.line <= h.endLine)) {
280
- f.scope = 'hook';
281
- }
282
- else {
283
- f.scope = 'module';
284
- }
285
- }
263
+ // Attribute each finding to the test it sits in (model v3) — see
264
+ // attributeFindingScopes in metrics.ts.
265
+ attributeFindingScopes(findings, fileSpans);
286
266
  const parseErrors = findings.filter((f) => f.rule === 'playwright-score/parse-error');
287
267
  if (parseErrors.length > 0) {
288
268
  // A file that isn't valid JS/TS can't be executed, let alone scored on
package/dist/metrics.d.ts CHANGED
@@ -63,6 +63,18 @@ export interface TestSpans {
63
63
  * malformed anyway) are dropped so each line maps to at most one test.
64
64
  */
65
65
  export declare function getTestSpans(source: string, file?: string): TestSpans;
66
+ /**
67
+ * Model-v3 finding attribution (see score-engine.ts): a finding inside a
68
+ * test span belongs to that test (`scope: 'test'` + a stable
69
+ * `file#index` testKey); inside a hook span it affects every test in the
70
+ * file (`scope: 'hook'`); anything else — imports, describe bodies,
71
+ * file-level metrics — is `scope: 'module'`, counted once per file.
72
+ * Exported so layers composing extra rules on top (e.g. a private
73
+ * house-rules dimension) attribute their own findings with the same
74
+ * logic the package itself uses, rather than reimplementing it.
75
+ * Mutates the findings in place and returns them.
76
+ */
77
+ export declare function attributeFindingScopes(findings: Finding[], spansByFile: Map<string, TestSpans>): Finding[];
66
78
  /** Count test( / test.only( / test.skip( / test.fixme( style declarations (approx). */
67
79
  export declare function countTests(source: string): number;
68
80
  export declare function findOversizedFile(source: string, file: string, maxSloc?: number): Finding[];
package/dist/metrics.js CHANGED
@@ -35,12 +35,18 @@ const CYPRESS_API_RE = /\bcy\.(?:visit|get|contains|click|type|should|intercept|
35
35
  export function looksLikeNonPlaywrightTest(source) {
36
36
  return NON_PLAYWRIGHT_IMPORT_RE.test(source) || RTL_API_RE.test(source) || CYPRESS_API_RE.test(source);
37
37
  }
38
+ // The full getBy* family Playwright ships. getByAltText/getByTitle are
39
+ // just as native as the rest — leaving them out meant an image-heavy suite
40
+ // (alt/title locators plus one .locator() fallback) scored 0 on the
41
+ // locators dimension for perfectly idiomatic code.
38
42
  const NATIVE_METHODS = new Set([
39
43
  'getByRole',
40
44
  'getByLabel',
41
45
  'getByTestId',
42
46
  'getByText',
43
47
  'getByPlaceholder',
48
+ 'getByAltText',
49
+ 'getByTitle',
44
50
  ]);
45
51
  /**
46
52
  * Generic ESTree walk (no visitor-keys dependency). `parent` is excluded so
@@ -382,6 +388,39 @@ export function getTestSpans(source, file) {
382
388
  }
383
389
  return { tests: outer, hooks };
384
390
  }
391
+ /**
392
+ * Model-v3 finding attribution (see score-engine.ts): a finding inside a
393
+ * test span belongs to that test (`scope: 'test'` + a stable
394
+ * `file#index` testKey); inside a hook span it affects every test in the
395
+ * file (`scope: 'hook'`); anything else — imports, describe bodies,
396
+ * file-level metrics — is `scope: 'module'`, counted once per file.
397
+ * Exported so layers composing extra rules on top (e.g. a private
398
+ * house-rules dimension) attribute their own findings with the same
399
+ * logic the package itself uses, rather than reimplementing it.
400
+ * Mutates the findings in place and returns them.
401
+ */
402
+ export function attributeFindingScopes(findings, spansByFile) {
403
+ for (const f of findings) {
404
+ const spans = spansByFile.get(f.file);
405
+ if (!spans || f.line === undefined) {
406
+ f.scope = 'module';
407
+ continue;
408
+ }
409
+ const line = f.line;
410
+ const idx = spans.tests.findIndex((t) => line >= t.startLine && line <= t.endLine);
411
+ if (idx >= 0) {
412
+ f.scope = 'test';
413
+ f.testKey = `${f.file}#${idx}`;
414
+ }
415
+ else if (spans.hooks.some((h) => line >= h.startLine && line <= h.endLine)) {
416
+ f.scope = 'hook';
417
+ }
418
+ else {
419
+ f.scope = 'module';
420
+ }
421
+ }
422
+ return findings;
423
+ }
385
424
  const TEST_NON_DECL_RE = /\btest\.(describe|step|beforeEach|afterEach|beforeAll|afterAll|use|setTimeout|info|expect)\b/;
386
425
  /** Count test( / test.only( / test.skip( / test.fixme( style declarations (approx). */
387
426
  export function countTests(source) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qaguardian/playwright-score",
3
- "version": "0.5.0",
4
- "description": "Deterministic, AI-free Playwright spec quality score (sqs-v2).",
3
+ "version": "1.0.0",
4
+ "description": "Deterministic, AI-free Playwright spec quality score — the weighted share of your tests that are clean.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",