@qaguardian/playwright-score 0.1.12 → 0.1.14
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/README.md +1 -1
- package/dist/cli.js +24 -9
- package/dist/formatters/markdown.js +1 -1
- package/dist/formatters/text.js +1 -1
- package/dist/index.js +16 -3
- package/dist/metrics.d.ts +5 -5
- package/dist/metrics.js +16 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Lint + score Playwright tests with:
|
|
|
16
16
|
|
|
17
17
|
The score never calls an LLM. AI may *generate* or *repair* code using findings; **rules grade code**.
|
|
18
18
|
|
|
19
|
-
See [METHODOLOGY.md](./METHODOLOGY.md) for the frozen formula, or the full product write-up on the [landing page](https://qaguardian.com/open-source/playwright-score).
|
|
19
|
+
See [METHODOLOGY.md](./METHODOLOGY.md) for the frozen formula, [VALIDATION.md](./VALIDATION.md) for real-world results against public repos (cal.com, Grafana, Mattermost, ...) and a re-runnable script to reproduce them, or the full product write-up on the [landing page](https://qaguardian.com/open-source/playwright-score).
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
package/dist/cli.js
CHANGED
|
@@ -34,6 +34,14 @@ Website: https://qaguardian.com/open-source/playwright-score
|
|
|
34
34
|
Methodology: METHODOLOGY.md (sqs-v1)
|
|
35
35
|
`);
|
|
36
36
|
}
|
|
37
|
+
// Deliberately never process.exit() right after a console.log/console.error
|
|
38
|
+
// — for a large enough write (verified against a real suite: a JSON report
|
|
39
|
+
// with 1000+ findings, ~370KB) stdout to a pipe (not a TTY) is asynchronous,
|
|
40
|
+
// and process.exit() can terminate the process before the OS has drained
|
|
41
|
+
// the write, silently truncating the output exactly at the pipe buffer
|
|
42
|
+
// size (64KB on macOS/Linux). Setting process.exitCode and returning lets
|
|
43
|
+
// Node exit naturally once pending I/O actually completes, which is the
|
|
44
|
+
// standard fix for this class of bug.
|
|
37
45
|
async function main() {
|
|
38
46
|
const args = process.argv.slice(2);
|
|
39
47
|
if (args.includes('--version') || args.includes('-v')) {
|
|
@@ -46,11 +54,13 @@ async function main() {
|
|
|
46
54
|
catch {
|
|
47
55
|
console.log('0.1.8');
|
|
48
56
|
}
|
|
49
|
-
process.
|
|
57
|
+
process.exitCode = 0;
|
|
58
|
+
return;
|
|
50
59
|
}
|
|
51
60
|
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
52
61
|
printHelp();
|
|
53
|
-
process.
|
|
62
|
+
process.exitCode = args.length === 0 ? 2 : 0;
|
|
63
|
+
return;
|
|
54
64
|
}
|
|
55
65
|
let profile = 'standard';
|
|
56
66
|
let threshold;
|
|
@@ -63,21 +73,24 @@ async function main() {
|
|
|
63
73
|
profile = args[++i];
|
|
64
74
|
if (profile !== 'standard' && profile !== 'guardian') {
|
|
65
75
|
console.error(`Invalid profile: ${profile}`);
|
|
66
|
-
process.
|
|
76
|
+
process.exitCode = 2;
|
|
77
|
+
return;
|
|
67
78
|
}
|
|
68
79
|
}
|
|
69
80
|
else if (a === '--threshold') {
|
|
70
81
|
threshold = Number(args[++i]);
|
|
71
82
|
if (Number.isNaN(threshold) || threshold < 0 || threshold > 100) {
|
|
72
83
|
console.error('threshold must be 0-100');
|
|
73
|
-
process.
|
|
84
|
+
process.exitCode = 2;
|
|
85
|
+
return;
|
|
74
86
|
}
|
|
75
87
|
}
|
|
76
88
|
else if (a === '--format') {
|
|
77
89
|
const f = args[++i];
|
|
78
90
|
if (f !== 'text' && f !== 'json' && f !== 'markdown' && f !== 'sarif') {
|
|
79
91
|
console.error(`Invalid format: ${f}`);
|
|
80
|
-
process.
|
|
92
|
+
process.exitCode = 2;
|
|
93
|
+
return;
|
|
81
94
|
}
|
|
82
95
|
format = f;
|
|
83
96
|
}
|
|
@@ -89,7 +102,8 @@ async function main() {
|
|
|
89
102
|
}
|
|
90
103
|
else if (a.startsWith('-')) {
|
|
91
104
|
console.error(`Unknown option: ${a}`);
|
|
92
|
-
process.
|
|
105
|
+
process.exitCode = 2;
|
|
106
|
+
return;
|
|
93
107
|
}
|
|
94
108
|
else {
|
|
95
109
|
paths.push(a);
|
|
@@ -97,7 +111,8 @@ async function main() {
|
|
|
97
111
|
}
|
|
98
112
|
if (paths.length === 0) {
|
|
99
113
|
console.error('At least one path is required');
|
|
100
|
-
process.
|
|
114
|
+
process.exitCode = 2;
|
|
115
|
+
return;
|
|
101
116
|
}
|
|
102
117
|
try {
|
|
103
118
|
const result = await scorePaths({ paths, profile, threshold });
|
|
@@ -122,11 +137,11 @@ async function main() {
|
|
|
122
137
|
else {
|
|
123
138
|
console.log(body);
|
|
124
139
|
}
|
|
125
|
-
process.
|
|
140
|
+
process.exitCode = result.pass ? 0 : 1;
|
|
126
141
|
}
|
|
127
142
|
catch (err) {
|
|
128
143
|
console.error(err instanceof Error ? err.message : err);
|
|
129
|
-
process.
|
|
144
|
+
process.exitCode = 2;
|
|
130
145
|
}
|
|
131
146
|
}
|
|
132
147
|
main();
|
|
@@ -24,7 +24,7 @@ export function formatMarkdown(result) {
|
|
|
24
24
|
}
|
|
25
25
|
if (result.skippedFiles?.length) {
|
|
26
26
|
lines.push('');
|
|
27
|
-
lines.push(`<details><summary>Skipped — looks like a non-Playwright test (Jest/Vitest/RTL) (${result.skippedFiles.length})</summary>`);
|
|
27
|
+
lines.push(`<details><summary>Skipped — looks like a non-Playwright test (Jest/Vitest/RTL/Cypress) (${result.skippedFiles.length})</summary>`);
|
|
28
28
|
lines.push('');
|
|
29
29
|
for (const f of result.skippedFiles) {
|
|
30
30
|
lines.push(`- \`${f}\``);
|
package/dist/formatters/text.js
CHANGED
|
@@ -26,7 +26,7 @@ export function formatText(result) {
|
|
|
26
26
|
}
|
|
27
27
|
if (result.skippedFiles?.length) {
|
|
28
28
|
lines.push('');
|
|
29
|
-
lines.push(`Skipped (looks like a non-Playwright test — Jest/Vitest/RTL — ${result.skippedFiles.length}):`);
|
|
29
|
+
lines.push(`Skipped (looks like a non-Playwright test — Jest/Vitest/RTL/Cypress — ${result.skippedFiles.length}):`);
|
|
30
30
|
for (const f of result.skippedFiles.slice(0, 20)) {
|
|
31
31
|
lines.push(` ${f}`);
|
|
32
32
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,20 @@ export { formatJson } from './formatters/json.js';
|
|
|
14
14
|
export { formatText } from './formatters/text.js';
|
|
15
15
|
export { formatMarkdown } from './formatters/markdown.js';
|
|
16
16
|
export { formatSarif } from './formatters/sarif.js';
|
|
17
|
-
|
|
17
|
+
// `.e2e.`/`.e2e-spec.`/`.e2e-test.` are real, common conventions for
|
|
18
|
+
// disambiguating end-to-end specs from unit tests living in the same tree
|
|
19
|
+
// — verified against two well-known, large real-world repos where a
|
|
20
|
+
// directory scan previously hard-failed with "no files matched" despite a
|
|
21
|
+
// fully healthy, populated suite: cal.com (53 specs, all *.e2e.ts) and
|
|
22
|
+
// Immich (52 specs, all *.e2e-spec.ts). looksLikeNonPlaywrightTest (see
|
|
23
|
+
// metrics.ts) is the safety net for a repo that uses `.e2e.` for a
|
|
24
|
+
// *different* framework — Cypress specifically was common enough here to
|
|
25
|
+
// get its own explicit check alongside the existing Jest/Vitest/RTL ones.
|
|
26
|
+
const SPEC_GLOBS = [
|
|
27
|
+
'**/*.{spec,test,e2e,e2e-spec,e2e-test}.{ts,tsx,js,jsx}',
|
|
28
|
+
'**/*.spec.ts',
|
|
29
|
+
'**/*.test.ts',
|
|
30
|
+
];
|
|
18
31
|
// Directory/glob expansion must never sweep in vendored or generated code —
|
|
19
32
|
// verified as a real bug: scanning a project root with a plain node_modules
|
|
20
33
|
// dependency that ships its own *.spec.ts files silently included them
|
|
@@ -43,7 +56,7 @@ const DEFAULT_IGNORE_GLOBS = [
|
|
|
43
56
|
* `expanded`: everything else (matched via a directory scan or a glob
|
|
44
57
|
* pattern) — subject to the looksLikePlaywrightSpec filter in scorePaths,
|
|
45
58
|
* since a broad `**\/*.test.ts`-style match can just as easily sweep in
|
|
46
|
-
* unrelated Jest/Vitest/RTL unit tests sitting in the same repo.
|
|
59
|
+
* unrelated Jest/Vitest/RTL/Cypress unit tests sitting in the same repo.
|
|
47
60
|
*/
|
|
48
61
|
function expandPaths(inputs, cwd) {
|
|
49
62
|
const explicit = new Set();
|
|
@@ -166,7 +179,7 @@ export async function scorePaths(options) {
|
|
|
166
179
|
{
|
|
167
180
|
rule: 'playwright-score/no-files',
|
|
168
181
|
severity: 'error',
|
|
169
|
-
message: `${expanded.length} file(s) matched but all look like non-Playwright tests (Jest/Vitest/RTL) — nothing to score.`,
|
|
182
|
+
message: `${expanded.length} file(s) matched but all look like non-Playwright tests (Jest/Vitest/RTL/Cypress) — nothing to score.`,
|
|
170
183
|
file: cwd,
|
|
171
184
|
dimension: 'structure',
|
|
172
185
|
},
|
package/dist/metrics.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Finding, LocatorCounts } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Heuristic: does this file show positive evidence of being a *non*-
|
|
4
|
-
* Playwright test (Jest/Vitest/Mocha/Jasmine/React Testing Library
|
|
5
|
-
* opposed to a Playwright spec that a broad `**\/*.test.ts`
|
|
6
|
-
* otherwise sweep in alongside real e2e specs? Used only to
|
|
7
|
-
* to auto-exclude a file discovered via directory/glob
|
|
8
|
-
* applied to a file path the caller named explicitly.
|
|
4
|
+
* Playwright test (Jest/Vitest/Mocha/Jasmine/React Testing Library/
|
|
5
|
+
* Cypress), as opposed to a Playwright spec that a broad `**\/*.test.ts`
|
|
6
|
+
* glob would otherwise sweep in alongside real e2e specs? Used only to
|
|
7
|
+
* decide whether to auto-exclude a file discovered via directory/glob
|
|
8
|
+
* expansion — never applied to a file path the caller named explicitly.
|
|
9
9
|
*/
|
|
10
10
|
export declare function looksLikeNonPlaywrightTest(source: string): boolean;
|
|
11
11
|
export declare function countLocators(source: string, file?: string): LocatorCounts;
|
package/dist/metrics.js
CHANGED
|
@@ -13,18 +13,27 @@ import { countSloc } from './sloc.js';
|
|
|
13
13
|
// in one real suite). Denylisting known non-Playwright frameworks is much
|
|
14
14
|
// safer — a missed rare/unusual file just scores harmlessly, where a
|
|
15
15
|
// falsely-skipped real spec silently stops being scored at all.
|
|
16
|
-
const NON_PLAYWRIGHT_IMPORT_RE = /from\s+['"](?:vitest|jest|@jest\/[a-z-]+|mocha|jasmine|chai|@testing-library\/[a-z-]
|
|
16
|
+
const NON_PLAYWRIGHT_IMPORT_RE = /from\s+['"](?:vitest|jest|@jest\/[a-z-]+|mocha|jasmine|chai|@testing-library\/[a-z-]+|cypress)['"]/;
|
|
17
17
|
const RTL_API_RE = /\bscreen\.(?:getBy|queryBy|findBy|getAllBy|queryAllBy|findAllBy)\w*\(|\btoBeInTheDocument\(/;
|
|
18
|
+
// Cypress's `cy` command object is globally available without an import in
|
|
19
|
+
// the framework's default setup, the same way RTL's `screen` often is —
|
|
20
|
+
// matched on specific, common Cypress command names (not a bare `cy.`
|
|
21
|
+
// prefix) to avoid colliding with an unrelated local variable named `cy`.
|
|
22
|
+
// Added alongside SPEC_GLOBS picking up `*.e2e.*` by default (see
|
|
23
|
+
// index.ts): `.e2e.` is Cypress's own scaffolding convention too, so
|
|
24
|
+
// broadening spec discovery to that suffix needed this as a companion
|
|
25
|
+
// safety net, not just Playwright's own conventions.
|
|
26
|
+
const CYPRESS_API_RE = /\bcy\.(?:visit|get|contains|click|type|should|intercept|wait|request|fixture)\(/;
|
|
18
27
|
/**
|
|
19
28
|
* Heuristic: does this file show positive evidence of being a *non*-
|
|
20
|
-
* Playwright test (Jest/Vitest/Mocha/Jasmine/React Testing Library
|
|
21
|
-
* opposed to a Playwright spec that a broad `**\/*.test.ts`
|
|
22
|
-
* otherwise sweep in alongside real e2e specs? Used only to
|
|
23
|
-
* to auto-exclude a file discovered via directory/glob
|
|
24
|
-
* applied to a file path the caller named explicitly.
|
|
29
|
+
* Playwright test (Jest/Vitest/Mocha/Jasmine/React Testing Library/
|
|
30
|
+
* Cypress), as opposed to a Playwright spec that a broad `**\/*.test.ts`
|
|
31
|
+
* glob would otherwise sweep in alongside real e2e specs? Used only to
|
|
32
|
+
* decide whether to auto-exclude a file discovered via directory/glob
|
|
33
|
+
* expansion — never applied to a file path the caller named explicitly.
|
|
25
34
|
*/
|
|
26
35
|
export function looksLikeNonPlaywrightTest(source) {
|
|
27
|
-
return NON_PLAYWRIGHT_IMPORT_RE.test(source) || RTL_API_RE.test(source);
|
|
36
|
+
return NON_PLAYWRIGHT_IMPORT_RE.test(source) || RTL_API_RE.test(source) || CYPRESS_API_RE.test(source);
|
|
28
37
|
}
|
|
29
38
|
const NATIVE_METHODS = new Set([
|
|
30
39
|
'getByRole',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qaguardian/playwright-score",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Deterministic, AI-free Playwright spec quality score (sqs-v1). Lint + score with standard and guardian profiles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|