@qaguardian/playwright-score 2.0.0 → 2.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/CHANGELOG.md +131 -3
- package/README.md +50 -63
- package/dist/eslint-runner.d.ts +2 -0
- package/dist/eslint-runner.js +22 -4
- package/dist/formatters/markdown.js +8 -0
- package/dist/formatters/text.js +7 -0
- package/dist/import-graph.d.ts +1 -0
- package/dist/import-graph.js +1 -1
- package/dist/index.js +123 -10
- package/dist/metrics.d.ts +17 -4
- package/dist/metrics.js +27 -8
- package/dist/playwright-config.d.ts +114 -0
- package/dist/playwright-config.js +520 -0
- package/dist/rules/base-plugin.js +46 -12
- package/dist/test-aliases.d.ts +18 -0
- package/dist/test-aliases.js +218 -0
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { parseForESLint } from '@typescript-eslint/parser';
|
|
2
|
+
import { resolveRelativeImport } from './import-graph.js';
|
|
3
|
+
function isRelativeSpecifier(value) {
|
|
4
|
+
return typeof value === 'string' && value.startsWith('.');
|
|
5
|
+
}
|
|
6
|
+
function parseFileInfo(source, file) {
|
|
7
|
+
let ast;
|
|
8
|
+
try {
|
|
9
|
+
ast = parseForESLint(source, {
|
|
10
|
+
ecmaVersion: 2022,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
ecmaFeatures: { jsx: /\.(tsx|jsx)$/.test(file) },
|
|
13
|
+
}).ast;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const playwrightTestLocalNames = new Set();
|
|
19
|
+
const extendDeclarations = [];
|
|
20
|
+
const directExportNames = new Set();
|
|
21
|
+
const imports = [];
|
|
22
|
+
const reExports = [];
|
|
23
|
+
function collectExtendFromVariableDeclaration(decl) {
|
|
24
|
+
const declarations = (decl.declarations ?? []);
|
|
25
|
+
for (const d of declarations) {
|
|
26
|
+
if (d.id?.type !== 'Identifier' || !d.id.name || !d.init)
|
|
27
|
+
continue;
|
|
28
|
+
const init = d.init;
|
|
29
|
+
if (init.type === 'CallExpression' &&
|
|
30
|
+
init.callee?.type === 'MemberExpression') {
|
|
31
|
+
const callee = init.callee;
|
|
32
|
+
const property = callee.property;
|
|
33
|
+
const object = callee.object;
|
|
34
|
+
if (!callee.computed &&
|
|
35
|
+
property?.type === 'Identifier' &&
|
|
36
|
+
property.name === 'extend' &&
|
|
37
|
+
object?.type === 'Identifier' &&
|
|
38
|
+
object.name) {
|
|
39
|
+
extendDeclarations.push({ declaredName: d.id.name, baseName: object.name });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Generic subtree walk to find `const X = Y.extend(...)` anywhere (not
|
|
45
|
+
// just top-level) — a fixture module may nest these inside a helper.
|
|
46
|
+
function walk(node) {
|
|
47
|
+
if (!node || typeof node !== 'object')
|
|
48
|
+
return;
|
|
49
|
+
if (Array.isArray(node)) {
|
|
50
|
+
for (const item of node)
|
|
51
|
+
walk(item);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const obj = node;
|
|
55
|
+
if (obj.type === 'VariableDeclaration')
|
|
56
|
+
collectExtendFromVariableDeclaration(obj);
|
|
57
|
+
for (const key in obj) {
|
|
58
|
+
if (key === 'parent')
|
|
59
|
+
continue;
|
|
60
|
+
const value = obj[key];
|
|
61
|
+
if (value && typeof value === 'object')
|
|
62
|
+
walk(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const node of ast.body ?? []) {
|
|
66
|
+
if (node.type === 'ImportDeclaration') {
|
|
67
|
+
const src = node.source?.value;
|
|
68
|
+
if (!isRelativeSpecifier(src)) {
|
|
69
|
+
if (src === '@playwright/test') {
|
|
70
|
+
for (const spec of (node.specifiers ?? [])) {
|
|
71
|
+
if (spec.type !== 'ImportSpecifier')
|
|
72
|
+
continue;
|
|
73
|
+
const importedName = spec.imported?.name ?? spec.imported?.value;
|
|
74
|
+
if (importedName === 'test' && spec.local?.name) {
|
|
75
|
+
playwrightTestLocalNames.add(spec.local.name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const spec of (node.specifiers ?? [])) {
|
|
82
|
+
if (spec.type !== 'ImportSpecifier')
|
|
83
|
+
continue;
|
|
84
|
+
const importedName = spec.imported?.name ?? spec.imported?.value;
|
|
85
|
+
if (importedName && spec.local?.name) {
|
|
86
|
+
imports.push({ localName: spec.local.name, importedName, source: src });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (node.type === 'ExportNamedDeclaration') {
|
|
91
|
+
const declaration = node.declaration;
|
|
92
|
+
const src = node.source?.value;
|
|
93
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
94
|
+
collectExtendFromVariableDeclaration(declaration);
|
|
95
|
+
for (const d of (declaration.declarations ?? [])) {
|
|
96
|
+
if (d.id?.type === 'Identifier' && d.id.name)
|
|
97
|
+
directExportNames.add(d.id.name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
for (const spec of (node.specifiers ?? [])) {
|
|
102
|
+
const localName = spec.local?.name ?? spec.local?.value;
|
|
103
|
+
const exportedName = spec.exported?.name ?? spec.exported?.value;
|
|
104
|
+
if (!localName || !exportedName)
|
|
105
|
+
continue;
|
|
106
|
+
reExports.push({
|
|
107
|
+
exportedName,
|
|
108
|
+
localName,
|
|
109
|
+
source: isRelativeSpecifier(src) ? src : undefined,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Fixtures modules commonly declare the extend() call inside a nested
|
|
116
|
+
// scope too (e.g. wrapped in a `(() => {...})()` IIFE is rare, but
|
|
117
|
+
// multiple top-level `const`s are common) — the top-level pass above
|
|
118
|
+
// already covers the overwhelming majority; this catches the rest
|
|
119
|
+
// without a second parse.
|
|
120
|
+
walk(ast.body ?? []);
|
|
121
|
+
return {
|
|
122
|
+
playwrightTestLocalNames,
|
|
123
|
+
extendDeclarations,
|
|
124
|
+
directExportNames,
|
|
125
|
+
imports,
|
|
126
|
+
reExports,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Detects identifiers bound (directly, or via a local import/re-export
|
|
131
|
+
* chain of any depth) to `<base>.extend(...)` where `<base>` is `test`
|
|
132
|
+
* imported from `@playwright/test`, or is itself such an extended test.
|
|
133
|
+
* Returns the flat set of alias name strings (e.g. `loggedTest`,
|
|
134
|
+
* `testWithAssets`) usable anywhere they're called across the scored
|
|
135
|
+
* files — callers feed this into `eslint-runner.ts`'s
|
|
136
|
+
* `settings.playwright.globalAliases.test` and into `metrics.ts`'s
|
|
137
|
+
* `getTestSpans`/`countTests` and `base-plugin.ts`'s test-name checks.
|
|
138
|
+
*
|
|
139
|
+
* `sources`/`importedFiles` are the same maps `index.ts` already builds
|
|
140
|
+
* (scored spec files, and their transitively-collected local relative
|
|
141
|
+
* imports — see `collectLocallyImportedFiles`); this function does not
|
|
142
|
+
* walk the filesystem itself beyond resolving relative specifiers already
|
|
143
|
+
* present in those files' own import/export statements, and never
|
|
144
|
+
* escapes `boundaryDir` (same safety rule as `import-graph.ts`).
|
|
145
|
+
*/
|
|
146
|
+
export function detectExtendedTestAliases(sources, importedFiles, boundaryDir) {
|
|
147
|
+
const allFiles = new Map([...sources, ...importedFiles]);
|
|
148
|
+
const parsed = new Map();
|
|
149
|
+
for (const [file, source] of allFiles) {
|
|
150
|
+
const info = parseFileInfo(source, file);
|
|
151
|
+
if (info)
|
|
152
|
+
parsed.set(file, info);
|
|
153
|
+
}
|
|
154
|
+
const localAlias = new Map();
|
|
155
|
+
const exportedAlias = new Map();
|
|
156
|
+
for (const file of parsed.keys()) {
|
|
157
|
+
localAlias.set(file, new Set());
|
|
158
|
+
exportedAlias.set(file, new Set());
|
|
159
|
+
}
|
|
160
|
+
const resolve = (fromFile, specifier) => resolveRelativeImport(fromFile, specifier, boundaryDir);
|
|
161
|
+
// Bounded fixed point: each round can only add names (monotonic), and
|
|
162
|
+
// there are at most parsed.size * (max names per file) additions total —
|
|
163
|
+
// capping rounds at file count + a small constant is generous relative
|
|
164
|
+
// to any real import-chain depth (mirrors import-graph.ts's own
|
|
165
|
+
// MAX_DEPTH-style bound).
|
|
166
|
+
const maxRounds = parsed.size + 5;
|
|
167
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
168
|
+
let changed = false;
|
|
169
|
+
for (const [file, info] of parsed) {
|
|
170
|
+
const known = localAlias.get(file);
|
|
171
|
+
const exported = exportedAlias.get(file);
|
|
172
|
+
for (const { declaredName, baseName } of info.extendDeclarations) {
|
|
173
|
+
if (known.has(declaredName))
|
|
174
|
+
continue;
|
|
175
|
+
if (info.playwrightTestLocalNames.has(baseName) || known.has(baseName)) {
|
|
176
|
+
known.add(declaredName);
|
|
177
|
+
changed = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const { localName, importedName, source } of info.imports) {
|
|
181
|
+
if (known.has(localName))
|
|
182
|
+
continue;
|
|
183
|
+
const resolved = resolve(file, source);
|
|
184
|
+
if (!resolved)
|
|
185
|
+
continue;
|
|
186
|
+
if (exportedAlias.get(resolved)?.has(importedName)) {
|
|
187
|
+
known.add(localName);
|
|
188
|
+
changed = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const name of known) {
|
|
192
|
+
if (info.directExportNames.has(name) && !exported.has(name)) {
|
|
193
|
+
exported.add(name);
|
|
194
|
+
changed = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const { exportedName, localName, source } of info.reExports) {
|
|
198
|
+
if (exported.has(exportedName))
|
|
199
|
+
continue;
|
|
200
|
+
const isAlias = source
|
|
201
|
+
? !!resolve(file, source) && exportedAlias.get(resolve(file, source))?.has(localName)
|
|
202
|
+
: known.has(localName);
|
|
203
|
+
if (isAlias) {
|
|
204
|
+
exported.add(exportedName);
|
|
205
|
+
changed = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!changed)
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
const result = new Set();
|
|
213
|
+
for (const names of localAlias.values()) {
|
|
214
|
+
for (const name of names)
|
|
215
|
+
result.add(name);
|
|
216
|
+
}
|
|
217
|
+
return [...result].sort();
|
|
218
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -74,6 +74,14 @@ export interface ScoreResult {
|
|
|
74
74
|
* if they'd otherwise be skipped.
|
|
75
75
|
*/
|
|
76
76
|
skippedFiles?: string[];
|
|
77
|
+
/**
|
|
78
|
+
* A playwright.config.* was found at or above a scanned directory but
|
|
79
|
+
* could not be read statically (a dynamic value, e.g. `process.env`, a
|
|
80
|
+
* spread, or an unsupported export shape) — discovery fell back to
|
|
81
|
+
* today's filename-suffix behavior for that directory instead of acting
|
|
82
|
+
* on a partial or guessed reading. See playwright-config.ts.
|
|
83
|
+
*/
|
|
84
|
+
configWarnings?: string[];
|
|
77
85
|
}
|
|
78
86
|
export interface ScoreOptions {
|
|
79
87
|
paths: string[];
|
package/package.json
CHANGED