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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
|
-
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, requiredModule, } from "./bindings.js";
|
|
4
|
-
import { atLeast, findFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, readCondition, readOptions, readReason, readStrings, readTitle, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
|
|
3
|
+
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
|
|
4
|
+
import { atLeast, findFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
|
|
5
5
|
const MISMATCHED_SOURCES = new Set(['vitest', '@jest/globals']);
|
|
6
6
|
// Maps rather than object literals, so names such as `constructor` don't match inherited properties.
|
|
7
7
|
const DESCRIBES = new Map([
|
|
@@ -24,6 +24,10 @@ const STATES = new Map([
|
|
|
24
24
|
]);
|
|
25
25
|
/** Members of `test` that are known but don't declare or modify tests. */
|
|
26
26
|
const OTHER_MEMBERS = new Set(['use', 'slow', 'setTimeout', 'extend']);
|
|
27
|
+
/** Members that set how the tests run where they are called. */
|
|
28
|
+
const RUN_MEMBERS = new Set(['slow', 'setTimeout']);
|
|
29
|
+
/** First members of chains that never declare a test, even when Playwright refuses the rest of the chain. */
|
|
30
|
+
const NOT_DECLARING = new Set(['step', 'abort', ...HOOKS, ...OTHER_MEMBERS]);
|
|
27
31
|
/** First members that show a name imported from a local file is used as Playwright's `test`. */
|
|
28
32
|
const TEST_MEMBERS = new Set(['describe', 'only', 'skip', 'fixme', 'fail', 'step', ...HOOKS]);
|
|
29
33
|
/** The members of `test` a value chosen at run time holds either way: those both have, if both are the same kind. */
|
|
@@ -57,9 +61,12 @@ class PlaywrightBindings {
|
|
|
57
61
|
/** Names imported from other modules, which may be test objects there. */
|
|
58
62
|
imported;
|
|
59
63
|
follow;
|
|
64
|
+
/** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
|
|
65
|
+
functions;
|
|
60
66
|
constructor(program, follow) {
|
|
61
67
|
this.imported = moduleImports(program, (source) => isPlaywrightSource(source) || MISMATCHED_SOURCES.has(source));
|
|
62
68
|
this.follow = follow;
|
|
69
|
+
this.functions = new ModuleFunctions(program, this.imported, follow);
|
|
63
70
|
for (const statement of program.body) {
|
|
64
71
|
if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
|
|
65
72
|
continue;
|
|
@@ -85,41 +92,59 @@ class PlaywrightBindings {
|
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
|
-
|
|
95
|
+
const declarators = allDeclarators(program);
|
|
96
|
+
bindAll(declarators, (declarator) => {
|
|
89
97
|
const bound = this.bind(declarator);
|
|
90
98
|
if (bound)
|
|
91
99
|
this.declarations.add(declarator);
|
|
92
100
|
return bound;
|
|
93
101
|
});
|
|
102
|
+
this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
|
|
94
103
|
}
|
|
95
104
|
isTestName(name) {
|
|
96
|
-
return this.testNames.has(name) || this.importsTestObject(name);
|
|
105
|
+
return this.testNames.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
|
|
97
106
|
}
|
|
98
|
-
|
|
107
|
+
/**
|
|
108
|
+
* `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
|
|
109
|
+
* a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
|
|
110
|
+
* follows in their scopes.
|
|
111
|
+
*/
|
|
112
|
+
isTestObject(expression, followsCalls = true) {
|
|
99
113
|
const node = unwrap(expression);
|
|
100
114
|
switch (node.type) {
|
|
101
115
|
case 'Identifier':
|
|
102
|
-
return this.testNames.has(node.name) ||
|
|
116
|
+
return (this.testNames.has(node.name) ||
|
|
117
|
+
this.importsTestObject(node.name) ||
|
|
118
|
+
(followsCalls && this.holdsTestObject(node.name)));
|
|
103
119
|
case 'MemberExpression':
|
|
104
120
|
return propertyName(node) === 'test' && this.isModule(node.object);
|
|
105
121
|
case 'ConditionalExpression':
|
|
106
122
|
// `process.env.CI ? ciTest : test` is a test object either way.
|
|
107
|
-
return this.isTestObject(node.consequent) || this.isTestObject(node.alternate);
|
|
123
|
+
return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
|
|
108
124
|
case 'CallExpression': {
|
|
109
125
|
const callee = unwrap(node.callee);
|
|
110
126
|
if (callee.type === 'MemberExpression' && propertyName(callee) === 'extend') {
|
|
111
|
-
return this.isTestObject(callee.object);
|
|
127
|
+
return this.isTestObject(callee.object, followsCalls);
|
|
112
128
|
}
|
|
129
|
+
// `mergeTests(a, b)`, or `defineBrokerTest()`, a function that returns a test object.
|
|
113
130
|
return callee.type === 'Identifier'
|
|
114
|
-
? this.mergeTestsNames.has(callee.name)
|
|
131
|
+
? this.mergeTestsNames.has(callee.name) || (followsCalls && this.returnsTestObject(callee.name))
|
|
115
132
|
: callee.type === 'MemberExpression' && propertyName(callee) === 'mergeTests' && this.isModule(callee.object);
|
|
116
133
|
}
|
|
117
134
|
default:
|
|
118
135
|
return false;
|
|
119
136
|
}
|
|
120
137
|
}
|
|
121
|
-
|
|
122
|
-
return
|
|
138
|
+
returnsTestObject(name) {
|
|
139
|
+
return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
|
|
140
|
+
}
|
|
141
|
+
holdsTestObject(name) {
|
|
142
|
+
return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
|
|
143
|
+
}
|
|
144
|
+
reexports(source, imported, exported) {
|
|
145
|
+
if (!isPlaywrightSource(source))
|
|
146
|
+
return this.follow(source, imported, exported);
|
|
147
|
+
return exported === 'test object' && (imported === 'test' || imported === 'default');
|
|
123
148
|
}
|
|
124
149
|
/** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
|
|
125
150
|
importsTestObject(name) {
|
|
@@ -148,7 +173,7 @@ class PlaywrightBindings {
|
|
|
148
173
|
}
|
|
149
174
|
return true;
|
|
150
175
|
}
|
|
151
|
-
if (this.isTestObject(init)) {
|
|
176
|
+
if (this.isTestObject(init, false)) {
|
|
152
177
|
this.testNames.add(id.name);
|
|
153
178
|
return true;
|
|
154
179
|
}
|
|
@@ -192,10 +217,11 @@ export class PlaywrightAdapter {
|
|
|
192
217
|
// A test object the file builds from an imported one, as in `const test = base.extend({...})`, is followed
|
|
193
218
|
// quietly: a call through the import itself is what reports an import that can't be followed.
|
|
194
219
|
const followed = new Map();
|
|
195
|
-
this.bindings = new PlaywrightBindings(program, (source, imported) => {
|
|
196
|
-
const key = `${source}\0${imported}`;
|
|
197
|
-
if (!followed.has(key))
|
|
198
|
-
followed.set(key, followModule(source, imported, readModule, PLAYWRIGHT).exported);
|
|
220
|
+
this.bindings = new PlaywrightBindings(program, (source, imported, exported = 'test object') => {
|
|
221
|
+
const key = `${source}\0${exported}\0${imported}`;
|
|
222
|
+
if (!followed.has(key)) {
|
|
223
|
+
followed.set(key, followModule(source, imported, readModule, PLAYWRIGHT, exported).exported);
|
|
224
|
+
}
|
|
199
225
|
return followed.get(key) === true;
|
|
200
226
|
});
|
|
201
227
|
this.problems = [...this.bindings.mismatches];
|
|
@@ -210,14 +236,18 @@ export class PlaywrightAdapter {
|
|
|
210
236
|
const key = names.join('.');
|
|
211
237
|
const addedIn = ADDED_IN.get(key);
|
|
212
238
|
if (addedIn && !atLeast(this.file.version, ...addedIn)) {
|
|
213
|
-
return this.invalid(call, `needs Playwright ${addedIn.join('.')} or newer
|
|
239
|
+
return this.invalid(call, names, `needs Playwright ${addedIn.join('.')} or newer`, context);
|
|
214
240
|
}
|
|
215
241
|
if (names[0] === 'info')
|
|
216
242
|
return this.annotationsPush(call, links, context);
|
|
243
|
+
if ((key === 'step' || key === 'step.skip') && context.inTest && call.arguments.length > 0) {
|
|
244
|
+
return this.step(call, key === 'step.skip', context);
|
|
245
|
+
}
|
|
217
246
|
if (names[0] === 'expect' || names[0] === 'step' || key === 'abort')
|
|
218
247
|
return { kind: 'other' };
|
|
219
|
-
if (links.some((link) => link.args ?? link.template))
|
|
220
|
-
return this.invalid(call, "is not part of Playwright's API");
|
|
248
|
+
if (links.some((link) => link.args ?? link.template)) {
|
|
249
|
+
return this.invalid(call, names, "is not part of Playwright's API", context);
|
|
250
|
+
}
|
|
221
251
|
if (key === '' || key === 'only' || key === 'skip' || key === 'fixme' || key === 'fail' || key === 'fail.only') {
|
|
222
252
|
return this.testCall(call, key, context);
|
|
223
253
|
}
|
|
@@ -226,11 +256,15 @@ export class PlaywrightAdapter {
|
|
|
226
256
|
return this.describeCall(call, describe, context);
|
|
227
257
|
if (key === 'describe.configure')
|
|
228
258
|
return this.configure(call, context);
|
|
229
|
-
if (HOOKS.has(key))
|
|
230
|
-
|
|
259
|
+
if (HOOKS.has(key)) {
|
|
260
|
+
const fn = findFunction(call.arguments);
|
|
261
|
+
return { kind: 'hook', name: key, fn, testContext: null, fixtures: destructuredNames(fn, 0) };
|
|
262
|
+
}
|
|
263
|
+
if (RUN_MEMBERS.has(key))
|
|
264
|
+
return this.runSetting(call, key, context);
|
|
231
265
|
if (OTHER_MEMBERS.has(key))
|
|
232
266
|
return { kind: 'other' };
|
|
233
|
-
return this.invalid(call, "is not part of Playwright's API");
|
|
267
|
+
return this.invalid(call, names, "is not part of Playwright's API", context);
|
|
234
268
|
}
|
|
235
269
|
/**
|
|
236
270
|
* The links after the `test` object, or null when the chain doesn't start from Playwright's `test`. An import that
|
|
@@ -283,7 +317,7 @@ export class PlaywrightAdapter {
|
|
|
283
317
|
*/
|
|
284
318
|
returned(call, links, args, lookup) {
|
|
285
319
|
const callee = unwrap(call.callee);
|
|
286
|
-
if (callee.type !== 'Identifier'
|
|
320
|
+
if (callee.type !== 'Identifier')
|
|
287
321
|
return null;
|
|
288
322
|
const returns = lookup.returnsOf(callee.name, lookup.scopes);
|
|
289
323
|
if (returns)
|
|
@@ -296,8 +330,8 @@ export class PlaywrightAdapter {
|
|
|
296
330
|
}
|
|
297
331
|
return [];
|
|
298
332
|
}
|
|
299
|
-
testFunction(expression, scopes) {
|
|
300
|
-
return this.members(expression, { scopes, inBody: false, returnsOf
|
|
333
|
+
testFunction(expression, scopes, returnsOf) {
|
|
334
|
+
return this.members(expression, { scopes, inBody: false, returnsOf });
|
|
301
335
|
}
|
|
302
336
|
/** Whether a local variable is read as a test object it could be; see `localTestObject`. */
|
|
303
337
|
assumeLocal(declaration, name, links, args) {
|
|
@@ -343,7 +377,7 @@ export class PlaywrightAdapter {
|
|
|
343
377
|
// `IS_PLATFORM ? test.describe.serial : test.describe`: only the members both branches have apply.
|
|
344
378
|
return sharedMembers(this.members(node.consequent, lookup), this.members(node.alternate, lookup));
|
|
345
379
|
}
|
|
346
|
-
if (this.bindings.isTestObject(node))
|
|
380
|
+
if (this.bindings.isTestObject(node, false))
|
|
347
381
|
return [];
|
|
348
382
|
const chain = readChain(node);
|
|
349
383
|
if (!chain || chain.links.some((link) => link.args ?? link.template))
|
|
@@ -376,9 +410,15 @@ export class PlaywrightAdapter {
|
|
|
376
410
|
const resolved = this.file.values.resolve(node, context.scopes);
|
|
377
411
|
return resolved.ok && typeof resolved.value === 'string';
|
|
378
412
|
}
|
|
379
|
-
|
|
413
|
+
/** A chain Playwright refuses; one that starts like a declaration also declares a plain test or describe. */
|
|
414
|
+
invalid(call, names, reason, context) {
|
|
380
415
|
const callee = snippet(text(this.file, call.callee));
|
|
381
|
-
|
|
416
|
+
const refusal = problem('invalid-chain', call, `${callee} ${reason}.`, callee);
|
|
417
|
+
return refused(call, refusal, context, () => {
|
|
418
|
+
if (NOT_DECLARING.has(names[0]))
|
|
419
|
+
return { kind: 'other' };
|
|
420
|
+
return names.includes('describe') ? this.describeCall(call, {}, context) : this.testCall(call, '', context);
|
|
421
|
+
});
|
|
382
422
|
}
|
|
383
423
|
testCall(call, key, context) {
|
|
384
424
|
const args = call.arguments;
|
|
@@ -420,6 +460,7 @@ export class PlaywrightAdapter {
|
|
|
420
460
|
problems.push(titleProblem);
|
|
421
461
|
const line = this.file.source.line(call.start);
|
|
422
462
|
const fn = isFunctionNode(last) ? last : null;
|
|
463
|
+
const hashed = fn ?? namedFunction(this.file, context.scopes, last);
|
|
423
464
|
return {
|
|
424
465
|
kind: 'test',
|
|
425
466
|
title,
|
|
@@ -428,13 +469,23 @@ export class PlaywrightAdapter {
|
|
|
428
469
|
mode: null,
|
|
429
470
|
details: this.details(args.length === 3 ? args[1] : undefined, context, problems, title.text),
|
|
430
471
|
fn,
|
|
431
|
-
hashed
|
|
472
|
+
hashed,
|
|
432
473
|
isParameterized: false,
|
|
433
474
|
caseCount: null,
|
|
434
475
|
testContext: null,
|
|
476
|
+
fixtures: destructuredNames(hashed, 0),
|
|
477
|
+
usesBench: false,
|
|
435
478
|
problems,
|
|
436
479
|
};
|
|
437
480
|
}
|
|
481
|
+
/** `test.step(title, body)` in a test body; a title that can't be worked out is kept as source text. */
|
|
482
|
+
step(call, isSkipped, context) {
|
|
483
|
+
const { title } = readTitle(this.file, context.scopes, call.arguments[0], {
|
|
484
|
+
stripTags: false,
|
|
485
|
+
functionNames: false,
|
|
486
|
+
});
|
|
487
|
+
return { kind: 'step', title, isSkipped, fn: findFunction(call.arguments, 1) };
|
|
488
|
+
}
|
|
438
489
|
modifier(call, state, context) {
|
|
439
490
|
const [condition, description] = [call.arguments.at(0), call.arguments.at(1)];
|
|
440
491
|
const problems = [];
|
|
@@ -479,6 +530,7 @@ export class PlaywrightAdapter {
|
|
|
479
530
|
isOnly: kind.only === true,
|
|
480
531
|
mode: kind.mode ?? null,
|
|
481
532
|
isShuffled: false,
|
|
533
|
+
passesDown: null,
|
|
482
534
|
details: this.details(!anonymous && args.length === 3 ? args[1] : undefined, context, problems, title?.text ?? ''),
|
|
483
535
|
fn,
|
|
484
536
|
hashed: fn,
|
|
@@ -510,10 +562,37 @@ export class PlaywrightAdapter {
|
|
|
510
562
|
problems.push(unresolvedOption(this.file, 'lock', unresolved, importedFrom, 'the lock is missing'));
|
|
511
563
|
}
|
|
512
564
|
}
|
|
513
|
-
|
|
565
|
+
const run = {};
|
|
566
|
+
const timeout = options?.values.get('timeout');
|
|
567
|
+
const timeoutValue = timeout ? readCount(this.file, 'timeout', timeout, problems) : null;
|
|
568
|
+
if (timeoutValue !== null)
|
|
569
|
+
run.timeout = timeoutValue;
|
|
570
|
+
const retries = options?.values.get('retries');
|
|
571
|
+
const count = retries ? readCount(this.file, 'retries', retries, problems) : null;
|
|
572
|
+
if (count !== null)
|
|
573
|
+
run.retry = { count, delay: null, condition: null };
|
|
574
|
+
if (!mode && locks.length === 0 && Object.keys(run).length === 0) {
|
|
514
575
|
return problems.length > 0 ? { kind: 'invalid', problem: problems[0] } : { kind: 'other' };
|
|
515
576
|
}
|
|
516
|
-
return { kind: 'configure', mode, locks, problems };
|
|
577
|
+
return { kind: 'configure', mode, locks, run, problems };
|
|
578
|
+
}
|
|
579
|
+
/** `test.setTimeout(ms)` or `test.slow(condition?, description?)`, which apply where they are called. */
|
|
580
|
+
runSetting(call, key, context) {
|
|
581
|
+
const problems = [];
|
|
582
|
+
const settings = {};
|
|
583
|
+
const first = call.arguments.at(0);
|
|
584
|
+
if (key === 'slow') {
|
|
585
|
+
const condition = readCondition(this.file, context.scopes, first);
|
|
586
|
+
if (condition.applies !== 'never')
|
|
587
|
+
settings.slow = { conditions: condition.text === null ? null : [condition.text] };
|
|
588
|
+
}
|
|
589
|
+
else if (first && first.type !== 'SpreadElement') {
|
|
590
|
+
const option = { node: first, resolved: this.file.values.resolve(first, context.scopes) };
|
|
591
|
+
const timeout = readCount(this.file, 'timeout', option, problems);
|
|
592
|
+
if (timeout !== null)
|
|
593
|
+
settings.timeout = timeout;
|
|
594
|
+
}
|
|
595
|
+
return { kind: 'run', settings, argument: first ? snippet(text(this.file, first)) : null, problems };
|
|
517
596
|
}
|
|
518
597
|
/** `test.info().annotations.push(...)` inside a test body. */
|
|
519
598
|
annotationsPush(call, links, context) {
|
|
@@ -529,7 +608,7 @@ export class PlaywrightAdapter {
|
|
|
529
608
|
}
|
|
530
609
|
/** Reads the `{ tag, annotation, lock }` details of a test or describe. */
|
|
531
610
|
details(node, context, problems, title) {
|
|
532
|
-
const details = { tags: titleTags(title), annotations: [], locks: [], meta: null };
|
|
611
|
+
const details = { tags: titleTags(title), annotations: [], locks: [], meta: null, run: {} };
|
|
533
612
|
if (!node)
|
|
534
613
|
return details;
|
|
535
614
|
const options = readOptions(this.file, context.scopes, node);
|
|
@@ -26,7 +26,7 @@ export declare class VitestAdapter implements Adapter {
|
|
|
26
26
|
private warnedAboutTags;
|
|
27
27
|
constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
|
|
28
28
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
29
|
-
testFunction(expression: Expression, scopes: readonly Node[]): TestFunction | null;
|
|
29
|
+
testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): TestFunction | null;
|
|
30
30
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
31
31
|
private benchCall;
|
|
32
32
|
private chainCall;
|
|
@@ -62,6 +62,11 @@ export declare class VitestAdapter implements Adapter {
|
|
|
62
62
|
private hookCall;
|
|
63
63
|
private hook;
|
|
64
64
|
private contextSkip;
|
|
65
|
+
/**
|
|
66
|
+
* `annotate(message, type?)` from the test context (3.2), as an annotation of type `notice` when no type is given; the
|
|
67
|
+
* second argument may also be an attachment.
|
|
68
|
+
*/
|
|
69
|
+
private annotate;
|
|
65
70
|
private declaration;
|
|
66
71
|
/** Whether an argument holds options: an object, a name for one, or anything but a function before the body. */
|
|
67
72
|
private isOptions;
|
|
@@ -69,6 +74,8 @@ export declare class VitestAdapter implements Adapter {
|
|
|
69
74
|
/** Vitest's order: `only` wins over `skip`, which wins over `todo`. */
|
|
70
75
|
private states;
|
|
71
76
|
private readDeclarationOptions;
|
|
77
|
+
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`; the runner ignores an object before. */
|
|
78
|
+
private retry;
|
|
72
79
|
private meta;
|
|
73
80
|
private caseCount;
|
|
74
81
|
private arrayLength;
|