test-inventory 0.2.1 → 0.4.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 +28 -0
- package/README.md +9 -1
- package/dist/ast.d.ts +11 -0
- package/dist/ast.js +30 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +59 -12
- package/dist/diagnostics.js +2 -0
- package/dist/extract.d.ts +26 -2
- package/dist/extract.js +204 -48
- package/dist/filter.d.ts +38 -0
- package/dist/filter.js +169 -0
- package/dist/frameworks/bindings.d.ts +37 -6
- package/dist/frameworks/bindings.js +167 -33
- package/dist/frameworks/common.d.ts +10 -1
- package/dist/frameworks/common.js +47 -4
- package/dist/frameworks/playwright.d.ts +6 -1
- package/dist/frameworks/playwright.js +111 -32
- package/dist/frameworks/vitest.d.ts +8 -1
- package/dist/frameworks/vitest.js +245 -85
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/inventory.js +1 -0
- package/dist/json.d.ts +7 -1
- package/dist/json.js +32 -9
- package/dist/model.d.ts +62 -5
- package/dist/records.d.ts +7 -2
- package/dist/records.js +74 -4
- package/dist/scan-file.d.ts +2 -0
- package/dist/scan-file.js +12 -6
- package/dist/scan.js +2 -3
- package/dist/summary.js +1 -0
- package/dist/types.d.ts +109 -1
- package/package.json +2 -2
- package/schema/v1.json +326 -2
|
@@ -9,12 +9,43 @@ export interface ModuleImport {
|
|
|
9
9
|
node: Node;
|
|
10
10
|
}
|
|
11
11
|
export declare function exportName(name: ModuleExportName): string;
|
|
12
|
+
/** What a module is followed for: a test object, or a function that returns one, as in `defineBrokerTest()`. */
|
|
13
|
+
export type Exported = 'test object' | 'function';
|
|
12
14
|
/** Names a module's test objects are known by, used to check what a fixtures file exports. */
|
|
13
15
|
export interface TestObjects {
|
|
14
16
|
isTestObject(expression: Expression): boolean;
|
|
15
17
|
isTestName(name: string): boolean;
|
|
16
|
-
/** Whether
|
|
17
|
-
|
|
18
|
+
/** Whether a function the module declares or imports under `name` returns a test object. */
|
|
19
|
+
returnsTestObject(name: string): boolean;
|
|
20
|
+
/** Whether re-exporting `imported` from `source` exports a test object, or a function that returns one. */
|
|
21
|
+
reexports(source: string, imported: string, exported: Exported): boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The functions a module declares at its top level, by name, to tell whether one returns a test object: every value it
|
|
25
|
+
* returns is one, as `return base.extend({...})` is. An imported function is followed to its module. The function a
|
|
26
|
+
* module exports as its default, named or not, is also known as `default`.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ModuleFunctions {
|
|
29
|
+
private readonly functions;
|
|
30
|
+
/** The functions being read, to end a function that returns what it returns itself. */
|
|
31
|
+
private readonly reading;
|
|
32
|
+
/** Whether each function returns a test object, read once however many calls and functions lead to it. */
|
|
33
|
+
private readonly read;
|
|
34
|
+
/** The values of variables the bindings left unbound, by name, which a call may make test objects. */
|
|
35
|
+
private readonly values;
|
|
36
|
+
/** Whether each of those holds a test object; null while that is being worked out, which ends a loop. */
|
|
37
|
+
private readonly built;
|
|
38
|
+
private readonly imported;
|
|
39
|
+
private readonly follow;
|
|
40
|
+
constructor(program: Program, imported: Map<string, ModuleImport>, follow: FollowImport);
|
|
41
|
+
/** Keeps the variables the bindings didn't bind without following calls, to be worked out when asked. */
|
|
42
|
+
keep(declarators: Iterable<VariableDeclarator>): void;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a variable holds a test object built by a call, as `const test = defineBrokerTest()`. It is worked out
|
|
45
|
+
* only when asked, as following the call reads other modules.
|
|
46
|
+
*/
|
|
47
|
+
holdsTestObject(name: string, isTestObject: (expression: Expression) => boolean): boolean;
|
|
48
|
+
returnsTestObject(name: string, isTestObject: (expression: Expression) => boolean): boolean;
|
|
18
49
|
}
|
|
19
50
|
export declare function topLevelDeclarators(program: Program): VariableDeclarator[];
|
|
20
51
|
/** All variable declarators in the file, at any depth, such as `const { it } = import.meta.vitest` inside an `if`. */
|
|
@@ -25,12 +56,12 @@ export declare function moduleImports(program: Program, isFramework: (source: st
|
|
|
25
56
|
export declare function requiredModule(expression: Expression): string | null;
|
|
26
57
|
/** Applies `bind` to each declarator until no more names are bound, so declaration order doesn't matter. */
|
|
27
58
|
export declare function bindAll(declarators: VariableDeclarator[], bind: (declarator: VariableDeclarator) => boolean): void;
|
|
28
|
-
/** Whether a module exports `name` as a test object. */
|
|
29
|
-
export declare function exportsTestObject(program: Program, name: string, objects: TestObjects): boolean;
|
|
59
|
+
/** Whether a module exports `name` as a test object, or as a function that returns one. */
|
|
60
|
+
export declare function exportsTestObject(program: Program, name: string, objects: TestObjects, exported?: Exported): boolean;
|
|
30
61
|
/** Whether a module imports, re-exports or requires any of the given sources. */
|
|
31
62
|
export declare function importsFrom(program: Program, matches: (source: string) => boolean): boolean;
|
|
32
63
|
/** Whether a name a module imports from another module is a test object there, which follows that module in turn. */
|
|
33
|
-
export type FollowImport = (source: string, imported: string) => boolean;
|
|
64
|
+
export type FollowImport = (source: string, imported: string, exported?: Exported) => boolean;
|
|
34
65
|
/** How a framework recognizes the modules and test objects of a followed file. */
|
|
35
66
|
export interface Framework {
|
|
36
67
|
name: string;
|
|
@@ -46,7 +77,7 @@ export declare function parseModule(module: ModuleSource): ParsedFile;
|
|
|
46
77
|
* nothing to do with the framework, as when a call only looked like a test. Each module is followed once for a name,
|
|
47
78
|
* however many imports lead to it, as fixtures files that build on several others share their bases.
|
|
48
79
|
*/
|
|
49
|
-
export declare function followModule(source: string, name: string, readModule: ModuleReader | null, framework: Framework, path?: readonly string[], followed?: Map<string, Followed>): Followed;
|
|
80
|
+
export declare function followModule(source: string, name: string, readModule: ModuleReader | null, framework: Framework, exported?: Exported, path?: readonly string[], followed?: Map<string, Followed>): Followed;
|
|
50
81
|
interface Followed {
|
|
51
82
|
exported: boolean;
|
|
52
83
|
reason: string | null;
|
|
@@ -1,10 +1,94 @@
|
|
|
1
|
-
import { childNodes, propertyName, unwrap } from "../ast.js";
|
|
1
|
+
import { childNodes, isFunctionNode, propertyName, returnValues, unwrap } from "../ast.js";
|
|
2
2
|
import { parseFile } from "../parse.js";
|
|
3
3
|
import { snippet } from "../source.js";
|
|
4
4
|
import { problem, text } from "./common.js";
|
|
5
5
|
export function exportName(name) {
|
|
6
6
|
return name.type === 'Identifier' ? name.name : name.value;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The functions a module declares at its top level, by name, to tell whether one returns a test object: every value it
|
|
10
|
+
* returns is one, as `return base.extend({...})` is. An imported function is followed to its module. The function a
|
|
11
|
+
* module exports as its default, named or not, is also known as `default`.
|
|
12
|
+
*/
|
|
13
|
+
export class ModuleFunctions {
|
|
14
|
+
functions;
|
|
15
|
+
/** The functions being read, to end a function that returns what it returns itself. */
|
|
16
|
+
reading = new Set();
|
|
17
|
+
/** Whether each function returns a test object, read once however many calls and functions lead to it. */
|
|
18
|
+
read = new Map();
|
|
19
|
+
/** The values of variables the bindings left unbound, by name, which a call may make test objects. */
|
|
20
|
+
values = new Map();
|
|
21
|
+
/** Whether each of those holds a test object; null while that is being worked out, which ends a loop. */
|
|
22
|
+
built = new Map();
|
|
23
|
+
imported;
|
|
24
|
+
follow;
|
|
25
|
+
constructor(program, imported, follow) {
|
|
26
|
+
this.functions = topLevelFunctions(program);
|
|
27
|
+
this.imported = imported;
|
|
28
|
+
this.follow = follow;
|
|
29
|
+
}
|
|
30
|
+
/** Keeps the variables the bindings didn't bind without following calls, to be worked out when asked. */
|
|
31
|
+
keep(declarators) {
|
|
32
|
+
for (const { id, init } of declarators) {
|
|
33
|
+
if (id.type === 'Identifier' && init)
|
|
34
|
+
this.values.set(id.name, [...(this.values.get(id.name) ?? []), init]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether a variable holds a test object built by a call, as `const test = defineBrokerTest()`. It is worked out
|
|
39
|
+
* only when asked, as following the call reads other modules.
|
|
40
|
+
*/
|
|
41
|
+
holdsTestObject(name, isTestObject) {
|
|
42
|
+
const values = this.values.get(name);
|
|
43
|
+
if (!values)
|
|
44
|
+
return false;
|
|
45
|
+
if (!this.built.has(name)) {
|
|
46
|
+
this.built.set(name, null);
|
|
47
|
+
this.built.set(name, values.some(isTestObject));
|
|
48
|
+
}
|
|
49
|
+
return this.built.get(name) === true;
|
|
50
|
+
}
|
|
51
|
+
returnsTestObject(name, isTestObject) {
|
|
52
|
+
const fn = this.functions.get(name);
|
|
53
|
+
if (!fn) {
|
|
54
|
+
const from = this.imported.get(name);
|
|
55
|
+
return from !== undefined && this.follow(from.source, from.imported, 'function');
|
|
56
|
+
}
|
|
57
|
+
if (this.reading.has(fn))
|
|
58
|
+
return false;
|
|
59
|
+
if (!this.read.has(fn)) {
|
|
60
|
+
this.reading.add(fn);
|
|
61
|
+
this.read.set(fn, returnValues(fn)?.every(({ expression }) => isTestObject(expression)) ?? false);
|
|
62
|
+
this.reading.delete(fn);
|
|
63
|
+
}
|
|
64
|
+
return this.read.get(fn) === true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** The functions a module declares at its top level, by name; the one it exports as its default is also `default`. */
|
|
68
|
+
function topLevelFunctions(program) {
|
|
69
|
+
const functions = new Map();
|
|
70
|
+
for (const statement of program.body) {
|
|
71
|
+
if (statement.type === 'ExportDefaultDeclaration') {
|
|
72
|
+
const value = statement.declaration;
|
|
73
|
+
if (value.type === 'FunctionDeclaration' || isFunctionNode(value))
|
|
74
|
+
functions.set('default', value);
|
|
75
|
+
}
|
|
76
|
+
const declaration = statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
|
|
77
|
+
? statement.declaration
|
|
78
|
+
: statement;
|
|
79
|
+
if (declaration?.type === 'FunctionDeclaration' && declaration.id) {
|
|
80
|
+
functions.set(declaration.id.name, declaration);
|
|
81
|
+
}
|
|
82
|
+
else if (declaration?.type === 'VariableDeclaration') {
|
|
83
|
+
for (const { id, init } of declaration.declarations) {
|
|
84
|
+
const value = init ? unwrap(init) : null;
|
|
85
|
+
if (id.type === 'Identifier' && isFunctionNode(value))
|
|
86
|
+
functions.set(id.name, value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return functions;
|
|
91
|
+
}
|
|
8
92
|
export function topLevelDeclarators(program) {
|
|
9
93
|
const declarators = [];
|
|
10
94
|
for (const statement of program.body) {
|
|
@@ -79,22 +163,28 @@ export function bindAll(declarators, bind) {
|
|
|
79
163
|
}
|
|
80
164
|
}
|
|
81
165
|
}
|
|
82
|
-
/** Whether a module exports `name` as a test object. */
|
|
83
|
-
export function exportsTestObject(program, name, objects) {
|
|
166
|
+
/** Whether a module exports `name` as a test object, or as a function that returns one. */
|
|
167
|
+
export function exportsTestObject(program, name, objects, exported = 'test object') {
|
|
84
168
|
const own = ownExport(program, name);
|
|
169
|
+
const local = (localName) => exported === 'function' ? objects.returnsTestObject(localName) : objects.isTestName(localName);
|
|
85
170
|
switch (own?.kind) {
|
|
86
|
-
case 'default':
|
|
87
|
-
|
|
171
|
+
case 'default': {
|
|
172
|
+
if (exported === 'test object')
|
|
173
|
+
return objects.isTestObject(own.value);
|
|
174
|
+
// `export default defineBrokerTest`, or a function written there, which the module's functions know as `default`.
|
|
175
|
+
const value = unwrap(own.value);
|
|
176
|
+
return objects.returnsTestObject(value.type === 'Identifier' ? value.name : 'default');
|
|
177
|
+
}
|
|
88
178
|
case 'variable':
|
|
89
|
-
return
|
|
179
|
+
return local(name);
|
|
90
180
|
case 'function':
|
|
91
|
-
return
|
|
181
|
+
return exported === 'function' && objects.returnsTestObject(name);
|
|
92
182
|
case 'specifier':
|
|
93
|
-
return own.source === null ?
|
|
183
|
+
return own.source === null ? local(own.local) : objects.reexports(own.source, own.local, exported);
|
|
94
184
|
}
|
|
95
185
|
return program.body.some((statement) => {
|
|
96
186
|
const source = starReexport(statement);
|
|
97
|
-
return source !== null && objects.reexports(source, name);
|
|
187
|
+
return source !== null && objects.reexports(source, name, exported);
|
|
98
188
|
});
|
|
99
189
|
}
|
|
100
190
|
/**
|
|
@@ -135,35 +225,44 @@ function starReexport(statement) {
|
|
|
135
225
|
* The module and name a module's export comes from when it re-exports it, as in `export { test } from './base'`, or
|
|
136
226
|
* builds it from something imported, directly or through its own variables, as in `const test = base.extend({...})`.
|
|
137
227
|
*/
|
|
138
|
-
function importOrigin(program, name, isFramework) {
|
|
228
|
+
function importOrigin(program, name, isFramework, exported) {
|
|
139
229
|
const imported = moduleImports(program, isFramework);
|
|
140
230
|
const own = ownExport(program, name);
|
|
141
231
|
if (own === null) {
|
|
142
232
|
for (const statement of program.body) {
|
|
143
233
|
const source = starReexport(statement);
|
|
144
234
|
if (source !== null && !isFramework(source))
|
|
145
|
-
return { source, imported: name, how: 're-exports' };
|
|
235
|
+
return { source, imported: name, how: 're-exports', exported };
|
|
146
236
|
}
|
|
147
237
|
}
|
|
148
238
|
else if (own.kind === 'specifier') {
|
|
149
239
|
// `export { test } from './fixtures'`, or `import { test } from './fixtures'` and then `export { test }`.
|
|
150
240
|
const from = own.source === null ? imported.get(own.local) : { source: own.source, imported: own.local };
|
|
151
|
-
if (from && !isFramework(from.source))
|
|
152
|
-
return { source: from.source, imported: from.imported, how: 're-exports' };
|
|
241
|
+
if (from && !isFramework(from.source)) {
|
|
242
|
+
return { source: from.source, imported: from.imported, how: 're-exports', exported };
|
|
243
|
+
}
|
|
153
244
|
}
|
|
245
|
+
if (exported === 'function')
|
|
246
|
+
return null;
|
|
154
247
|
const values = new Map();
|
|
155
248
|
for (const declarator of topLevelDeclarators(program)) {
|
|
156
249
|
if (declarator.id.type === 'Identifier' && declarator.init)
|
|
157
250
|
values.set(declarator.id.name, declarator.init);
|
|
158
251
|
}
|
|
252
|
+
const functions = topLevelFunctions(program);
|
|
159
253
|
const seen = new Set();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
254
|
+
const from = (local, kind) => {
|
|
255
|
+
const found = imported.get(local);
|
|
256
|
+
return found ? { source: found.source, imported: found.imported, exported: kind } : null;
|
|
257
|
+
};
|
|
258
|
+
// The import a local name is, or is built from: the object `extend` is called on, an imported function that builds
|
|
259
|
+
// it, what a function the module writes returns, the test objects a call such as `mergeTests(a, b)` combines,
|
|
260
|
+
// either side of `?:`, or another local name. Values only passed along, such as a fixtures object in
|
|
261
|
+
// `base.extend(fixtures)`, are not what the test object is built on.
|
|
163
262
|
const builtFrom = (local) => {
|
|
164
263
|
const value = values.get(local);
|
|
165
264
|
if (imported.has(local) || !value || seen.has(local))
|
|
166
|
-
return
|
|
265
|
+
return from(local, 'test object');
|
|
167
266
|
seen.add(local);
|
|
168
267
|
return builtOn(value);
|
|
169
268
|
};
|
|
@@ -180,6 +279,17 @@ function importOrigin(program, name, isFramework) {
|
|
|
180
279
|
const callee = unwrap(expression.callee);
|
|
181
280
|
if (callee.type === 'MemberExpression' && propertyName(callee) === 'extend')
|
|
182
281
|
return builtOn(callee.object);
|
|
282
|
+
if (callee.type === 'Identifier' && imported.has(callee.name))
|
|
283
|
+
return from(callee.name, 'function');
|
|
284
|
+
const fn = callee.type === 'Identifier' && !seen.has(callee.name) ? functions.get(callee.name) : undefined;
|
|
285
|
+
if (fn && callee.type === 'Identifier') {
|
|
286
|
+
seen.add(callee.name);
|
|
287
|
+
for (const { expression: value } of returnValues(fn) ?? []) {
|
|
288
|
+
const found = builtOn(value);
|
|
289
|
+
if (found)
|
|
290
|
+
return found;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
183
293
|
for (const argument of expression.arguments) {
|
|
184
294
|
const found = argument.type === 'SpreadElement' ? null : builtOn(argument);
|
|
185
295
|
if (found)
|
|
@@ -198,9 +308,11 @@ function importOrigin(program, name, isFramework) {
|
|
|
198
308
|
for (const local of [name, ...locals]) {
|
|
199
309
|
const found = values.has(local) ? builtFrom(local) : null;
|
|
200
310
|
if (found)
|
|
201
|
-
return {
|
|
311
|
+
return { ...found, how: 'builds' };
|
|
202
312
|
}
|
|
203
|
-
|
|
313
|
+
// `export default defineBrokerTest()`, built where it is exported.
|
|
314
|
+
const found = own?.kind === 'default' ? builtOn(own.value) : null;
|
|
315
|
+
return found && { ...found, how: 'builds' };
|
|
204
316
|
}
|
|
205
317
|
/** Whether a module imports, re-exports or requires any of the given sources. */
|
|
206
318
|
export function importsFrom(program, matches) {
|
|
@@ -232,39 +344,61 @@ const MAX_DEPTH = 16;
|
|
|
232
344
|
* nothing to do with the framework, as when a call only looked like a test. Each module is followed once for a name,
|
|
233
345
|
* however many imports lead to it, as fixtures files that build on several others share their bases.
|
|
234
346
|
*/
|
|
235
|
-
export function followModule(source, name, readModule, framework, path = [], followed = new Map()) {
|
|
347
|
+
export function followModule(source, name, readModule, framework, exported = 'test object', path = [], followed = new Map()) {
|
|
236
348
|
// Without a reader, as in scanSource, nothing can be read.
|
|
237
349
|
const module = readModule ? readModule(source) : { reason: 'could not be read' };
|
|
238
350
|
if ('reason' in module)
|
|
239
351
|
return { exported: false, reason: module.reason };
|
|
240
|
-
|
|
352
|
+
// A module may be followed again for another name, as a fixtures file that a function it imports builds on.
|
|
353
|
+
const key = `${module.relativeFilePath}\0${exported}\0${name}`;
|
|
354
|
+
if (path.includes(key))
|
|
241
355
|
return { exported: false, reason: 'imports it back, in a cycle' };
|
|
242
356
|
if (path.length >= MAX_DEPTH)
|
|
243
357
|
return { exported: false, reason: `is more than ${MAX_DEPTH} modules deep` };
|
|
244
|
-
const key = `${module.relativeFilePath}\0${name}`;
|
|
245
358
|
let result = followed.get(key);
|
|
246
359
|
if (!result) {
|
|
247
|
-
result = followExports(module, name, framework, [...path,
|
|
360
|
+
result = followExports(module, name, framework, exported, [...path, key], followed);
|
|
248
361
|
followed.set(key, result);
|
|
249
362
|
}
|
|
250
363
|
return result;
|
|
251
364
|
}
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Whether a module exports `name` as a test object; `within` is the chain of modules and names followed to it, itself
|
|
367
|
+
* last.
|
|
368
|
+
*/
|
|
369
|
+
function followExports(module, name, framework, exported, within, followed) {
|
|
370
|
+
const { program, error, source } = parseModule(module);
|
|
255
371
|
if (error)
|
|
256
372
|
return { exported: false, reason: 'could not be parsed' };
|
|
257
|
-
const followFrom = (from, imported) => followModule(from, imported, module.readModule, framework, within, followed);
|
|
258
|
-
const objects = framework.testObjects(program, (from, imported) => followFrom(from, imported).exported);
|
|
259
|
-
if (exportsTestObject(program, name, objects))
|
|
373
|
+
const followFrom = (from, imported, kind) => followModule(from, imported, module.readModule, framework, kind, within, followed);
|
|
374
|
+
const objects = framework.testObjects(program, (from, imported, kind = 'test object') => followFrom(from, imported, kind).exported);
|
|
375
|
+
if (exportsTestObject(program, name, objects, exported))
|
|
260
376
|
return { exported: true, reason: null };
|
|
261
|
-
const unrecognized = `does not export a ${framework.name} test object that could be recognized`;
|
|
262
|
-
const origin = importOrigin(program, name, framework.isSource);
|
|
377
|
+
const unrecognized = (kind) => `does not export ${kind === 'function' ? 'a function that returns a test object' : `a ${framework.name} test object`} that could be recognized`;
|
|
378
|
+
const origin = importOrigin(program, name, framework.isSource, exported);
|
|
263
379
|
if (origin) {
|
|
264
|
-
const end = followFrom(origin.source, origin.imported).reason ?? unrecognized;
|
|
380
|
+
const end = followFrom(origin.source, origin.imported, origin.exported).reason ?? unrecognized(origin.exported);
|
|
265
381
|
return { exported: false, reason: `${origin.how} it from '${origin.source}', which ${end}` };
|
|
266
382
|
}
|
|
267
|
-
|
|
383
|
+
if (importsFrom(program, framework.isSource))
|
|
384
|
+
return { exported: false, reason: unrecognized(exported) };
|
|
385
|
+
// A module that doesn't import the framework may still build the test object with a function, as in
|
|
386
|
+
// `export const test = defineBrokerTest()`; one the scan can't follow is worth an error, not silence.
|
|
387
|
+
const call = exported === 'test object' ? creatingCall(program, name, source) : null;
|
|
388
|
+
return { exported: false, reason: call ? `builds it with ${call}, which is not followed` : null };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The call that creates what the module exports as `name`, a variable or its default export, as `defineBrokerTest()`;
|
|
392
|
+
* null for other values.
|
|
393
|
+
*/
|
|
394
|
+
function creatingCall(program, name, source) {
|
|
395
|
+
const own = ownExport(program, name);
|
|
396
|
+
const local = own?.kind === 'variable' ? name : own?.kind === 'specifier' && own.source === null ? own.local : null;
|
|
397
|
+
const declarator = topLevelDeclarators(program).find((item) => item.id.type === 'Identifier' && item.id.name === local);
|
|
398
|
+
const written = own?.kind === 'default' ? own.value : declarator?.init;
|
|
399
|
+
const init = written ? unwrap(written) : null;
|
|
400
|
+
const value = init?.type === 'AwaitExpression' ? unwrap(init.argument) : init;
|
|
401
|
+
return value?.type === 'CallExpression' ? `${snippet(source.slice(value.callee.start, value.callee.end))}()` : null;
|
|
268
402
|
}
|
|
269
403
|
/** Follows the import of `name` in a test file, with the diagnostic to report when it isn't a test object. */
|
|
270
404
|
export function followImport(name, imported, readModule, file, framework) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Argument, CallExpression, Expression, Node, StringLiteral, TemplateLiteral, VariableDeclarator } from 'oxc-parser';
|
|
2
2
|
import { type FunctionNode } from '../ast.ts';
|
|
3
|
-
import type { Condition, Problem, Returns, Span, Title } from '../model.ts';
|
|
3
|
+
import type { ApiCall, CallContext, Condition, Problem, Returns, Span, Title } from '../model.ts';
|
|
4
4
|
import { type SourceText } from '../source.ts';
|
|
5
5
|
import type { DiagnosticCode } from '../types.ts';
|
|
6
6
|
import { type Resolved, type Values } from '../values.ts';
|
|
@@ -61,6 +61,11 @@ export declare function returnedFunction<T>(returns: Returns, shared: (a: T | nu
|
|
|
61
61
|
* so worth following to see if the name is a custom test object.
|
|
62
62
|
*/
|
|
63
63
|
export declare function looksLikeDeclaration(args: readonly Argument[]): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* A call the runner refuses. Shaped like a declaration outside any test or hook, it is also declared, without what
|
|
66
|
+
* makes it fail, so that its tests, and those of a describe, are listed as not loaded rather than missing.
|
|
67
|
+
*/
|
|
68
|
+
export declare function refused(call: CallExpression, refusal: Problem, context: CallContext, declare: () => ApiCall): ApiCall;
|
|
64
69
|
/**
|
|
65
70
|
* Whether a call shaped like a test, through an import that can't be followed, is worth an error: the name reads like
|
|
66
71
|
* a test function, or a member such as `.describe` follows it. Helpers such as `join('root', file)` and
|
|
@@ -107,6 +112,10 @@ export interface Options {
|
|
|
107
112
|
export declare function readOptions(file: FileContext, scopes: readonly Node[], node: Argument): Options;
|
|
108
113
|
/** Warnings for the parts of an options object that could not be worked out, with what is missing as a result. */
|
|
109
114
|
export declare function unresolvedOptions(file: FileContext, options: Options, result: string): Problem[];
|
|
115
|
+
/** The names a function takes from the object pattern of one of its parameters, in source order. */
|
|
116
|
+
export declare function destructuredNames(fn: FunctionNode | null, index: number | null): string[];
|
|
117
|
+
/** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
|
|
118
|
+
export declare function readCount(file: FileContext, name: string, option: OptionValue, problems: Problem[]): number | null;
|
|
110
119
|
/** A warning for one option whose value could not be worked out. */
|
|
111
120
|
export declare function unresolvedOption(file: FileContext, name: string, node: Node, importedFrom: string | null, result: string): Problem;
|
|
112
121
|
export interface Strings {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isFunctionNode, isStringLiteral, propertyName, unwrap } from "../ast.js";
|
|
1
|
+
import { isFunctionNode, isStringLiteral, propertyKey, propertyName, unwrap } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
3
|
import { isKey } from "../values.js";
|
|
4
4
|
export function readChain(callee) {
|
|
@@ -93,11 +93,22 @@ export function returnedTestObject(file, call) {
|
|
|
93
93
|
* `shared` combines two; null when it may return something else.
|
|
94
94
|
*/
|
|
95
95
|
export function returnedFunction(returns, shared) {
|
|
96
|
-
|
|
96
|
+
const { values } = returns;
|
|
97
|
+
// A function that returns what it returns itself, through others or not, ends the lookup.
|
|
98
|
+
if (!values || reading.has(returns))
|
|
97
99
|
return null;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
if (!read.has(returns)) {
|
|
101
|
+
reading.add(returns);
|
|
102
|
+
const found = values.map(({ expression, scopes }) => returns.adapter.testFunction(expression, scopes, returns.returnsOf));
|
|
103
|
+
reading.delete(returns);
|
|
104
|
+
read.set(returns, found.slice(1).reduce(shared, found[0]));
|
|
105
|
+
}
|
|
106
|
+
return read.get(returns);
|
|
100
107
|
}
|
|
108
|
+
/** The functions whose values are being read, each one returning what the next returns. */
|
|
109
|
+
const reading = new Set();
|
|
110
|
+
/** What each function returns, read once however many calls and functions use it. */
|
|
111
|
+
const read = new WeakMap();
|
|
101
112
|
/** A title written as a string, a template, or text joined to one, as in `'cycles through ' + venue`. */
|
|
102
113
|
function isTitleShaped(node) {
|
|
103
114
|
if (node?.type === 'BinaryExpression' && node.operator === '+') {
|
|
@@ -119,6 +130,17 @@ export function looksLikeDeclaration(args) {
|
|
|
119
130
|
(args.length === 3 && isFunctionNode(args[1]));
|
|
120
131
|
return isTitleShaped(args.at(0)) && args.length >= 2 && args.length <= 3 && body;
|
|
121
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* A call the runner refuses. Shaped like a declaration outside any test or hook, it is also declared, without what
|
|
135
|
+
* makes it fail, so that its tests, and those of a describe, are listed as not loaded rather than missing.
|
|
136
|
+
*/
|
|
137
|
+
export function refused(call, refusal, context, declare) {
|
|
138
|
+
const declared = context.inBody || !looksLikeDeclaration(call.arguments) ? null : declare();
|
|
139
|
+
// A declaration refused for a second reason as well, as for its options, is reported for the first.
|
|
140
|
+
const found = declared?.kind === 'invalid' ? declared.declaration : declared;
|
|
141
|
+
const declaration = found?.kind === 'test' || found?.kind === 'describe' ? found : undefined;
|
|
142
|
+
return { kind: 'invalid', problem: refusal, declaration };
|
|
143
|
+
}
|
|
122
144
|
/**
|
|
123
145
|
* Whether a call shaped like a test, through an import that can't be followed, is worth an error: the name reads like
|
|
124
146
|
* a test function, or a member such as `.describe` follows it. Helpers such as `join('root', file)` and
|
|
@@ -253,6 +275,27 @@ export function unresolvedOptions(file, options, result) {
|
|
|
253
275
|
return problem('unresolved-option', node, `Options object ${source}${origin(importedFrom)} could not be worked out; ${result}.`, source);
|
|
254
276
|
});
|
|
255
277
|
}
|
|
278
|
+
/** The names a function takes from the object pattern of one of its parameters, in source order. */
|
|
279
|
+
export function destructuredNames(fn, index) {
|
|
280
|
+
const parameter = index === null ? undefined : fn?.params[index];
|
|
281
|
+
if (parameter?.type !== 'ObjectPattern')
|
|
282
|
+
return [];
|
|
283
|
+
const names = parameter.properties.map((property) => (property.type === 'Property' ? propertyKey(property) : null));
|
|
284
|
+
return names.filter((name) => name !== null);
|
|
285
|
+
}
|
|
286
|
+
/** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
|
|
287
|
+
export function readCount(file, name, option, problems) {
|
|
288
|
+
const { node, resolved } = option;
|
|
289
|
+
if (!resolved.ok) {
|
|
290
|
+
problems.push(unresolvedOption(file, name, node, resolved.importedFrom, 'it is not applied'));
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (Number.isInteger(resolved.value) && resolved.value >= 0)
|
|
294
|
+
return resolved.value;
|
|
295
|
+
const source = snippet(text(file, node));
|
|
296
|
+
problems.push(problem('unresolved-option', node, `Option ${name}: ${source} is not a whole number; it is not applied.`, source));
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
256
299
|
/** A warning for one option whose value could not be worked out. */
|
|
257
300
|
export function unresolvedOption(file, name, node, importedFrom, result) {
|
|
258
301
|
const source = snippet(text(file, node));
|
|
@@ -28,7 +28,7 @@ export declare class PlaywrightAdapter implements Adapter {
|
|
|
28
28
|
* When the function can't be found, a call named like a test object is read as one on trust, with a warning.
|
|
29
29
|
*/
|
|
30
30
|
private returned;
|
|
31
|
-
testFunction(expression: Expression, scopes: readonly Node[]): string[] | null;
|
|
31
|
+
testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): string[] | null;
|
|
32
32
|
/** Whether a local variable is read as a test object it could be; see `localTestObject`. */
|
|
33
33
|
private assumeLocal;
|
|
34
34
|
private looksLikeTest;
|
|
@@ -49,11 +49,16 @@ export declare class PlaywrightAdapter implements Adapter {
|
|
|
49
49
|
private follow;
|
|
50
50
|
/** Whether an argument is, or works out to, a string, as a title is. Spread arguments are handled before. */
|
|
51
51
|
private isTitle;
|
|
52
|
+
/** A chain Playwright refuses; one that starts like a declaration also declares a plain test or describe. */
|
|
52
53
|
private invalid;
|
|
53
54
|
private testCall;
|
|
55
|
+
/** `test.step(title, body)` in a test body; a title that can't be worked out is kept as source text. */
|
|
56
|
+
private step;
|
|
54
57
|
private modifier;
|
|
55
58
|
private describeCall;
|
|
56
59
|
private configure;
|
|
60
|
+
/** `test.setTimeout(ms)` or `test.slow(condition?, description?)`, which apply where they are called. */
|
|
61
|
+
private runSetting;
|
|
57
62
|
/** `test.info().annotations.push(...)` inside a test body. */
|
|
58
63
|
private annotationsPush;
|
|
59
64
|
private option;
|