@qaguardian/playwright-score 0.1.11 → 0.1.13
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/dist/formatters/markdown.js +1 -1
- package/dist/formatters/text.js +1 -1
- package/dist/index.js +16 -3
- package/dist/metrics.d.ts +31 -20
- package/dist/metrics.js +121 -25
- package/package.json +1 -1
|
@@ -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.` is a real, common convention for disambiguating end-to-end specs
|
|
18
|
+
// from unit tests living in the same tree — verified against a
|
|
19
|
+
// well-known, large real-world repo (cal.com: 53 real Playwright specs,
|
|
20
|
+
// all named *.e2e.ts, zero *.spec.ts/*.test.ts) where a directory scan
|
|
21
|
+
// previously hard-failed with "no files matched" despite a fully healthy,
|
|
22
|
+
// populated suite. looksLikeNonPlaywrightTest (see metrics.ts) is the
|
|
23
|
+
// safety net for a repo that uses `.e2e.` for a *different* framework —
|
|
24
|
+
// Cypress specifically was common enough here to get its own explicit
|
|
25
|
+
// check alongside the existing Jest/Vitest/RTL ones.
|
|
26
|
+
const SPEC_GLOBS = [
|
|
27
|
+
'**/*.{spec,test,e2e}.{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,30 +1,41 @@
|
|
|
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;
|
|
12
12
|
/**
|
|
13
|
-
* Names of same-file
|
|
14
|
-
* an arrow/function expression
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
13
|
+
* Names of same-file assertion helpers — function declarations, a const
|
|
14
|
+
* bound to an arrow/function expression, or a class method (`kind:
|
|
15
|
+
* 'method'`, e.g. a Page Object Model's `async expectNodeVisible(name) {
|
|
16
|
+
* await expect(...) }`) — whose own body contains an expect(...)-shaped
|
|
17
|
+
* call, a `.waitFor(...)` call (see containsWaitForCall), or a call to a
|
|
18
|
+
* name already recognized by an earlier round (so e.g.
|
|
19
|
+
* `waitForNotificationAndClose()` calling `this.waitForNotification()`,
|
|
20
|
+
* itself resolved to a direct `.waitFor()` call, is recognized too —
|
|
21
|
+
* verified against a real Page Object Model, n8n's NotificationsPage,
|
|
22
|
+
* where the assertion sits two calls deep). Fed into
|
|
23
|
+
* playwright/expect-expect's own assertFunctionNames option (see
|
|
24
|
+
* eslint-runner.ts) so a test that delegates its assertion to a helper —
|
|
25
|
+
* `audit(page, path)`, `canvas.expectNodeVisible(name)`,
|
|
26
|
+
* `n8n.notifications.waitForNotificationAndClose(text)` — isn't flagged as
|
|
27
|
+
* having no assertions, regardless of what the helper is named or how
|
|
28
|
+
* deep the delegation chain runs *within this one file*. Matching is by
|
|
29
|
+
* name only, any receiver — the same tradeoff eslint-plugin-playwright's
|
|
30
|
+
* own option already makes, so this doesn't introduce a new risk, just
|
|
31
|
+
* extends an already-accepted one from free functions to methods too; a
|
|
32
|
+
* coincidental name collision between an asserting and non-asserting
|
|
33
|
+
* function is the known, accepted failure mode, same as before.
|
|
34
|
+
* Deliberately single-file only: resolving `this.foo()` across file
|
|
35
|
+
* boundaries needs real type resolution, well beyond what a fast,
|
|
36
|
+
* syntactic per-file check should attempt (cross-*file* delegation is
|
|
37
|
+
* handled separately, at the call site in index.ts, by running this same
|
|
38
|
+
* function against each imported dependency file too).
|
|
28
39
|
*/
|
|
29
40
|
export declare function findLocalAssertionHelperNames(source: string, file?: string): string[];
|
|
30
41
|
/** Count test( / test.only( / test.skip( / test.fixme( style declarations (approx). */
|
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',
|
|
@@ -172,27 +181,88 @@ function containsExpectCall(node) {
|
|
|
172
181
|
return found;
|
|
173
182
|
}
|
|
174
183
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
* `locator.waitFor({ state: ... })` (state defaults to `'visible'` when
|
|
185
|
+
* omitted) has no purpose other than verifying the element reaches a
|
|
186
|
+
* state — unlike an action method (`.click()`, `.fill()`) that also
|
|
187
|
+
* happens to throw on failure as a side effect, this call exists only to
|
|
188
|
+
* check something, functionally equivalent to
|
|
189
|
+
* `expect(locator).toBeVisible()`/`toBeHidden()`. Matched on the exact
|
|
190
|
+
* property name `waitFor` (not a prefix), so it can't be confused with
|
|
191
|
+
* Page's differently-named `waitForEvent`/`waitForURL`/`waitForResponse`/
|
|
192
|
+
* `waitForLoadState`/`waitForTimeout`, none of which are assertions.
|
|
193
|
+
*/
|
|
194
|
+
function containsWaitForCall(node) {
|
|
195
|
+
let found = false;
|
|
196
|
+
walk(node, (n) => {
|
|
197
|
+
if (found || n.type !== 'CallExpression')
|
|
198
|
+
return;
|
|
199
|
+
const callee = n.callee;
|
|
200
|
+
if (callee?.type !== 'MemberExpression' || callee.computed)
|
|
201
|
+
return;
|
|
202
|
+
const property = callee.property;
|
|
203
|
+
if (property?.type === 'Identifier' && property.name === 'waitFor')
|
|
204
|
+
found = true;
|
|
205
|
+
});
|
|
206
|
+
return found;
|
|
207
|
+
}
|
|
208
|
+
/** Does `node`'s body contain a call (any receiver, matched by name only —
|
|
209
|
+
* same name-based matching eslint-plugin-playwright's own
|
|
210
|
+
* assertFunctionNames option uses) to one of `names`? */
|
|
211
|
+
function containsCallToAnyName(node, names) {
|
|
212
|
+
if (names.size === 0)
|
|
213
|
+
return false;
|
|
214
|
+
let found = false;
|
|
215
|
+
walk(node, (n) => {
|
|
216
|
+
if (found || n.type !== 'CallExpression')
|
|
217
|
+
return;
|
|
218
|
+
const callee = n.callee;
|
|
219
|
+
let name;
|
|
220
|
+
if (callee?.type === 'Identifier') {
|
|
221
|
+
name = callee.name;
|
|
222
|
+
}
|
|
223
|
+
else if (callee?.type === 'MemberExpression' && !callee.computed) {
|
|
224
|
+
const property = callee.property;
|
|
225
|
+
if (property?.type === 'Identifier')
|
|
226
|
+
name = property.name;
|
|
227
|
+
}
|
|
228
|
+
if (name && names.has(name))
|
|
229
|
+
found = true;
|
|
230
|
+
});
|
|
231
|
+
return found;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Names of same-file assertion helpers — function declarations, a const
|
|
235
|
+
* bound to an arrow/function expression, or a class method (`kind:
|
|
236
|
+
* 'method'`, e.g. a Page Object Model's `async expectNodeVisible(name) {
|
|
237
|
+
* await expect(...) }`) — whose own body contains an expect(...)-shaped
|
|
238
|
+
* call, a `.waitFor(...)` call (see containsWaitForCall), or a call to a
|
|
239
|
+
* name already recognized by an earlier round (so e.g.
|
|
240
|
+
* `waitForNotificationAndClose()` calling `this.waitForNotification()`,
|
|
241
|
+
* itself resolved to a direct `.waitFor()` call, is recognized too —
|
|
242
|
+
* verified against a real Page Object Model, n8n's NotificationsPage,
|
|
243
|
+
* where the assertion sits two calls deep). Fed into
|
|
244
|
+
* playwright/expect-expect's own assertFunctionNames option (see
|
|
245
|
+
* eslint-runner.ts) so a test that delegates its assertion to a helper —
|
|
246
|
+
* `audit(page, path)`, `canvas.expectNodeVisible(name)`,
|
|
247
|
+
* `n8n.notifications.waitForNotificationAndClose(text)` — isn't flagged as
|
|
248
|
+
* having no assertions, regardless of what the helper is named or how
|
|
249
|
+
* deep the delegation chain runs *within this one file*. Matching is by
|
|
250
|
+
* name only, any receiver — the same tradeoff eslint-plugin-playwright's
|
|
251
|
+
* own option already makes, so this doesn't introduce a new risk, just
|
|
252
|
+
* extends an already-accepted one from free functions to methods too; a
|
|
253
|
+
* coincidental name collision between an asserting and non-asserting
|
|
254
|
+
* function is the known, accepted failure mode, same as before.
|
|
255
|
+
* Deliberately single-file only: resolving `this.foo()` across file
|
|
256
|
+
* boundaries needs real type resolution, well beyond what a fast,
|
|
257
|
+
* syntactic per-file check should attempt (cross-*file* delegation is
|
|
258
|
+
* handled separately, at the call site in index.ts, by running this same
|
|
259
|
+
* function against each imported dependency file too).
|
|
190
260
|
*/
|
|
191
261
|
export function findLocalAssertionHelperNames(source, file) {
|
|
192
262
|
const ast = parseSource(source, file);
|
|
193
263
|
if (!ast)
|
|
194
264
|
return [];
|
|
195
|
-
const
|
|
265
|
+
const candidates = [];
|
|
196
266
|
walk(ast, (node) => {
|
|
197
267
|
let name;
|
|
198
268
|
let body;
|
|
@@ -212,9 +282,35 @@ export function findLocalAssertionHelperNames(source, file) {
|
|
|
212
282
|
body = init;
|
|
213
283
|
}
|
|
214
284
|
}
|
|
215
|
-
if (
|
|
216
|
-
|
|
285
|
+
else if (node.type === 'MethodDefinition') {
|
|
286
|
+
const key = node.key;
|
|
287
|
+
if (!node.computed && key?.type === 'Identifier' && node.kind === 'method') {
|
|
288
|
+
name = key.name;
|
|
289
|
+
body = node.value;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (name && body)
|
|
293
|
+
candidates.push({ name, body });
|
|
217
294
|
});
|
|
295
|
+
const names = new Set();
|
|
296
|
+
for (const c of candidates) {
|
|
297
|
+
if (containsExpectCall(c.body) || containsWaitForCall(c.body))
|
|
298
|
+
names.add(c.name);
|
|
299
|
+
}
|
|
300
|
+
// Fixed-point: naturally bounded by candidates.length rounds, since each
|
|
301
|
+
// round adds at least one name or the loop stops.
|
|
302
|
+
let added = true;
|
|
303
|
+
while (added) {
|
|
304
|
+
added = false;
|
|
305
|
+
for (const c of candidates) {
|
|
306
|
+
if (names.has(c.name))
|
|
307
|
+
continue;
|
|
308
|
+
if (containsCallToAnyName(c.body, names)) {
|
|
309
|
+
names.add(c.name);
|
|
310
|
+
added = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
218
314
|
return [...names];
|
|
219
315
|
}
|
|
220
316
|
const TEST_NON_DECL_RE = /\btest\.(describe|step|beforeEach|afterEach|beforeAll|afterAll|use|setTimeout|info|expect)\b/;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qaguardian/playwright-score",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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",
|