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, isImportMetaVitest, 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, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, text, } from "./common.js";
|
|
3
|
+
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
|
|
4
|
+
import { atLeast, findFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
5
5
|
/** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
|
|
6
6
|
function shared(a, b) {
|
|
7
7
|
if (!a || a.kind !== b?.kind)
|
|
@@ -37,6 +37,11 @@ const HOOKS = new Map([
|
|
|
37
37
|
['afterAll', null],
|
|
38
38
|
['aroundAll', null],
|
|
39
39
|
]);
|
|
40
|
+
function isHook(name) {
|
|
41
|
+
return HOOKS.has(name);
|
|
42
|
+
}
|
|
43
|
+
/** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
|
|
44
|
+
const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
|
|
40
45
|
/** First members that show a name imported from a local file is used as Vitest's `test`. */
|
|
41
46
|
const TEST_MEMBERS = new Set([...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS, ...KINDS.keys()]);
|
|
42
47
|
/** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
|
|
@@ -89,6 +94,23 @@ function hoistedValue(call, parents) {
|
|
|
89
94
|
const parent = parents.at(-1);
|
|
90
95
|
return parent?.type === 'AwaitExpression' ? parent : call;
|
|
91
96
|
}
|
|
97
|
+
/** Keys of the test context that Vitest provides itself, with the version that added each; they are not fixtures. */
|
|
98
|
+
const CONTEXT_KEYS = new Map([
|
|
99
|
+
['task', [3, 0]],
|
|
100
|
+
['expect', [3, 0]],
|
|
101
|
+
['skip', [3, 0]],
|
|
102
|
+
['onTestFailed', [3, 0]],
|
|
103
|
+
['onTestFinished', [3, 0]],
|
|
104
|
+
['annotate', [3, 2]],
|
|
105
|
+
['signal', [3, 2]],
|
|
106
|
+
['bench', [5, 0]],
|
|
107
|
+
]);
|
|
108
|
+
function fixturesOf(fn, index, version) {
|
|
109
|
+
return destructuredNames(fn, index).filter((name) => {
|
|
110
|
+
const since = CONTEXT_KEYS.get(name);
|
|
111
|
+
return !since || !atLeast(version, ...since);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
92
114
|
/** The names a file binds to Vitest's `test`, `describe`, hooks and namespace. */
|
|
93
115
|
class VitestBindings {
|
|
94
116
|
kinds = new Map();
|
|
@@ -102,9 +124,12 @@ class VitestBindings {
|
|
|
102
124
|
/** Names imported from other modules, which may be test objects there. */
|
|
103
125
|
imported;
|
|
104
126
|
follow;
|
|
127
|
+
/** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
|
|
128
|
+
functions;
|
|
105
129
|
constructor(program, version, follow) {
|
|
106
130
|
this.imported = moduleImports(program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
107
131
|
this.follow = follow;
|
|
132
|
+
this.functions = new ModuleFunctions(program, this.imported, follow);
|
|
108
133
|
for (const statement of program.body) {
|
|
109
134
|
if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
|
|
110
135
|
continue;
|
|
@@ -139,30 +164,49 @@ class VitestBindings {
|
|
|
139
164
|
this.declarations.add(declarator);
|
|
140
165
|
return bound;
|
|
141
166
|
});
|
|
167
|
+
this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
|
|
142
168
|
}
|
|
143
169
|
isTestName(name) {
|
|
144
|
-
return this.kinds.has(name) || this.importsTestObject(name);
|
|
170
|
+
return this.kinds.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
|
|
145
171
|
}
|
|
146
|
-
|
|
172
|
+
/**
|
|
173
|
+
* `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
|
|
174
|
+
* a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
|
|
175
|
+
* follows in their scopes.
|
|
176
|
+
*/
|
|
177
|
+
isTestObject(expression, followsCalls = true) {
|
|
147
178
|
const node = unwrap(expression);
|
|
148
|
-
if (node.type === 'Identifier')
|
|
149
|
-
|
|
179
|
+
if (node.type === 'Identifier') {
|
|
180
|
+
const { name } = node;
|
|
181
|
+
return (this.kinds.get(name) === 'test' || this.importsTestObject(name) || (followsCalls && this.holdsTestObject(name)));
|
|
182
|
+
}
|
|
150
183
|
if (node.type === 'MemberExpression') {
|
|
151
184
|
return KINDS.get(propertyName(node) ?? '') === 'test' && this.isNamespace(node.object);
|
|
152
185
|
}
|
|
153
186
|
// `process.env.CI ? vitestTest : skippedTest` is a test object either way.
|
|
154
187
|
if (node.type === 'ConditionalExpression') {
|
|
155
|
-
return this.isTestObject(node.consequent) || this.isTestObject(node.alternate);
|
|
188
|
+
return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
|
|
156
189
|
}
|
|
157
190
|
if (node.type !== 'CallExpression')
|
|
158
191
|
return false;
|
|
159
192
|
const callee = unwrap(node.callee);
|
|
193
|
+
// `defineBrokerTest()`, a function that returns a test object.
|
|
194
|
+
if (callee.type === 'Identifier')
|
|
195
|
+
return followsCalls && this.returnsTestObject(callee.name);
|
|
160
196
|
return (callee.type === 'MemberExpression' &&
|
|
161
197
|
DERIVATIONS.has(propertyName(callee) ?? '') &&
|
|
162
|
-
this.isTestObject(callee.object));
|
|
198
|
+
this.isTestObject(callee.object, followsCalls));
|
|
199
|
+
}
|
|
200
|
+
returnsTestObject(name) {
|
|
201
|
+
return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
|
|
202
|
+
}
|
|
203
|
+
holdsTestObject(name) {
|
|
204
|
+
return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
|
|
163
205
|
}
|
|
164
|
-
reexports(source, imported) {
|
|
165
|
-
|
|
206
|
+
reexports(source, imported, exported) {
|
|
207
|
+
if (source !== 'vitest')
|
|
208
|
+
return this.follow(source, imported, exported);
|
|
209
|
+
return exported === 'test object' && KINDS.has(imported);
|
|
166
210
|
}
|
|
167
211
|
/** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
|
|
168
212
|
importsTestObject(name) {
|
|
@@ -178,7 +222,7 @@ class VitestBindings {
|
|
|
178
222
|
if (kind) {
|
|
179
223
|
this.kinds.set(local, kind);
|
|
180
224
|
}
|
|
181
|
-
else if (
|
|
225
|
+
else if (isHook(imported)) {
|
|
182
226
|
this.hooks.set(local, imported);
|
|
183
227
|
}
|
|
184
228
|
else if (imported === 'bench' && atLeast(version, 5, 0)) {
|
|
@@ -189,7 +233,7 @@ class VitestBindings {
|
|
|
189
233
|
const { id, init } = declarator;
|
|
190
234
|
if (!init)
|
|
191
235
|
return false;
|
|
192
|
-
if (id.type === 'Identifier' && this.isTestObject(init)) {
|
|
236
|
+
if (id.type === 'Identifier' && this.isTestObject(init, false)) {
|
|
193
237
|
this.kinds.set(id.name, 'test');
|
|
194
238
|
return true;
|
|
195
239
|
}
|
|
@@ -203,7 +247,7 @@ class VitestBindings {
|
|
|
203
247
|
const kind = KINDS.get(key ?? '');
|
|
204
248
|
if (kind)
|
|
205
249
|
this.kinds.set(property.value.name, kind);
|
|
206
|
-
if (key !== null &&
|
|
250
|
+
if (key !== null && isHook(key))
|
|
207
251
|
this.hooks.set(property.value.name, key);
|
|
208
252
|
}
|
|
209
253
|
return true;
|
|
@@ -231,10 +275,11 @@ export class VitestAdapter {
|
|
|
231
275
|
// A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
|
|
232
276
|
// a call through the import itself is what reports an import that can't be followed.
|
|
233
277
|
const followed = new Map();
|
|
234
|
-
this.bindings = new VitestBindings(parsed.program, file.version, (source, imported) => {
|
|
235
|
-
const key = `${source}\0${imported}`;
|
|
236
|
-
if (!followed.has(key))
|
|
237
|
-
followed.set(key, followModule(source, imported, readModule, this.framework()).exported);
|
|
278
|
+
this.bindings = new VitestBindings(parsed.program, file.version, (source, imported, exported = 'test object') => {
|
|
279
|
+
const key = `${source}\0${exported}\0${imported}`;
|
|
280
|
+
if (!followed.has(key)) {
|
|
281
|
+
followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
|
|
282
|
+
}
|
|
238
283
|
return followed.get(key) === true;
|
|
239
284
|
});
|
|
240
285
|
this.problems = [...this.bindings.problems];
|
|
@@ -259,8 +304,8 @@ export class VitestAdapter {
|
|
|
259
304
|
this.contextSkip(call, context) ??
|
|
260
305
|
this.chainCall(call, context));
|
|
261
306
|
}
|
|
262
|
-
testFunction(expression, scopes) {
|
|
263
|
-
return this.aliasOf(expression, { scopes, inBody: false, returnsOf
|
|
307
|
+
testFunction(expression, scopes, returnsOf) {
|
|
308
|
+
return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
|
|
264
309
|
}
|
|
265
310
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
266
311
|
benchCall(call, context) {
|
|
@@ -282,7 +327,7 @@ export class VitestAdapter {
|
|
|
282
327
|
if (!resolved)
|
|
283
328
|
return null;
|
|
284
329
|
const { kind, links } = resolved;
|
|
285
|
-
if (kind === 'test' && links.length === 1 &&
|
|
330
|
+
if (kind === 'test' && links.length === 1 && isHook(links[0].name))
|
|
286
331
|
return this.hook(call, links[0].name);
|
|
287
332
|
if (links.length === 1 && DERIVATIONS.has(links[0].name))
|
|
288
333
|
return { kind: 'other' };
|
|
@@ -290,29 +335,24 @@ export class VitestAdapter {
|
|
|
290
335
|
if (last && (TABLES.has(last.name) || CONDITIONS.has(last.name)) && !last.args && !last.template) {
|
|
291
336
|
return { kind: 'other' };
|
|
292
337
|
}
|
|
293
|
-
|
|
338
|
+
const callee = snippet(text(this.file, call.callee));
|
|
339
|
+
const removed = (link) => link.name === 'sequential' && atLeast(this.file.version, 5, 0);
|
|
340
|
+
const valid = (link) => {
|
|
294
341
|
const called = link.args !== null || link.template !== null;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
kind: 'invalid',
|
|
311
|
-
problem: problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee),
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return this.declaration(call, kind, links, context);
|
|
342
|
+
if (MODIFIERS[kind].has(link.name))
|
|
343
|
+
return !called && !removed(link);
|
|
344
|
+
return TABLES.has(link.name) ? called : CONDITIONS.has(link.name) && link.args !== null;
|
|
345
|
+
};
|
|
346
|
+
const invalid = links.find((link) => !valid(link));
|
|
347
|
+
if (!invalid)
|
|
348
|
+
return this.declaration(call, kind, links, context);
|
|
349
|
+
const refusal = removed(invalid)
|
|
350
|
+
? problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee)
|
|
351
|
+
: problem('invalid-chain', call, `${callee} is not part of Vitest's API.`, callee);
|
|
352
|
+
if (NOT_DECLARING.has(links[0].name))
|
|
353
|
+
return { kind: 'invalid', problem: refusal };
|
|
354
|
+
// Which of its parts the chain means is not known, so it declares a plain test or describe.
|
|
355
|
+
return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
|
|
316
356
|
}
|
|
317
357
|
/**
|
|
318
358
|
* The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
|
|
@@ -402,7 +442,7 @@ export class VitestAdapter {
|
|
|
402
442
|
*/
|
|
403
443
|
returned(call, links, args, lookup) {
|
|
404
444
|
const callee = unwrap(call.callee);
|
|
405
|
-
if (callee.type !== 'Identifier'
|
|
445
|
+
if (callee.type !== 'Identifier')
|
|
406
446
|
return null;
|
|
407
447
|
const returns = lookup.returnsOf(callee.name, lookup.scopes);
|
|
408
448
|
if (returns)
|
|
@@ -472,27 +512,28 @@ export class VitestAdapter {
|
|
|
472
512
|
return null;
|
|
473
513
|
const declaration = this.file.values.declarationOf(callee.name, context.scopes);
|
|
474
514
|
const hook = declaration === null
|
|
475
|
-
?
|
|
515
|
+
? isHook(callee.name) && callee.name
|
|
476
516
|
: this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
|
|
477
517
|
return hook ? this.hook(call, hook) : null;
|
|
478
518
|
}
|
|
479
519
|
hook(call, name) {
|
|
480
520
|
const fn = findFunction(call.arguments);
|
|
481
521
|
const index = HOOKS.get(name);
|
|
482
|
-
return {
|
|
522
|
+
return {
|
|
523
|
+
kind: 'hook',
|
|
524
|
+
name,
|
|
525
|
+
fn,
|
|
526
|
+
testContext: index === null ? null : contextNames(fn, index),
|
|
527
|
+
fixtures: fixturesOf(fn, index, this.file.version),
|
|
528
|
+
};
|
|
483
529
|
}
|
|
484
530
|
contextSkip(call, context) {
|
|
485
531
|
const names = context.testContext;
|
|
486
532
|
if (!names)
|
|
487
533
|
return null;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
: callee.type === 'MemberExpression' &&
|
|
492
|
-
propertyName(callee) === 'skip' &&
|
|
493
|
-
callee.object.type === 'Identifier' &&
|
|
494
|
-
names.objects.has(callee.object.name);
|
|
495
|
-
if (!isSkip)
|
|
534
|
+
if (atLeast(this.file.version, 3, 2) && callsContext(call, names, 'annotate'))
|
|
535
|
+
return this.annotate(call, context);
|
|
536
|
+
if (!callsContext(call, names, 'skip'))
|
|
496
537
|
return null;
|
|
497
538
|
const [first, second] = call.arguments;
|
|
498
539
|
// skip(), skip(note) or skip(condition, note); before Vitest 3.1 the first argument is always the note.
|
|
@@ -507,6 +548,29 @@ export class VitestAdapter {
|
|
|
507
548
|
problems: reason.problem ? [reason.problem] : [],
|
|
508
549
|
};
|
|
509
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* `annotate(message, type?)` from the test context (3.2), as an annotation of type `notice` when no type is given; the
|
|
553
|
+
* second argument may also be an attachment.
|
|
554
|
+
*/
|
|
555
|
+
annotate(call, context) {
|
|
556
|
+
const args = call.arguments.map((arg) => arg.type === 'SpreadElement' ? null : this.file.values.resolve(arg, context.scopes));
|
|
557
|
+
const [message, second] = args;
|
|
558
|
+
// A second argument that isn't a string is an attachment, given in place of the type.
|
|
559
|
+
let type = 'notice';
|
|
560
|
+
if (args.length > 1 && !second?.ok)
|
|
561
|
+
type = null;
|
|
562
|
+
else if (second?.ok && typeof second.value === 'string')
|
|
563
|
+
type = second.value;
|
|
564
|
+
if (message?.ok && typeof message.value === 'string' && type !== null) {
|
|
565
|
+
const annotation = { type, description: message.value, lineStart: this.file.source.line(call.start) };
|
|
566
|
+
return { kind: 'annotations', items: [annotation], problems: [] };
|
|
567
|
+
}
|
|
568
|
+
const source = snippet(text(this.file, call));
|
|
569
|
+
const unresolved = [message, second].find((item) => item && !item.ok);
|
|
570
|
+
const from = unresolved && !unresolved.ok ? unresolved.importedFrom : null;
|
|
571
|
+
const missing = problem('unresolved-annotation', call, `Annotation ${source}${origin(from)} could not be worked out; it is missing.`, source);
|
|
572
|
+
return { kind: 'annotations', items: [], problems: [missing] };
|
|
573
|
+
}
|
|
510
574
|
declaration(call, kind, links, context) {
|
|
511
575
|
const args = call.arguments;
|
|
512
576
|
const callee = snippet(text(this.file, call.callee));
|
|
@@ -528,6 +592,8 @@ export class VitestAdapter {
|
|
|
528
592
|
// (title, fn, timeout), (title, options, fn) or the removed (title, fn, options)
|
|
529
593
|
let options;
|
|
530
594
|
let body;
|
|
595
|
+
let timeout;
|
|
596
|
+
let refusal = null;
|
|
531
597
|
const [second, third] = [args.at(1), args.at(2)];
|
|
532
598
|
if (args.some((arg) => arg.type === 'SpreadElement')) {
|
|
533
599
|
// With spread arguments, as in test(...args), the function and options are unknown.
|
|
@@ -540,29 +606,63 @@ export class VitestAdapter {
|
|
|
540
606
|
body = second;
|
|
541
607
|
if (third !== undefined && this.isOptions(third, undefined, context)) {
|
|
542
608
|
if (atLeast(this.file.version, 4, 0)) {
|
|
543
|
-
|
|
544
|
-
kind: 'invalid',
|
|
545
|
-
problem: problem('removed-api', third, `${callee}() takes options as its second argument since Vitest 4; with options third the call fails.`, snippet(text(this.file, third))),
|
|
546
|
-
};
|
|
609
|
+
refusal = problem('removed-api', third, `${callee}() takes options as its second argument since Vitest 4; with options third the call fails.`, snippet(text(this.file, third)));
|
|
547
610
|
}
|
|
548
611
|
options = third;
|
|
549
612
|
}
|
|
613
|
+
else if (third === undefined ||
|
|
614
|
+
isFunctionNode(second) ||
|
|
615
|
+
namedFunction(this.file, context.scopes, second) ||
|
|
616
|
+
this.file.values.resolve(third, context.scopes).ok) {
|
|
617
|
+
// With imported options and a body passed by name, either could be the body, so the third is no timeout.
|
|
618
|
+
timeout = third;
|
|
619
|
+
}
|
|
550
620
|
}
|
|
551
621
|
const fn = isFunctionNode(body) ? body : null;
|
|
552
622
|
const read = this.readDeclarationOptions(options, context, problems);
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
];
|
|
623
|
+
if (timeout) {
|
|
624
|
+
const node = timeout;
|
|
625
|
+
const option = { node, resolved: this.file.values.resolve(node, context.scopes) };
|
|
626
|
+
const value = readCount(this.file, 'timeout', option, problems);
|
|
627
|
+
if (value !== null)
|
|
628
|
+
read.details.run.timeout = value;
|
|
629
|
+
}
|
|
630
|
+
const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
|
|
631
|
+
const flags = new Set(chain);
|
|
632
|
+
let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
|
|
633
|
+
// A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
|
|
634
|
+
// test's mode, and for everything on a describe.
|
|
635
|
+
for (const [flag, value] of read.flags) {
|
|
636
|
+
if (value)
|
|
637
|
+
flags.add(flag);
|
|
638
|
+
else if (kind === 'test' && flag !== 'sequential')
|
|
639
|
+
flags.delete(flag);
|
|
640
|
+
}
|
|
641
|
+
if (read.skip && kind === 'test') {
|
|
642
|
+
flags.delete('skip');
|
|
643
|
+
conditions = [read.skip];
|
|
644
|
+
}
|
|
645
|
+
else if (read.skip) {
|
|
646
|
+
conditions.push(read.skip);
|
|
647
|
+
}
|
|
648
|
+
// Vitest passes a describe's options down to the tests and describes inside, where their own options and `only`
|
|
649
|
+
// win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
|
|
650
|
+
const line = this.file.source.line(call.start);
|
|
651
|
+
const around = context.passedDown;
|
|
652
|
+
const passed = read.skip ? null : around;
|
|
653
|
+
const skips = passed?.condition.applies === 'always' && !flags.has('only');
|
|
561
654
|
const table = links.find((link) => TABLES.has(link.name)) ?? null;
|
|
562
655
|
const isTodo = flags.has('todo') || body === undefined;
|
|
563
|
-
const states = this.states(flags, conditions, isTodo,
|
|
564
|
-
|
|
565
|
-
|
|
656
|
+
const states = this.states(flags, conditions, isTodo && !skips, line);
|
|
657
|
+
const skipped = () => states.some((setting) => setting.state === 'skip' && !setting.condition);
|
|
658
|
+
// The skip is set where the describe that passes it down sets it. A skipped describe around already skips what is
|
|
659
|
+
// inside, as it does when its own `only` beats its skip; a todo describe gets the skip itself.
|
|
660
|
+
if (skips && !skipped() && (!passed.covered || (kind === 'describe' && isTodo))) {
|
|
661
|
+
states.push({ state: 'skip', condition: null, reason: null, hasDynamicReason: false, line: passed.line });
|
|
662
|
+
}
|
|
663
|
+
// The chain's mode wins over the options'. `{ concurrent: false }` is how Vitest 5 turns parallel off, in place of
|
|
664
|
+
// `sequential`.
|
|
665
|
+
const parallel = chain.has('concurrent') || (!chain.has('sequential') && read.concurrent === true);
|
|
566
666
|
const mode = parallel
|
|
567
667
|
? 'parallel'
|
|
568
668
|
: flags.has('sequential') || read.concurrent === false
|
|
@@ -580,18 +680,27 @@ export class VitestAdapter {
|
|
|
580
680
|
caseCount: table ? this.caseCount(call, table, context, problems) : null,
|
|
581
681
|
problems,
|
|
582
682
|
};
|
|
683
|
+
let declared;
|
|
583
684
|
if (kind === 'describe') {
|
|
584
685
|
// A table's cases are passed instead.
|
|
585
686
|
const first = table ? null : fn?.params.at(0);
|
|
586
687
|
if (fn && first?.type === 'Identifier')
|
|
587
688
|
this.suiteParameters.set(fn, first.name);
|
|
588
|
-
|
|
689
|
+
const own = read.skip ? { condition: read.skip, line } : around;
|
|
690
|
+
const passesDown = own && { ...own, covered: (around?.covered ?? false) || skipped() };
|
|
691
|
+
declared = { kind: 'describe', ...base, isShuffled: flags.has('shuffle'), passesDown };
|
|
589
692
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
693
|
+
else {
|
|
694
|
+
const index = table?.name === 'for' ? 1 : table ? null : 0;
|
|
695
|
+
declared = {
|
|
696
|
+
kind: 'test',
|
|
697
|
+
...base,
|
|
698
|
+
testContext: contextNames(base.hashed, index),
|
|
699
|
+
fixtures: fixturesOf(base.hashed, index, this.file.version),
|
|
700
|
+
usesBench: atLeast(this.file.version, 5, 0) && destructuredNames(base.hashed, index).includes('bench'),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
return refusal ? refused(call, refusal, context, () => declared) : declared;
|
|
595
704
|
}
|
|
596
705
|
/** Whether an argument holds options: an object, a name for one, or anything but a function before the body. */
|
|
597
706
|
isOptions(node, next, context) {
|
|
@@ -637,11 +746,11 @@ export class VitestAdapter {
|
|
|
637
746
|
return fails;
|
|
638
747
|
}
|
|
639
748
|
readDeclarationOptions(node, context, problems) {
|
|
640
|
-
const details = { tags: [], annotations: [], locks: [], meta: null };
|
|
749
|
+
const details = { tags: [], annotations: [], locks: [], meta: null, run: {} };
|
|
641
750
|
const result = {
|
|
642
|
-
flags:
|
|
751
|
+
flags: new Map(),
|
|
643
752
|
concurrent: null,
|
|
644
|
-
|
|
753
|
+
skip: null,
|
|
645
754
|
details,
|
|
646
755
|
};
|
|
647
756
|
if (!node)
|
|
@@ -657,15 +766,15 @@ export class VitestAdapter {
|
|
|
657
766
|
else if (option?.resolved.ok && flag === 'concurrent') {
|
|
658
767
|
result.concurrent = Boolean(option.resolved.value);
|
|
659
768
|
}
|
|
660
|
-
else if (option?.resolved.ok
|
|
661
|
-
result.flags.
|
|
769
|
+
else if (option?.resolved.ok) {
|
|
770
|
+
result.flags.set(flag, Boolean(option.resolved.value));
|
|
662
771
|
}
|
|
663
772
|
}
|
|
664
773
|
const skip = options.values.get('skip');
|
|
665
774
|
if (skip) {
|
|
666
|
-
result.
|
|
775
|
+
result.skip = skip.resolved.ok
|
|
667
776
|
? { applies: skip.resolved.value ? 'always' : 'never', text: null }
|
|
668
|
-
: { applies: 'sometimes', text: snippet(text(this.file, skip.node)) }
|
|
777
|
+
: { applies: 'sometimes', text: snippet(text(this.file, skip.node)) };
|
|
669
778
|
}
|
|
670
779
|
const tags = options.values.get('tags');
|
|
671
780
|
if (tags && atLeast(this.file.version, 4, 1)) {
|
|
@@ -680,8 +789,46 @@ export class VitestAdapter {
|
|
|
680
789
|
const meta = options.values.get('meta');
|
|
681
790
|
if (meta)
|
|
682
791
|
details.meta = this.meta(meta, context, problems);
|
|
792
|
+
for (const key of ['timeout', 'repeats']) {
|
|
793
|
+
const option = options.values.get(key);
|
|
794
|
+
const value = option ? readCount(this.file, key, option, problems) : null;
|
|
795
|
+
if (value !== null)
|
|
796
|
+
details.run[key] = value;
|
|
797
|
+
}
|
|
798
|
+
const retry = options.values.get('retry');
|
|
799
|
+
if (retry)
|
|
800
|
+
this.retry(retry, details, context, problems);
|
|
683
801
|
return result;
|
|
684
802
|
}
|
|
803
|
+
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`; the runner ignores an object before. */
|
|
804
|
+
retry(option, details, context, problems) {
|
|
805
|
+
const node = unwrap(option.node);
|
|
806
|
+
if (node.type !== 'ObjectExpression') {
|
|
807
|
+
const count = readCount(this.file, 'retry', option, problems);
|
|
808
|
+
if (count !== null)
|
|
809
|
+
details.run.retry = { count, delay: null, condition: null };
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (!atLeast(this.file.version, 4, 1))
|
|
813
|
+
return;
|
|
814
|
+
const values = readOptions(this.file, context.scopes, node);
|
|
815
|
+
problems.push(...unresolvedOptions(this.file, values, 'the retry is not applied'));
|
|
816
|
+
const read = (key) => {
|
|
817
|
+
const value = values.values.get(key);
|
|
818
|
+
return value ? readCount(this.file, `retry.${key}`, value, problems) : undefined;
|
|
819
|
+
};
|
|
820
|
+
const count = read('count');
|
|
821
|
+
const delay = read('delay');
|
|
822
|
+
if (count === null)
|
|
823
|
+
return;
|
|
824
|
+
const condition = values.values.get('condition');
|
|
825
|
+
details.run.retry = {
|
|
826
|
+
// Vitest retries 0 times when the object has no count.
|
|
827
|
+
count: count ?? 0,
|
|
828
|
+
delay: delay ?? null,
|
|
829
|
+
condition: condition ? snippet(text(this.file, condition.node)) : null,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
685
832
|
meta(option, context, problems) {
|
|
686
833
|
const entries = readOptions(this.file, context.scopes, option.node);
|
|
687
834
|
problems.push(...unresolvedOptions(this.file, entries, 'its meta values are missing'));
|
|
@@ -738,19 +885,32 @@ function templateRows(template) {
|
|
|
738
885
|
function isMetaValue(value) {
|
|
739
886
|
return value !== undefined;
|
|
740
887
|
}
|
|
741
|
-
function
|
|
888
|
+
function contextNames(fn, index) {
|
|
742
889
|
const parameter = index === null ? undefined : fn?.params[index];
|
|
743
890
|
if (!parameter)
|
|
744
891
|
return null;
|
|
745
|
-
const names = { objects: new Set(),
|
|
892
|
+
const names = { objects: new Set(), skip: new Set(), annotate: new Set() };
|
|
746
893
|
if (parameter.type === 'Identifier')
|
|
747
894
|
names.objects.add(parameter.name);
|
|
748
895
|
if (parameter.type === 'ObjectPattern') {
|
|
749
896
|
for (const property of parameter.properties) {
|
|
750
|
-
|
|
751
|
-
|
|
897
|
+
const key = property.type === 'Property' ? propertyKey(property) : null;
|
|
898
|
+
if ((key === 'skip' || key === 'annotate') &&
|
|
899
|
+
property.type === 'Property' &&
|
|
900
|
+
property.value.type === 'Identifier') {
|
|
901
|
+
names[key].add(property.value.name);
|
|
752
902
|
}
|
|
753
903
|
}
|
|
754
904
|
}
|
|
755
905
|
return names;
|
|
756
906
|
}
|
|
907
|
+
/** Whether a call is `name(...)` for a context function taken out of the context, or `context.name(...)`. */
|
|
908
|
+
function callsContext(call, names, member) {
|
|
909
|
+
const callee = unwrap(call.callee);
|
|
910
|
+
return callee.type === 'Identifier'
|
|
911
|
+
? names[member].has(callee.name)
|
|
912
|
+
: callee.type === 'MemberExpression' &&
|
|
913
|
+
propertyName(callee) === member &&
|
|
914
|
+
callee.object.type === 'Identifier' &&
|
|
915
|
+
names.objects.has(callee.object.name);
|
|
916
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { scan } from './scan.ts';
|
|
2
2
|
export { scanSource } from './scan-source.ts';
|
|
3
|
+
export { filterInventory, pickTestFields } from './filter.ts';
|
|
4
|
+
export type { InventoryFilter, PickedInventory } from './filter.ts';
|
|
3
5
|
export type { CommentsMode, ScanOptions, ScanSourceOptions } from './options.ts';
|
|
4
|
-
export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, Framework, Inventory, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
|
|
6
|
+
export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, FileRecord, Framework, HookCounts, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
|
package/dist/index.js
CHANGED
package/dist/inventory.js
CHANGED
package/dist/json.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Inventory } from './types.ts';
|
|
2
|
+
/** An inventory as the command line writes it, with every key of the test records or only the picked ones. */
|
|
3
|
+
export type Output = Omit<Inventory, 'tests'> & {
|
|
4
|
+
tests: object[];
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
7
|
* Writes an inventory as JSON one record at a time, so a large inventory never becomes one huge string.
|
|
4
8
|
* The text is exactly what `JSON.stringify(inventory, null, pretty ? 2 : undefined)` returns.
|
|
5
9
|
*/
|
|
6
|
-
export declare function writeInventory(inventory:
|
|
10
|
+
export declare function writeInventory(inventory: Output, pretty: boolean, write: (chunk: string) => Promise<void>): Promise<void>;
|
|
11
|
+
/** Writes an inventory as JSON Lines: one line per record, each marked by `type`, as `InventoryLine` describes. */
|
|
12
|
+
export declare function writeInventoryLines(inventory: Output, write: (chunk: string) => Promise<void>): Promise<void>;
|
package/dist/json.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/** Collects small pieces into larger chunks, so a stream gets few writes. */
|
|
2
2
|
const CHUNK_SIZE = 64 * 1024;
|
|
3
|
+
function chunked(write) {
|
|
4
|
+
let buffer = '';
|
|
5
|
+
return {
|
|
6
|
+
emit: async (text) => {
|
|
7
|
+
buffer += text;
|
|
8
|
+
if (buffer.length >= CHUNK_SIZE) {
|
|
9
|
+
await write(buffer);
|
|
10
|
+
buffer = '';
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
flush: () => write(buffer),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
3
16
|
/**
|
|
4
17
|
* Writes an inventory as JSON one record at a time, so a large inventory never becomes one huge string.
|
|
5
18
|
* The text is exactly what `JSON.stringify(inventory, null, pretty ? 2 : undefined)` returns.
|
|
6
19
|
*/
|
|
7
20
|
export async function writeInventory(inventory, pretty, write) {
|
|
8
|
-
|
|
9
|
-
const emit = async (text) => {
|
|
10
|
-
buffer += text;
|
|
11
|
-
if (buffer.length >= CHUNK_SIZE) {
|
|
12
|
-
await write(buffer);
|
|
13
|
-
buffer = '';
|
|
14
|
-
}
|
|
15
|
-
};
|
|
21
|
+
const { emit, flush } = chunked(write);
|
|
16
22
|
const stringify = (value, depth) => {
|
|
17
23
|
const json = JSON.stringify(value, null, pretty ? 2 : undefined);
|
|
18
24
|
return pretty ? json.replaceAll('\n', `\n${' '.repeat(depth)}`) : json;
|
|
@@ -34,5 +40,22 @@ export async function writeInventory(inventory, pretty, write) {
|
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
await emit(`${newline(0)}}`);
|
|
37
|
-
await
|
|
43
|
+
await flush();
|
|
44
|
+
}
|
|
45
|
+
/** Writes an inventory as JSON Lines: one line per record, each marked by `type`, as `InventoryLine` describes. */
|
|
46
|
+
export async function writeInventoryLines(inventory, write) {
|
|
47
|
+
const { emit, flush } = chunked(write);
|
|
48
|
+
const line = (type, record) => emit(`${JSON.stringify({ type, ...record })}\n`);
|
|
49
|
+
const { schemaVersion, tool, framework, frameworkVersion } = inventory;
|
|
50
|
+
await line('inventory', { schemaVersion, tool, framework, frameworkVersion });
|
|
51
|
+
for (const file of inventory.files)
|
|
52
|
+
await line('file', file);
|
|
53
|
+
for (const suite of inventory.suites)
|
|
54
|
+
await line('suite', suite);
|
|
55
|
+
for (const test of inventory.tests)
|
|
56
|
+
await line('test', test);
|
|
57
|
+
for (const diagnostic of inventory.diagnostics)
|
|
58
|
+
await line('diagnostic', diagnostic);
|
|
59
|
+
await line('summary', inventory.summary);
|
|
60
|
+
await flush();
|
|
38
61
|
}
|