@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 +1 -1
- package/README.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -25
- package/dist/metrics.d.ts +12 -0
- package/dist/metrics.js +39 -0
- package/package.json +2 -2
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,
|
|
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)
|
|
264
|
-
//
|
|
265
|
-
|
|
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.
|
|
4
|
-
"description": "Deterministic, AI-free Playwright spec quality score
|
|
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",
|