@qaguardian/playwright-score 0.1.11 → 0.1.12
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/metrics.d.ts +26 -15
- package/dist/metrics.js +105 -18
- package/package.json +1 -1
package/dist/metrics.d.ts
CHANGED
|
@@ -10,21 +10,32 @@ import type { Finding, LocatorCounts } from './types.js';
|
|
|
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
|
@@ -172,27 +172,88 @@ function containsExpectCall(node) {
|
|
|
172
172
|
return found;
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
175
|
+
* `locator.waitFor({ state: ... })` (state defaults to `'visible'` when
|
|
176
|
+
* omitted) has no purpose other than verifying the element reaches a
|
|
177
|
+
* state — unlike an action method (`.click()`, `.fill()`) that also
|
|
178
|
+
* happens to throw on failure as a side effect, this call exists only to
|
|
179
|
+
* check something, functionally equivalent to
|
|
180
|
+
* `expect(locator).toBeVisible()`/`toBeHidden()`. Matched on the exact
|
|
181
|
+
* property name `waitFor` (not a prefix), so it can't be confused with
|
|
182
|
+
* Page's differently-named `waitForEvent`/`waitForURL`/`waitForResponse`/
|
|
183
|
+
* `waitForLoadState`/`waitForTimeout`, none of which are assertions.
|
|
184
|
+
*/
|
|
185
|
+
function containsWaitForCall(node) {
|
|
186
|
+
let found = false;
|
|
187
|
+
walk(node, (n) => {
|
|
188
|
+
if (found || n.type !== 'CallExpression')
|
|
189
|
+
return;
|
|
190
|
+
const callee = n.callee;
|
|
191
|
+
if (callee?.type !== 'MemberExpression' || callee.computed)
|
|
192
|
+
return;
|
|
193
|
+
const property = callee.property;
|
|
194
|
+
if (property?.type === 'Identifier' && property.name === 'waitFor')
|
|
195
|
+
found = true;
|
|
196
|
+
});
|
|
197
|
+
return found;
|
|
198
|
+
}
|
|
199
|
+
/** Does `node`'s body contain a call (any receiver, matched by name only —
|
|
200
|
+
* same name-based matching eslint-plugin-playwright's own
|
|
201
|
+
* assertFunctionNames option uses) to one of `names`? */
|
|
202
|
+
function containsCallToAnyName(node, names) {
|
|
203
|
+
if (names.size === 0)
|
|
204
|
+
return false;
|
|
205
|
+
let found = false;
|
|
206
|
+
walk(node, (n) => {
|
|
207
|
+
if (found || n.type !== 'CallExpression')
|
|
208
|
+
return;
|
|
209
|
+
const callee = n.callee;
|
|
210
|
+
let name;
|
|
211
|
+
if (callee?.type === 'Identifier') {
|
|
212
|
+
name = callee.name;
|
|
213
|
+
}
|
|
214
|
+
else if (callee?.type === 'MemberExpression' && !callee.computed) {
|
|
215
|
+
const property = callee.property;
|
|
216
|
+
if (property?.type === 'Identifier')
|
|
217
|
+
name = property.name;
|
|
218
|
+
}
|
|
219
|
+
if (name && names.has(name))
|
|
220
|
+
found = true;
|
|
221
|
+
});
|
|
222
|
+
return found;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Names of same-file assertion helpers — function declarations, a const
|
|
226
|
+
* bound to an arrow/function expression, or a class method (`kind:
|
|
227
|
+
* 'method'`, e.g. a Page Object Model's `async expectNodeVisible(name) {
|
|
228
|
+
* await expect(...) }`) — whose own body contains an expect(...)-shaped
|
|
229
|
+
* call, a `.waitFor(...)` call (see containsWaitForCall), or a call to a
|
|
230
|
+
* name already recognized by an earlier round (so e.g.
|
|
231
|
+
* `waitForNotificationAndClose()` calling `this.waitForNotification()`,
|
|
232
|
+
* itself resolved to a direct `.waitFor()` call, is recognized too —
|
|
233
|
+
* verified against a real Page Object Model, n8n's NotificationsPage,
|
|
234
|
+
* where the assertion sits two calls deep). Fed into
|
|
235
|
+
* playwright/expect-expect's own assertFunctionNames option (see
|
|
236
|
+
* eslint-runner.ts) so a test that delegates its assertion to a helper —
|
|
237
|
+
* `audit(page, path)`, `canvas.expectNodeVisible(name)`,
|
|
238
|
+
* `n8n.notifications.waitForNotificationAndClose(text)` — isn't flagged as
|
|
239
|
+
* having no assertions, regardless of what the helper is named or how
|
|
240
|
+
* deep the delegation chain runs *within this one file*. Matching is by
|
|
241
|
+
* name only, any receiver — the same tradeoff eslint-plugin-playwright's
|
|
242
|
+
* own option already makes, so this doesn't introduce a new risk, just
|
|
243
|
+
* extends an already-accepted one from free functions to methods too; a
|
|
244
|
+
* coincidental name collision between an asserting and non-asserting
|
|
245
|
+
* function is the known, accepted failure mode, same as before.
|
|
246
|
+
* Deliberately single-file only: resolving `this.foo()` across file
|
|
247
|
+
* boundaries needs real type resolution, well beyond what a fast,
|
|
248
|
+
* syntactic per-file check should attempt (cross-*file* delegation is
|
|
249
|
+
* handled separately, at the call site in index.ts, by running this same
|
|
250
|
+
* function against each imported dependency file too).
|
|
190
251
|
*/
|
|
191
252
|
export function findLocalAssertionHelperNames(source, file) {
|
|
192
253
|
const ast = parseSource(source, file);
|
|
193
254
|
if (!ast)
|
|
194
255
|
return [];
|
|
195
|
-
const
|
|
256
|
+
const candidates = [];
|
|
196
257
|
walk(ast, (node) => {
|
|
197
258
|
let name;
|
|
198
259
|
let body;
|
|
@@ -212,9 +273,35 @@ export function findLocalAssertionHelperNames(source, file) {
|
|
|
212
273
|
body = init;
|
|
213
274
|
}
|
|
214
275
|
}
|
|
215
|
-
if (
|
|
216
|
-
|
|
276
|
+
else if (node.type === 'MethodDefinition') {
|
|
277
|
+
const key = node.key;
|
|
278
|
+
if (!node.computed && key?.type === 'Identifier' && node.kind === 'method') {
|
|
279
|
+
name = key.name;
|
|
280
|
+
body = node.value;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (name && body)
|
|
284
|
+
candidates.push({ name, body });
|
|
217
285
|
});
|
|
286
|
+
const names = new Set();
|
|
287
|
+
for (const c of candidates) {
|
|
288
|
+
if (containsExpectCall(c.body) || containsWaitForCall(c.body))
|
|
289
|
+
names.add(c.name);
|
|
290
|
+
}
|
|
291
|
+
// Fixed-point: naturally bounded by candidates.length rounds, since each
|
|
292
|
+
// round adds at least one name or the loop stops.
|
|
293
|
+
let added = true;
|
|
294
|
+
while (added) {
|
|
295
|
+
added = false;
|
|
296
|
+
for (const c of candidates) {
|
|
297
|
+
if (names.has(c.name))
|
|
298
|
+
continue;
|
|
299
|
+
if (containsCallToAnyName(c.body, names)) {
|
|
300
|
+
names.add(c.name);
|
|
301
|
+
added = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
218
305
|
return [...names];
|
|
219
306
|
}
|
|
220
307
|
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.12",
|
|
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",
|