@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 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 functions (function declarations, or a const bound to
14
- * an arrow/function expression) whose own body contains an expect(...)-
15
- * shaped call anywhere. Fed into playwright/expect-expect's own
16
- * assertFunctionNames option (see eslint-runner.ts) so a test that
17
- * delegates its assertion to a locally-defined helper — `audit(page,
18
- * path)`, `checkFlashMessageVisibility(page, msg)`, `assertDashboardLoaded`
19
- * — isn't flagged as having no assertions, regardless of what the helper
20
- * happens to be named. expect-expect can only see expect() calls written
21
- * directly in the test body, so without this, delegating to a helper at
22
- * all — an extremely common way to dedupe near-identical specs — reads as
23
- * "no assertions"; verified against several real production files using
24
- * several different naming conventions for the exact same shape.
25
- * Deliberately local-only: a helper imported from another file can't be
26
- * resolved without following module specifiers and parsing that file too,
27
- * well beyond what a fast, per-file syntactic check should attempt.
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
- * Names of same-file functions (function declarations, or a const bound to
176
- * an arrow/function expression) whose own body contains an expect(...)-
177
- * shaped call anywhere. Fed into playwright/expect-expect's own
178
- * assertFunctionNames option (see eslint-runner.ts) so a test that
179
- * delegates its assertion to a locally-defined helper — `audit(page,
180
- * path)`, `checkFlashMessageVisibility(page, msg)`, `assertDashboardLoaded`
181
- * — isn't flagged as having no assertions, regardless of what the helper
182
- * happens to be named. expect-expect can only see expect() calls written
183
- * directly in the test body, so without this, delegating to a helper at
184
- * all — an extremely common way to dedupe near-identical specs — reads as
185
- * "no assertions"; verified against several real production files using
186
- * several different naming conventions for the exact same shape.
187
- * Deliberately local-only: a helper imported from another file can't be
188
- * resolved without following module specifiers and parsing that file too,
189
- * well beyond what a fast, per-file syntactic check should attempt.
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 names = new Set();
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 (name && body && containsExpectCall(body))
216
- names.add(name);
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.11",
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",