@qaguardian/playwright-score 0.1.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/LICENSE +21 -0
- package/METHODOLOGY.md +178 -0
- package/README.md +109 -0
- package/bin/playwright-score.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +113 -0
- package/dist/eslint-runner.d.ts +10 -0
- package/dist/eslint-runner.js +120 -0
- package/dist/formatters/json.d.ts +2 -0
- package/dist/formatters/json.js +3 -0
- package/dist/formatters/markdown.d.ts +2 -0
- package/dist/formatters/markdown.js +26 -0
- package/dist/formatters/sarif.d.ts +3 -0
- package/dist/formatters/sarif.js +55 -0
- package/dist/formatters/text.d.ts +2 -0
- package/dist/formatters/text.js +32 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +95 -0
- package/dist/metrics.d.ts +17 -0
- package/dist/metrics.js +79 -0
- package/dist/profiles.d.ts +15 -0
- package/dist/profiles.js +130 -0
- package/dist/rules/guardian-plugin.d.ts +29 -0
- package/dist/rules/guardian-plugin.js +285 -0
- package/dist/score-engine.d.ts +29 -0
- package/dist/score-engine.js +114 -0
- package/dist/sloc.d.ts +4 -0
- package/dist/sloc.js +38 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QA Guardian / playwright-score contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/METHODOLOGY.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Playwright Spec Score (SQS) Methodology
|
|
2
|
+
|
|
3
|
+
**Score version:** `sqs-v1`
|
|
4
|
+
**Package:** `playwright-score`
|
|
5
|
+
**Landing page:** https://qaguardian.com/open-source/playwright-score
|
|
6
|
+
**Maintainer:** [QA Guardian](https://qaguardian.com)
|
|
7
|
+
**Deterministic:** same inputs → same score
|
|
8
|
+
**AI-free:** no LLM calls in the scoring path
|
|
9
|
+
|
|
10
|
+
Any change to formulas, weights, SLOC definition, caps, or constants requires a new version (`sqs-v2`, …).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Profiles
|
|
15
|
+
|
|
16
|
+
| Profile | Purpose |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `standard` | Community Playwright best practices (`eslint-plugin-playwright`) + suite metrics |
|
|
19
|
+
| `guardian` | `standard` + QA Guardian house conventions (optional; clearly labeled) |
|
|
20
|
+
|
|
21
|
+
House rules are **not** claimed to be official Playwright doctrine.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Output
|
|
26
|
+
|
|
27
|
+
- **score:** integer 0–100
|
|
28
|
+
- **grade:** A (90–100), B (80–89), C (70–79), D (60–69), F (0–59)
|
|
29
|
+
- **pass:** `score >= threshold`
|
|
30
|
+
- **dimensions:** per-dimension 0–100
|
|
31
|
+
- **findings:** full list (uncapped) for humans/agents
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Dimension weights
|
|
36
|
+
|
|
37
|
+
### `standard`
|
|
38
|
+
|
|
39
|
+
| Dimension | Weight |
|
|
40
|
+
|---|---|
|
|
41
|
+
| playwrightHygiene | 40 |
|
|
42
|
+
| assertions | 25 |
|
|
43
|
+
| locators | 20 |
|
|
44
|
+
| structure | 15 |
|
|
45
|
+
|
|
46
|
+
### `guardian`
|
|
47
|
+
|
|
48
|
+
| Dimension | Weight |
|
|
49
|
+
|---|---|
|
|
50
|
+
| playwrightHygiene | 30 |
|
|
51
|
+
| assertions | 15 |
|
|
52
|
+
| locators | 15 |
|
|
53
|
+
| structure | 15 |
|
|
54
|
+
| guardianConventions | 25 |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Locators dimension (ratio)
|
|
59
|
+
|
|
60
|
+
Independent of finding-penalty math:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
native = count(getByRole|getByLabel|getByTestId|getByText|getByPlaceholder)
|
|
64
|
+
raw = count(page.locator(|frame.locator()
|
|
65
|
+
total = native + raw
|
|
66
|
+
|
|
67
|
+
locatorsScore = total === 0 ? 100 : round(100 * native / total)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
ESLint `no-raw-locators` / `prefer-native-locators` still appear in `findings[]` for repair guidance.
|
|
71
|
+
They are **not** double-counted into the locators dimension via exponential penalties.
|
|
72
|
+
For sqs-v1 they map to **report-only** for scoring (severity still shown in findings; units = 0 for penalty dimensions).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## SLOC (source lines of code)
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
sloc = lines that are NOT:
|
|
80
|
+
- blank / whitespace-only
|
|
81
|
+
- pure comment lines (// or block-comment-only lines)
|
|
82
|
+
sloc = max(sloc, 1)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Used for penalty density. JSDoc / decorative comments do not inflate slots.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Penalty dimensions
|
|
90
|
+
|
|
91
|
+
Applies to: `playwrightHygiene`, `assertions`, `structure`, `guardianConventions`.
|
|
92
|
+
|
|
93
|
+
### Constants (frozen sqs-v1)
|
|
94
|
+
|
|
95
|
+
| Constant | Value |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `ERROR_UNIT` | 1.0 |
|
|
98
|
+
| `WARNING_UNIT` | 0.4 |
|
|
99
|
+
| `INFO_UNIT` | 0.0 |
|
|
100
|
+
| `MAX_FINDINGS_PER_RULE_PER_FILE` | 3 (for penalty math only) |
|
|
101
|
+
| `SLOT_DIVISOR` | 25 |
|
|
102
|
+
| `MIN_SLOTS` | 4 |
|
|
103
|
+
| `K` | 0.4 |
|
|
104
|
+
|
|
105
|
+
### Steps
|
|
106
|
+
|
|
107
|
+
1. **Cap** findings per `(file, ruleId)` to 3 for penalty math (report all in `findings[]`).
|
|
108
|
+
2. **Units:** `rawUnits = E * 1.0 + W * 0.4`
|
|
109
|
+
3. **Slots:** `slots = max(sloc / 25, 4)`
|
|
110
|
+
4. **Load:** `load = rawUnits / slots`
|
|
111
|
+
5. **Score:** `dimensionScore = clamp(0, 100, round(100 * exp(-0.4 * load)))`
|
|
112
|
+
|
|
113
|
+
### Final score
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
score = round(Σ (weight_d / 100) * dimensionScore_d)
|
|
117
|
+
pass = score >= threshold
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Worked examples (Phase 0 validation)
|
|
123
|
+
|
|
124
|
+
Assumptions: single-file runs; weights as above.
|
|
125
|
+
|
|
126
|
+
### 1. Tiny clean (~15 SLOC, 0 findings, all native locators)
|
|
127
|
+
|
|
128
|
+
- All penalty dims = 100
|
|
129
|
+
- Locators = 100
|
|
130
|
+
- **standard score ≈ 100**
|
|
131
|
+
|
|
132
|
+
### 2. Tiny file, 1 hygiene error (~20 SLOC)
|
|
133
|
+
|
|
134
|
+
- slots = max(20/25, 4) = 4
|
|
135
|
+
- rawUnits = 1.0 → load = 0.25 → hygiene = round(100 * e^(-0.1)) ≈ **90**
|
|
136
|
+
- Other dims 100
|
|
137
|
+
- standard ≈ 0.4*90 + 0.25*100 + 0.2*100 + 0.15*100 = **96**
|
|
138
|
+
|
|
139
|
+
### 3. Medium file, 5 distinct hygiene errors (~100 SLOC)
|
|
140
|
+
|
|
141
|
+
- slots = max(100/25, 4) = 4
|
|
142
|
+
- rawUnits = 5 → load = 1.25 → hygiene = round(100 * e^(-0.5)) ≈ **61**
|
|
143
|
+
- standard ≈ 0.4*61 + 60 = **84** if other dims perfect
|
|
144
|
+
|
|
145
|
+
### 4. Large file, 20× same warning (capped to 3)
|
|
146
|
+
|
|
147
|
+
- 20 warnings same rule → cap 3 → rawUnits = 3 * 0.4 = 1.2
|
|
148
|
+
- sloc = 500 → slots = 20 → load = 0.06 → score ≈ **98** on that dim
|
|
149
|
+
- Cap prevents auto-zero
|
|
150
|
+
|
|
151
|
+
### 5. Locator ratio 2 native / 10 total
|
|
152
|
+
|
|
153
|
+
- locatorsScore = round(100 * 2/10) = **20**
|
|
154
|
+
- Independent of caps
|
|
155
|
+
|
|
156
|
+
These examples feel fair: tiny single error stays high; multi-error drops materialy; repeated same-rule does not nuke large files; locator mix is a pure ratio.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Default thresholds
|
|
161
|
+
|
|
162
|
+
| Context | Profile | Threshold |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| CLI default | standard | 80 |
|
|
165
|
+
| QAG dogfood (warn) | guardian | 75 → 80 |
|
|
166
|
+
| QAG gate (later) | guardian | 80 |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Rule → dimension map (summary)
|
|
171
|
+
|
|
172
|
+
See package rule docs / source `profiles.ts`. High level:
|
|
173
|
+
|
|
174
|
+
- **playwrightHygiene:** waits, force, networkidle, missing await, handles, conditionals
|
|
175
|
+
- **assertions:** expect-expect, empty tests, web-first preference
|
|
176
|
+
- **locators:** ratio metric only (for score); raw-locator ESLint in findings
|
|
177
|
+
- **structure:** focused/skipped, oversized file, describe shape
|
|
178
|
+
- **guardianConventions:** load-state ban, Date.now ban, secrets, test.step, one-describe-one-test, etc.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @qaguardian/playwright-score
|
|
2
|
+
|
|
3
|
+
**Deterministic, AI-free quality score for Playwright specs** (`sqs-v1`).
|
|
4
|
+
|
|
5
|
+
**Website:** [qaguardian.com/open-source/playwright-score](https://qaguardian.com/open-source/playwright-score)
|
|
6
|
+
**Built by:** [QA Guardian](https://qaguardian.com) — managed Playwright E2E (AI drafts, engineers verify, you own the code)
|
|
7
|
+
|
|
8
|
+
Lint + score Playwright tests with:
|
|
9
|
+
|
|
10
|
+
- **`standard`** — community best practices via [`eslint-plugin-playwright`](https://github.com/playwright-community/eslint-plugin-playwright) + suite metrics
|
|
11
|
+
- **`guardian`** — optional house conventions (QA Guardian codegen gate); not official Playwright doctrine
|
|
12
|
+
|
|
13
|
+
The score never calls an LLM. AI may *generate* or *repair* code using findings; **rules grade code**.
|
|
14
|
+
|
|
15
|
+
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).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -D @qaguardian/playwright-score
|
|
21
|
+
# or run from this package
|
|
22
|
+
npm install && npm run build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## CLI
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @qaguardian/playwright-score ./tests --profile standard --threshold 80
|
|
29
|
+
npx @qaguardian/playwright-score ./flow.spec.ts --profile guardian --format json --out report.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Flag | Description |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `--profile standard\|guardian` | Scoring profile (default `standard`) |
|
|
35
|
+
| `--threshold <n>` | Pass bar 0–100 |
|
|
36
|
+
| `--format text\|json\|markdown\|sarif` | Output format |
|
|
37
|
+
| `--out <file>` | Write report to file |
|
|
38
|
+
|
|
39
|
+
**Exit codes:** `0` pass · `1` below threshold · `2` tool error
|
|
40
|
+
|
|
41
|
+
## Library
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { scorePaths } from '@qaguardian/playwright-score';
|
|
45
|
+
|
|
46
|
+
const result = await scorePaths({
|
|
47
|
+
paths: ['tests/login.spec.ts'],
|
|
48
|
+
profile: 'standard',
|
|
49
|
+
threshold: 80,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(result.score, result.grade, result.pass, result.findings);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## CI example
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
- name: Playwright Spec Score
|
|
59
|
+
run: npx playwright-score ./tests --profile standard --threshold 80 --format text
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## QA Guardian integration
|
|
63
|
+
|
|
64
|
+
When used from `playwright_runner`, set:
|
|
65
|
+
|
|
66
|
+
| Env | Values | Default |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `SPEC_SCORE_MODE` | `off` \| `warn` \| `gate` | `warn` |
|
|
69
|
+
| `SPEC_SCORE_PROFILE` | `standard` \| `guardian` | `guardian` |
|
|
70
|
+
| `SPEC_SCORE_THRESHOLD` | `0`–`100` | `75` (guardian) / `80` (standard) |
|
|
71
|
+
|
|
72
|
+
- **`warn`**: log score; never fail the run; findings still inject into heal/generate repair prompts
|
|
73
|
+
- **`gate`**: fail the run when score < threshold
|
|
74
|
+
- After AI **generate**, specs below threshold get one automatic **heal** pass with score findings
|
|
75
|
+
- **Heal** prompts always include score findings when the scorer reports issues
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Local from monorepo
|
|
79
|
+
cd playwright-score && npm run build
|
|
80
|
+
cd ../playwright_runner && npm install
|
|
81
|
+
SPEC_SCORE_MODE=warn SPEC_SCORE_PROFILE=guardian node ...
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Profiles
|
|
85
|
+
|
|
86
|
+
| Profile | Use when |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `standard` | Any Playwright repo — best practices + locator mix + empty-expect detection |
|
|
89
|
+
| `guardian` | QA Guardian (or similar) codegen quality gate |
|
|
90
|
+
|
|
91
|
+
Disable a guardian secrets false positive:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// spec-score-disable-next-line guardian/no-hardcoded-secrets
|
|
95
|
+
const password = 'intentional-fixture-value-xyz';
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm install
|
|
102
|
+
npm run build
|
|
103
|
+
npm test
|
|
104
|
+
npm run score -- ./fixtures/good-standard.spec.ts
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* playwright-score CLI
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { scorePaths } from './index.js';
|
|
8
|
+
import { formatJson } from './formatters/json.js';
|
|
9
|
+
import { formatText } from './formatters/text.js';
|
|
10
|
+
import { formatMarkdown } from './formatters/markdown.js';
|
|
11
|
+
import { formatSarif } from './formatters/sarif.js';
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(`playwright-score — deterministic Playwright spec quality score (sqs-v1)
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
playwright-score <paths...> [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--profile <standard|guardian> Scoring profile (default: standard)
|
|
20
|
+
--threshold <n> Pass threshold 0-100 (default: 80 standard / 75 guardian)
|
|
21
|
+
--format <text|json|markdown|sarif> Output format (default: text)
|
|
22
|
+
--out <file> Write report to file
|
|
23
|
+
--help Show help
|
|
24
|
+
|
|
25
|
+
Exit codes:
|
|
26
|
+
0 score >= threshold
|
|
27
|
+
1 score < threshold
|
|
28
|
+
2 tool error
|
|
29
|
+
|
|
30
|
+
See METHODOLOGY.md for sqs-v1 formula.
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
async function main() {
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
36
|
+
printHelp();
|
|
37
|
+
process.exit(args.length === 0 ? 2 : 0);
|
|
38
|
+
}
|
|
39
|
+
let profile = 'standard';
|
|
40
|
+
let threshold;
|
|
41
|
+
let format = 'text';
|
|
42
|
+
let out;
|
|
43
|
+
const paths = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const a = args[i];
|
|
46
|
+
if (a === '--profile') {
|
|
47
|
+
profile = args[++i];
|
|
48
|
+
if (profile !== 'standard' && profile !== 'guardian') {
|
|
49
|
+
console.error(`Invalid profile: ${profile}`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (a === '--threshold') {
|
|
54
|
+
threshold = Number(args[++i]);
|
|
55
|
+
if (Number.isNaN(threshold) || threshold < 0 || threshold > 100) {
|
|
56
|
+
console.error('threshold must be 0-100');
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (a === '--format') {
|
|
61
|
+
const f = args[++i];
|
|
62
|
+
if (f !== 'text' && f !== 'json' && f !== 'markdown' && f !== 'sarif') {
|
|
63
|
+
console.error(`Invalid format: ${f}`);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
format = f;
|
|
67
|
+
}
|
|
68
|
+
else if (a === '--out') {
|
|
69
|
+
out = args[++i];
|
|
70
|
+
}
|
|
71
|
+
else if (a.startsWith('-')) {
|
|
72
|
+
console.error(`Unknown option: ${a}`);
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
paths.push(a);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (paths.length === 0) {
|
|
80
|
+
console.error('At least one path is required');
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const result = await scorePaths({ paths, profile, threshold });
|
|
85
|
+
let body;
|
|
86
|
+
switch (format) {
|
|
87
|
+
case 'json':
|
|
88
|
+
body = formatJson(result);
|
|
89
|
+
break;
|
|
90
|
+
case 'markdown':
|
|
91
|
+
body = formatMarkdown(result);
|
|
92
|
+
break;
|
|
93
|
+
case 'sarif':
|
|
94
|
+
body = formatSarif(result);
|
|
95
|
+
break;
|
|
96
|
+
default:
|
|
97
|
+
body = formatText(result);
|
|
98
|
+
}
|
|
99
|
+
if (out) {
|
|
100
|
+
fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true });
|
|
101
|
+
fs.writeFileSync(out, body + '\n', 'utf8');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(body);
|
|
105
|
+
}
|
|
106
|
+
process.exit(result.pass ? 0 : 1);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.error(err instanceof Error ? err.message : err);
|
|
110
|
+
process.exit(2);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
main();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Finding, ProfileName } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run ESLint once per scorePaths invocation with embedded config only.
|
|
4
|
+
* Does not load host-repo eslint.config.js.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runEslint(options: {
|
|
7
|
+
files: string[];
|
|
8
|
+
profile: ProfileName;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
}): Promise<Finding[]>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ESLint } from 'eslint';
|
|
3
|
+
import playwright from 'eslint-plugin-playwright';
|
|
4
|
+
import { guardianPlugin, guardianRuleConfigs, } from './rules/guardian-plugin.js';
|
|
5
|
+
import { mapRule } from './profiles.js';
|
|
6
|
+
function buildConfig(profile) {
|
|
7
|
+
const playwrightRecommended = playwright.configs?.['flat/recommended'] ??
|
|
8
|
+
playwright.configs?.recommended;
|
|
9
|
+
const base = [
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
ecmaVersion: 2022,
|
|
14
|
+
sourceType: 'module',
|
|
15
|
+
parser: undefined, // set below if available
|
|
16
|
+
},
|
|
17
|
+
plugins: {
|
|
18
|
+
playwright,
|
|
19
|
+
...(profile === 'guardian' ? { guardian: guardianPlugin } : {}),
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
// Ensure key hygiene rules even if recommended set differs by version
|
|
23
|
+
'playwright/no-wait-for-timeout': 'error',
|
|
24
|
+
'playwright/no-networkidle': 'error',
|
|
25
|
+
'playwright/no-force-option': 'error',
|
|
26
|
+
'playwright/no-wait-for-selector': 'error',
|
|
27
|
+
'playwright/no-wait-for-navigation': 'error',
|
|
28
|
+
'playwright/missing-playwright-await': 'error',
|
|
29
|
+
'playwright/expect-expect': 'error',
|
|
30
|
+
'playwright/no-focused-test': 'error',
|
|
31
|
+
'playwright/no-skipped-test': 'warn',
|
|
32
|
+
'playwright/no-raw-locators': 'warn',
|
|
33
|
+
'playwright/prefer-web-first-assertions': 'warn',
|
|
34
|
+
...(profile === 'guardian' ? { ...guardianRuleConfigs } : {}),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
// Prefer spreading official recommended flat config when available
|
|
39
|
+
if (Array.isArray(playwrightRecommended)) {
|
|
40
|
+
const configs = playwrightRecommended;
|
|
41
|
+
if (profile === 'guardian') {
|
|
42
|
+
return [
|
|
43
|
+
...configs,
|
|
44
|
+
{
|
|
45
|
+
plugins: { guardian: guardianPlugin },
|
|
46
|
+
rules: { ...guardianRuleConfigs },
|
|
47
|
+
},
|
|
48
|
+
// our severity tweaks
|
|
49
|
+
base[0],
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
return [...configs, base[0]];
|
|
53
|
+
}
|
|
54
|
+
return base;
|
|
55
|
+
}
|
|
56
|
+
function toSeverity(eslintSeverity, override) {
|
|
57
|
+
if (override)
|
|
58
|
+
return override;
|
|
59
|
+
if (eslintSeverity === 2)
|
|
60
|
+
return 'error';
|
|
61
|
+
if (eslintSeverity === 1)
|
|
62
|
+
return 'warning';
|
|
63
|
+
return 'info';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run ESLint once per scorePaths invocation with embedded config only.
|
|
67
|
+
* Does not load host-repo eslint.config.js.
|
|
68
|
+
*/
|
|
69
|
+
export async function runEslint(options) {
|
|
70
|
+
const cwd = options.cwd ?? process.cwd();
|
|
71
|
+
const overrideConfig = buildConfig(options.profile);
|
|
72
|
+
// Try to attach typescript-eslint parser for .ts files
|
|
73
|
+
try {
|
|
74
|
+
const tsParser = await import('@typescript-eslint/parser');
|
|
75
|
+
for (const cfg of overrideConfig) {
|
|
76
|
+
if (cfg.files) {
|
|
77
|
+
cfg.languageOptions = {
|
|
78
|
+
...cfg.languageOptions,
|
|
79
|
+
parser: tsParser,
|
|
80
|
+
parserOptions: {
|
|
81
|
+
ecmaVersion: 2022,
|
|
82
|
+
sourceType: 'module',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// parser optional for pure JS
|
|
90
|
+
}
|
|
91
|
+
const eslint = new ESLint({
|
|
92
|
+
cwd,
|
|
93
|
+
// Prevent loading ambient project ESLint configs (ESLint 9 flat API)
|
|
94
|
+
overrideConfigFile: true,
|
|
95
|
+
overrideConfig: overrideConfig,
|
|
96
|
+
// Ignore host ignore files that might exclude generated specs
|
|
97
|
+
ignore: false,
|
|
98
|
+
});
|
|
99
|
+
const results = await eslint.lintFiles(options.files);
|
|
100
|
+
const findings = [];
|
|
101
|
+
for (const result of results) {
|
|
102
|
+
const file = path.resolve(result.filePath);
|
|
103
|
+
for (const msg of result.messages) {
|
|
104
|
+
if (!msg.ruleId)
|
|
105
|
+
continue;
|
|
106
|
+
const mapping = mapRule(msg.ruleId);
|
|
107
|
+
findings.push({
|
|
108
|
+
rule: msg.ruleId,
|
|
109
|
+
severity: toSeverity(msg.severity, mapping.severityOverride),
|
|
110
|
+
message: msg.message,
|
|
111
|
+
file,
|
|
112
|
+
line: msg.line,
|
|
113
|
+
column: msg.column,
|
|
114
|
+
dimension: mapping.dimension,
|
|
115
|
+
reportOnly: mapping.reportOnly,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return findings;
|
|
120
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function formatMarkdown(result) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
lines.push(`# Playwright Spec Score (\`${result.scoreVersion}\`)`);
|
|
4
|
+
lines.push('');
|
|
5
|
+
lines.push(`**${result.score}/100 (${result.grade})** — ${result.pass ? 'PASS' : 'FAIL'} · profile \`${result.profile}\` · threshold ${result.threshold}`);
|
|
6
|
+
lines.push('');
|
|
7
|
+
lines.push('| Dimension | Score |');
|
|
8
|
+
lines.push('|---|---|');
|
|
9
|
+
for (const [k, v] of Object.entries(result.dimensions)) {
|
|
10
|
+
if (v === undefined)
|
|
11
|
+
continue;
|
|
12
|
+
lines.push(`| ${k} | ${v} |`);
|
|
13
|
+
}
|
|
14
|
+
lines.push('');
|
|
15
|
+
lines.push(`Files: ${result.summary.files} · Tests: ${result.summary.tests} · SLOC: ${result.summary.sloc} · Findings: ${result.summary.findings}`);
|
|
16
|
+
if (result.findings.length) {
|
|
17
|
+
lines.push('');
|
|
18
|
+
lines.push('## Findings');
|
|
19
|
+
lines.push('');
|
|
20
|
+
for (const f of result.findings.slice(0, 40)) {
|
|
21
|
+
const loc = f.line ? `:${f.line}` : '';
|
|
22
|
+
lines.push(`- **${f.severity}** \`${f.rule}\` \`${f.file}${loc}\` — ${f.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return lines.join('\n');
|
|
26
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Minimal SARIF 2.1.0 for CI integrations */
|
|
2
|
+
export function formatSarif(result) {
|
|
3
|
+
const rulesMap = new Map();
|
|
4
|
+
for (const f of result.findings) {
|
|
5
|
+
if (!rulesMap.has(f.rule)) {
|
|
6
|
+
rulesMap.set(f.rule, {
|
|
7
|
+
id: f.rule,
|
|
8
|
+
shortDescription: { text: f.rule },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const sarif = {
|
|
13
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
14
|
+
version: '2.1.0',
|
|
15
|
+
runs: [
|
|
16
|
+
{
|
|
17
|
+
tool: {
|
|
18
|
+
driver: {
|
|
19
|
+
name: 'playwright-score',
|
|
20
|
+
informationUri: 'https://github.com/qaguardian/playwright-score',
|
|
21
|
+
version: result.scoreVersion,
|
|
22
|
+
rules: [...rulesMap.values()],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
results: result.findings.map((f) => ({
|
|
26
|
+
ruleId: f.rule,
|
|
27
|
+
level: f.severity === 'error'
|
|
28
|
+
? 'error'
|
|
29
|
+
: f.severity === 'warning'
|
|
30
|
+
? 'warning'
|
|
31
|
+
: 'note',
|
|
32
|
+
message: { text: f.message },
|
|
33
|
+
locations: [
|
|
34
|
+
{
|
|
35
|
+
physicalLocation: {
|
|
36
|
+
artifactLocation: { uri: f.file },
|
|
37
|
+
region: f.line
|
|
38
|
+
? { startLine: f.line, startColumn: f.column ?? 1 }
|
|
39
|
+
: undefined,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
})),
|
|
44
|
+
properties: {
|
|
45
|
+
score: result.score,
|
|
46
|
+
grade: result.grade,
|
|
47
|
+
pass: result.pass,
|
|
48
|
+
profile: result.profile,
|
|
49
|
+
threshold: result.threshold,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
return JSON.stringify(sarif, null, 2);
|
|
55
|
+
}
|