@qaguardian/playwright-score 2.2.0 → 2.3.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/CHANGELOG.md +86 -0
- package/METHODOLOGY.md +41 -0
- package/README.md +1 -1
- package/dist/cli.js +61 -5
- package/dist/confidence.d.ts +30 -0
- package/dist/confidence.js +67 -0
- package/dist/eslint-runner.js +18 -0
- package/dist/formatters/json.js +4 -1
- package/dist/formatters/markdown.d.ts +2 -0
- package/dist/formatters/markdown.js +15 -4
- package/dist/formatters/report-css.d.ts +2 -0
- package/dist/formatters/report-css.js +68 -0
- package/dist/formatters/report.d.ts +20 -0
- package/dist/formatters/report.js +279 -0
- package/dist/formatters/text.d.ts +6 -1
- package/dist/formatters/text.js +73 -28
- package/dist/index.js +17 -6
- package/dist/profiles.js +12 -0
- package/dist/report-model.d.ts +52 -0
- package/dist/report-model.js +162 -0
- package/dist/report-source.d.ts +11 -0
- package/dist/report-source.js +96 -0
- package/dist/rule-copy.d.ts +14 -0
- package/dist/rule-copy.js +215 -0
- package/dist/rules/ast-util.d.ts +60 -0
- package/dist/rules/ast-util.js +140 -0
- package/dist/rules/base-plugin.d.ts +19 -5
- package/dist/rules/base-plugin.js +75 -7
- package/dist/rules/no-brittle-selector.d.ts +3 -0
- package/dist/rules/no-brittle-selector.js +150 -0
- package/dist/rules/no-excessive-timeout.d.ts +2 -0
- package/dist/rules/no-excessive-timeout.js +87 -0
- package/dist/rules/no-fixme-test.d.ts +14 -0
- package/dist/rules/no-fixme-test.js +53 -0
- package/dist/rules/no-self-fulfilling-fallback.d.ts +29 -0
- package/dist/rules/no-self-fulfilling-fallback.js +221 -0
- package/dist/rules/no-shared-mutable-state.d.ts +4 -0
- package/dist/rules/no-shared-mutable-state.js +186 -0
- package/dist/rules/no-swallowed-assertion.d.ts +13 -0
- package/dist/rules/no-swallowed-assertion.js +113 -0
- package/dist/rules/no-unawaited-action.d.ts +2 -0
- package/dist/rules/no-unawaited-action.js +77 -0
- package/dist/rules/no-unawaited-expect-value.d.ts +20 -0
- package/dist/rules/no-unawaited-expect-value.js +46 -0
- package/dist/rules/no-weak-assertion.d.ts +19 -0
- package/dist/rules/no-weak-assertion.js +82 -0
- package/dist/rules/rule-util.d.ts +56 -0
- package/dist/rules/rule-util.js +253 -0
- package/dist/score-engine.d.ts +5 -0
- package/dist/score-engine.js +9 -9
- package/dist/suite-name.d.ts +11 -0
- package/dist/suite-name.js +36 -0
- package/dist/test-aliases.js +144 -3
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/test-aliases.js
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
import { parseForESLint } from '@typescript-eslint/parser';
|
|
2
2
|
import { resolveRelativeImport } from './import-graph.js';
|
|
3
|
+
function classifyReturn(expr) {
|
|
4
|
+
if (!expr)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (expr.type === 'Identifier' && typeof expr.name === 'string') {
|
|
7
|
+
return { kind: 'ident', name: expr.name };
|
|
8
|
+
}
|
|
9
|
+
if (expr.type !== 'CallExpression')
|
|
10
|
+
return undefined;
|
|
11
|
+
const callee = expr.callee;
|
|
12
|
+
if (callee?.type === 'Identifier' && typeof callee.name === 'string') {
|
|
13
|
+
return { kind: 'call', calleeName: callee.name };
|
|
14
|
+
}
|
|
15
|
+
if (callee?.type === 'MemberExpression' && !callee.computed) {
|
|
16
|
+
const prop = callee.property;
|
|
17
|
+
const obj = callee.object;
|
|
18
|
+
if (prop?.type === 'Identifier' && prop.name === 'extend' && obj?.type === 'Identifier' && obj.name) {
|
|
19
|
+
return { kind: 'extend', baseName: obj.name };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/** `return <expr>` statements of one function body, not descending into nested functions. */
|
|
25
|
+
function collectFunctionReturns(fn) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const body = fn.body;
|
|
28
|
+
if (!body)
|
|
29
|
+
return out;
|
|
30
|
+
if (body.type !== 'BlockStatement') {
|
|
31
|
+
const r = classifyReturn(body);
|
|
32
|
+
if (r)
|
|
33
|
+
out.push(r);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
const visit = (node) => {
|
|
37
|
+
if (!node || typeof node !== 'object')
|
|
38
|
+
return;
|
|
39
|
+
if (Array.isArray(node)) {
|
|
40
|
+
for (const n of node)
|
|
41
|
+
visit(n);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const obj = node;
|
|
45
|
+
if (obj.type === 'FunctionDeclaration' ||
|
|
46
|
+
obj.type === 'FunctionExpression' ||
|
|
47
|
+
obj.type === 'ArrowFunctionExpression') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (obj.type === 'ReturnStatement') {
|
|
51
|
+
const r = classifyReturn(obj.argument);
|
|
52
|
+
if (r)
|
|
53
|
+
out.push(r);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const key in obj) {
|
|
57
|
+
if (key === 'parent')
|
|
58
|
+
continue;
|
|
59
|
+
const v = obj[key];
|
|
60
|
+
if (v && typeof v === 'object')
|
|
61
|
+
visit(v);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
visit(body.body);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
3
67
|
function isRelativeSpecifier(value) {
|
|
4
68
|
return typeof value === 'string' && value.startsWith('.');
|
|
5
69
|
}
|
|
@@ -20,12 +84,38 @@ function parseFileInfo(source, file) {
|
|
|
20
84
|
const directExportNames = new Set();
|
|
21
85
|
const imports = [];
|
|
22
86
|
const reExports = [];
|
|
87
|
+
const functions = [];
|
|
88
|
+
const callDeclarations = [];
|
|
89
|
+
function collectFunction(node, exported) {
|
|
90
|
+
if (node.type === 'FunctionDeclaration') {
|
|
91
|
+
const name = node.id?.name;
|
|
92
|
+
if (name)
|
|
93
|
+
functions.push({ name, returns: collectFunctionReturns(node), exported });
|
|
94
|
+
}
|
|
95
|
+
else if (node.type === 'VariableDeclaration') {
|
|
96
|
+
for (const d of (node.declarations ?? [])) {
|
|
97
|
+
const init = d.init;
|
|
98
|
+
if (d.id?.type === 'Identifier' &&
|
|
99
|
+
d.id.name &&
|
|
100
|
+
init &&
|
|
101
|
+
(init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
|
|
102
|
+
functions.push({ name: d.id.name, returns: collectFunctionReturns(init), exported });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
23
107
|
function collectExtendFromVariableDeclaration(decl) {
|
|
24
108
|
const declarations = (decl.declarations ?? []);
|
|
25
109
|
for (const d of declarations) {
|
|
26
110
|
if (d.id?.type !== 'Identifier' || !d.id.name || !d.init)
|
|
27
111
|
continue;
|
|
28
112
|
const init = d.init;
|
|
113
|
+
if (init.type === 'CallExpression' &&
|
|
114
|
+
init.callee?.type === 'Identifier') {
|
|
115
|
+
const calleeName = (init.callee).name;
|
|
116
|
+
if (calleeName)
|
|
117
|
+
callDeclarations.push({ declaredName: d.id.name, calleeName });
|
|
118
|
+
}
|
|
29
119
|
if (init.type === 'CallExpression' &&
|
|
30
120
|
init.callee?.type === 'MemberExpression') {
|
|
31
121
|
const callee = init.callee;
|
|
@@ -63,6 +153,9 @@ function parseFileInfo(source, file) {
|
|
|
63
153
|
}
|
|
64
154
|
}
|
|
65
155
|
for (const node of ast.body ?? []) {
|
|
156
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'VariableDeclaration') {
|
|
157
|
+
collectFunction(node, false);
|
|
158
|
+
}
|
|
66
159
|
if (node.type === 'ImportDeclaration') {
|
|
67
160
|
const src = node.source?.value;
|
|
68
161
|
if (!isRelativeSpecifier(src)) {
|
|
@@ -90,6 +183,8 @@ function parseFileInfo(source, file) {
|
|
|
90
183
|
else if (node.type === 'ExportNamedDeclaration') {
|
|
91
184
|
const declaration = node.declaration;
|
|
92
185
|
const src = node.source?.value;
|
|
186
|
+
if (declaration)
|
|
187
|
+
collectFunction(declaration, true);
|
|
93
188
|
if (declaration?.type === 'VariableDeclaration') {
|
|
94
189
|
collectExtendFromVariableDeclaration(declaration);
|
|
95
190
|
for (const d of (declaration.declarations ?? [])) {
|
|
@@ -124,6 +219,8 @@ function parseFileInfo(source, file) {
|
|
|
124
219
|
directExportNames,
|
|
125
220
|
imports,
|
|
126
221
|
reExports,
|
|
222
|
+
functions,
|
|
223
|
+
callDeclarations,
|
|
127
224
|
};
|
|
128
225
|
}
|
|
129
226
|
/**
|
|
@@ -153,9 +250,15 @@ export function detectExtendedTestAliases(sources, importedFiles, boundaryDir) {
|
|
|
153
250
|
}
|
|
154
251
|
const localAlias = new Map();
|
|
155
252
|
const exportedAlias = new Map();
|
|
253
|
+
// Test-factory functions (QAG-287): `function make(cfg) { return test.extend(...) }`.
|
|
254
|
+
// `const t = make(cfg)` then makes `t` a test alias.
|
|
255
|
+
const localFactory = new Map();
|
|
256
|
+
const exportedFactory = new Map();
|
|
156
257
|
for (const file of parsed.keys()) {
|
|
157
258
|
localAlias.set(file, new Set());
|
|
158
259
|
exportedAlias.set(file, new Set());
|
|
260
|
+
localFactory.set(file, new Set());
|
|
261
|
+
exportedFactory.set(file, new Set());
|
|
159
262
|
}
|
|
160
263
|
const resolve = (fromFile, specifier) => resolveRelativeImport(fromFile, specifier, boundaryDir);
|
|
161
264
|
// Bounded fixed point: each round can only add names (monotonic), and
|
|
@@ -177,16 +280,54 @@ export function detectExtendedTestAliases(sources, importedFiles, boundaryDir) {
|
|
|
177
280
|
changed = true;
|
|
178
281
|
}
|
|
179
282
|
}
|
|
283
|
+
const factories = localFactory.get(file);
|
|
284
|
+
const exportedFactories = exportedFactory.get(file);
|
|
180
285
|
for (const { localName, importedName, source } of info.imports) {
|
|
181
|
-
if (known.has(localName))
|
|
182
|
-
continue;
|
|
183
286
|
const resolved = resolve(file, source);
|
|
184
287
|
if (!resolved)
|
|
185
288
|
continue;
|
|
186
|
-
if (exportedAlias.get(resolved)?.has(importedName)) {
|
|
289
|
+
if (!known.has(localName) && exportedAlias.get(resolved)?.has(importedName)) {
|
|
187
290
|
known.add(localName);
|
|
188
291
|
changed = true;
|
|
189
292
|
}
|
|
293
|
+
if (!factories.has(localName) && exportedFactory.get(resolved)?.has(importedName)) {
|
|
294
|
+
factories.add(localName);
|
|
295
|
+
changed = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const fn of info.functions) {
|
|
299
|
+
if (!factories.has(fn.name)) {
|
|
300
|
+
const returnsAlias = fn.returns.some((r) => r.kind === 'extend'
|
|
301
|
+
? info.playwrightTestLocalNames.has(r.baseName) || known.has(r.baseName)
|
|
302
|
+
: r.kind === 'ident'
|
|
303
|
+
? known.has(r.name)
|
|
304
|
+
: factories.has(r.calleeName));
|
|
305
|
+
if (returnsAlias) {
|
|
306
|
+
factories.add(fn.name);
|
|
307
|
+
changed = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (fn.exported && factories.has(fn.name) && !exportedFactories.has(fn.name)) {
|
|
311
|
+
exportedFactories.add(fn.name);
|
|
312
|
+
changed = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const { declaredName, calleeName } of info.callDeclarations) {
|
|
316
|
+
if (known.has(declaredName) || !factories.has(calleeName))
|
|
317
|
+
continue;
|
|
318
|
+
known.add(declaredName);
|
|
319
|
+
changed = true;
|
|
320
|
+
}
|
|
321
|
+
for (const { exportedName, localName, source } of info.reExports) {
|
|
322
|
+
if (exportedFactories.has(exportedName))
|
|
323
|
+
continue;
|
|
324
|
+
const isFactory = source
|
|
325
|
+
? !!resolve(file, source) && exportedFactory.get(resolve(file, source))?.has(localName)
|
|
326
|
+
: factories.has(localName);
|
|
327
|
+
if (isFactory) {
|
|
328
|
+
exportedFactories.add(exportedName);
|
|
329
|
+
changed = true;
|
|
330
|
+
}
|
|
190
331
|
}
|
|
191
332
|
for (const name of known) {
|
|
192
333
|
if (info.directExportNames.has(name) && !exported.has(name)) {
|
package/dist/types.d.ts
CHANGED
|
@@ -69,6 +69,12 @@ export interface ScoreResult {
|
|
|
69
69
|
summary: ScoreSummary;
|
|
70
70
|
dimensions: ScoreDimensions;
|
|
71
71
|
findings: Finding[];
|
|
72
|
+
/**
|
|
73
|
+
* Absolute directory that every `Finding.file` is relative to (the scored
|
|
74
|
+
* files' common ancestor). Lets a consumer locate source for excerpts. Local
|
|
75
|
+
* to the machine that ran the scan: never put it in a shareable artifact.
|
|
76
|
+
*/
|
|
77
|
+
baseDir?: string;
|
|
72
78
|
/**
|
|
73
79
|
* Files matched by a directory/glob input but skipped because they show
|
|
74
80
|
* positive evidence of being a non-Playwright test (an import from
|
|
@@ -86,6 +92,14 @@ export interface ScoreResult {
|
|
|
86
92
|
* on a partial or guessed reading. See playwright-config.ts.
|
|
87
93
|
*/
|
|
88
94
|
configWarnings?: string[];
|
|
95
|
+
/**
|
|
96
|
+
* Rules that threw while scoring this run (contained, so the run finished
|
|
97
|
+
* but their findings may be missing). Present only when non-empty.
|
|
98
|
+
*/
|
|
99
|
+
ruleCrashes?: Array<{
|
|
100
|
+
rule: string;
|
|
101
|
+
count: number;
|
|
102
|
+
}>;
|
|
89
103
|
}
|
|
90
104
|
export interface ScoreOptions {
|
|
91
105
|
paths: string[];
|
package/package.json
CHANGED