test-inventory 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +3 -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/extract.js +55 -10
- package/dist/files.d.ts +1 -1
- package/dist/files.js +5 -3
- package/dist/filter.d.ts +37 -0
- package/dist/filter.js +153 -0
- package/dist/frameworks/bindings.d.ts +37 -6
- package/dist/frameworks/bindings.js +213 -74
- package/dist/frameworks/common.d.ts +17 -2
- package/dist/frameworks/common.js +46 -2
- package/dist/frameworks/playwright.d.ts +24 -4
- package/dist/frameworks/playwright.js +132 -63
- package/dist/frameworks/vitest.d.ts +23 -2
- package/dist/frameworks/vitest.js +166 -84
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +7 -1
- package/dist/json.js +30 -9
- package/dist/model.d.ts +41 -2
- package/dist/options.d.ts +5 -2
- package/dist/records.js +5 -3
- package/dist/resolve.js +6 -1
- package/dist/types.d.ts +17 -1
- package/package.json +2 -2
- package/schema/v1.json +1 -1
|
@@ -1,7 +1,18 @@
|
|
|
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, 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, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
5
|
+
/** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
|
|
6
|
+
function shared(a, b) {
|
|
7
|
+
if (!a || a.kind !== b?.kind)
|
|
8
|
+
return null;
|
|
9
|
+
const same = (link, index) => {
|
|
10
|
+
const other = b.links.at(index);
|
|
11
|
+
return other?.name === link.name && !link.args && !link.template && !other.args && !other.template;
|
|
12
|
+
};
|
|
13
|
+
const differs = a.links.findIndex((link, index) => !same(link, index));
|
|
14
|
+
return { kind: a.kind, links: differs === -1 ? a.links : a.links.slice(0, differs) };
|
|
15
|
+
}
|
|
5
16
|
// Maps rather than object literals, so names such as `constructor` don't match inherited properties.
|
|
6
17
|
const KINDS = new Map([
|
|
7
18
|
['test', 'test'],
|
|
@@ -26,6 +37,8 @@ const HOOKS = new Map([
|
|
|
26
37
|
['afterAll', null],
|
|
27
38
|
['aroundAll', null],
|
|
28
39
|
]);
|
|
40
|
+
/** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
|
|
41
|
+
const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
|
|
29
42
|
/** First members that show a name imported from a local file is used as Vitest's `test`. */
|
|
30
43
|
const TEST_MEMBERS = new Set([...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS, ...KINDS.keys()]);
|
|
31
44
|
/** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
|
|
@@ -91,9 +104,12 @@ class VitestBindings {
|
|
|
91
104
|
/** Names imported from other modules, which may be test objects there. */
|
|
92
105
|
imported;
|
|
93
106
|
follow;
|
|
107
|
+
/** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
|
|
108
|
+
functions;
|
|
94
109
|
constructor(program, version, follow) {
|
|
95
110
|
this.imported = moduleImports(program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
96
111
|
this.follow = follow;
|
|
112
|
+
this.functions = new ModuleFunctions(program, this.imported, follow);
|
|
97
113
|
for (const statement of program.body) {
|
|
98
114
|
if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
|
|
99
115
|
continue;
|
|
@@ -128,30 +144,49 @@ class VitestBindings {
|
|
|
128
144
|
this.declarations.add(declarator);
|
|
129
145
|
return bound;
|
|
130
146
|
});
|
|
147
|
+
this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
|
|
131
148
|
}
|
|
132
149
|
isTestName(name) {
|
|
133
|
-
return this.kinds.has(name) || this.importsTestObject(name);
|
|
150
|
+
return this.kinds.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
|
|
134
151
|
}
|
|
135
|
-
|
|
152
|
+
/**
|
|
153
|
+
* `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
|
|
154
|
+
* a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
|
|
155
|
+
* follows in their scopes.
|
|
156
|
+
*/
|
|
157
|
+
isTestObject(expression, followsCalls = true) {
|
|
136
158
|
const node = unwrap(expression);
|
|
137
|
-
if (node.type === 'Identifier')
|
|
138
|
-
|
|
159
|
+
if (node.type === 'Identifier') {
|
|
160
|
+
const { name } = node;
|
|
161
|
+
return (this.kinds.get(name) === 'test' || this.importsTestObject(name) || (followsCalls && this.holdsTestObject(name)));
|
|
162
|
+
}
|
|
139
163
|
if (node.type === 'MemberExpression') {
|
|
140
164
|
return KINDS.get(propertyName(node) ?? '') === 'test' && this.isNamespace(node.object);
|
|
141
165
|
}
|
|
142
166
|
// `process.env.CI ? vitestTest : skippedTest` is a test object either way.
|
|
143
167
|
if (node.type === 'ConditionalExpression') {
|
|
144
|
-
return this.isTestObject(node.consequent) || this.isTestObject(node.alternate);
|
|
168
|
+
return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
|
|
145
169
|
}
|
|
146
170
|
if (node.type !== 'CallExpression')
|
|
147
171
|
return false;
|
|
148
172
|
const callee = unwrap(node.callee);
|
|
173
|
+
// `defineBrokerTest()`, a function that returns a test object.
|
|
174
|
+
if (callee.type === 'Identifier')
|
|
175
|
+
return followsCalls && this.returnsTestObject(callee.name);
|
|
149
176
|
return (callee.type === 'MemberExpression' &&
|
|
150
177
|
DERIVATIONS.has(propertyName(callee) ?? '') &&
|
|
151
|
-
this.isTestObject(callee.object));
|
|
178
|
+
this.isTestObject(callee.object, followsCalls));
|
|
179
|
+
}
|
|
180
|
+
returnsTestObject(name) {
|
|
181
|
+
return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
|
|
152
182
|
}
|
|
153
|
-
|
|
154
|
-
return
|
|
183
|
+
holdsTestObject(name) {
|
|
184
|
+
return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
|
|
185
|
+
}
|
|
186
|
+
reexports(source, imported, exported) {
|
|
187
|
+
if (source !== 'vitest')
|
|
188
|
+
return this.follow(source, imported, exported);
|
|
189
|
+
return exported === 'test object' && KINDS.has(imported);
|
|
155
190
|
}
|
|
156
191
|
/** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
|
|
157
192
|
importsTestObject(name) {
|
|
@@ -178,7 +213,7 @@ class VitestBindings {
|
|
|
178
213
|
const { id, init } = declarator;
|
|
179
214
|
if (!init)
|
|
180
215
|
return false;
|
|
181
|
-
if (id.type === 'Identifier' && this.isTestObject(init)) {
|
|
216
|
+
if (id.type === 'Identifier' && this.isTestObject(init, false)) {
|
|
182
217
|
this.kinds.set(id.name, 'test');
|
|
183
218
|
return true;
|
|
184
219
|
}
|
|
@@ -208,6 +243,7 @@ export class VitestAdapter {
|
|
|
208
243
|
imports;
|
|
209
244
|
followed = new Map();
|
|
210
245
|
reported = new Set();
|
|
246
|
+
/** Variables and names of functions read as test objects on trust, each reported once. */
|
|
211
247
|
assumed = new Set();
|
|
212
248
|
aliases = new Map();
|
|
213
249
|
/** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
|
|
@@ -219,10 +255,11 @@ export class VitestAdapter {
|
|
|
219
255
|
// A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
|
|
220
256
|
// a call through the import itself is what reports an import that can't be followed.
|
|
221
257
|
const followed = new Map();
|
|
222
|
-
this.bindings = new VitestBindings(parsed.program, file.version, (source, imported) => {
|
|
223
|
-
const key = `${source}\0${imported}`;
|
|
224
|
-
if (!followed.has(key))
|
|
225
|
-
followed.set(key, followModule(source, imported, readModule, this.framework()).exported);
|
|
258
|
+
this.bindings = new VitestBindings(parsed.program, file.version, (source, imported, exported = 'test object') => {
|
|
259
|
+
const key = `${source}\0${exported}\0${imported}`;
|
|
260
|
+
if (!followed.has(key)) {
|
|
261
|
+
followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
|
|
262
|
+
}
|
|
226
263
|
return followed.get(key) === true;
|
|
227
264
|
});
|
|
228
265
|
this.problems = [...this.bindings.problems];
|
|
@@ -247,6 +284,9 @@ export class VitestAdapter {
|
|
|
247
284
|
this.contextSkip(call, context) ??
|
|
248
285
|
this.chainCall(call, context));
|
|
249
286
|
}
|
|
287
|
+
testFunction(expression, scopes, returnsOf) {
|
|
288
|
+
return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
|
|
289
|
+
}
|
|
250
290
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
251
291
|
benchCall(call, context) {
|
|
252
292
|
const base = readChain(call.callee)?.base;
|
|
@@ -263,7 +303,7 @@ export class VitestAdapter {
|
|
|
263
303
|
}
|
|
264
304
|
chainCall(call, context) {
|
|
265
305
|
const chain = readChain(call.callee);
|
|
266
|
-
const resolved = chain ? this.resolve(chain, call.arguments, context
|
|
306
|
+
const resolved = chain ? this.resolve(chain, call.arguments, context) : null;
|
|
267
307
|
if (!resolved)
|
|
268
308
|
return null;
|
|
269
309
|
const { kind, links } = resolved;
|
|
@@ -275,36 +315,31 @@ export class VitestAdapter {
|
|
|
275
315
|
if (last && (TABLES.has(last.name) || CONDITIONS.has(last.name)) && !last.args && !last.template) {
|
|
276
316
|
return { kind: 'other' };
|
|
277
317
|
}
|
|
278
|
-
|
|
318
|
+
const callee = snippet(text(this.file, call.callee));
|
|
319
|
+
const removed = (link) => link.name === 'sequential' && atLeast(this.file.version, 5, 0);
|
|
320
|
+
const valid = (link) => {
|
|
279
321
|
const called = link.args !== null || link.template !== null;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
kind: 'invalid',
|
|
296
|
-
problem: problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee),
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return this.declaration(call, kind, links, context);
|
|
322
|
+
if (MODIFIERS[kind].has(link.name))
|
|
323
|
+
return !called && !removed(link);
|
|
324
|
+
return TABLES.has(link.name) ? called : CONDITIONS.has(link.name) && link.args !== null;
|
|
325
|
+
};
|
|
326
|
+
const invalid = links.find((link) => !valid(link));
|
|
327
|
+
if (!invalid)
|
|
328
|
+
return this.declaration(call, kind, links, context);
|
|
329
|
+
const refusal = removed(invalid)
|
|
330
|
+
? problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee)
|
|
331
|
+
: problem('invalid-chain', call, `${callee} is not part of Vitest's API.`, callee);
|
|
332
|
+
if (NOT_DECLARING.has(links[0].name))
|
|
333
|
+
return { kind: 'invalid', problem: refusal };
|
|
334
|
+
// Which of its parts the chain means is not known, so it declares a plain test or describe.
|
|
335
|
+
return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
|
|
301
336
|
}
|
|
302
337
|
/**
|
|
303
338
|
* 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
|
|
304
339
|
* followed is reported only outside test and hook bodies, where a call can declare tests.
|
|
305
340
|
*/
|
|
306
|
-
resolve(chain, args,
|
|
307
|
-
const start = this.start(chain, args,
|
|
341
|
+
resolve(chain, args, lookup) {
|
|
342
|
+
const start = this.start(chain, args, lookup);
|
|
308
343
|
if (!start)
|
|
309
344
|
return null;
|
|
310
345
|
let kind = start.kind;
|
|
@@ -318,8 +353,13 @@ export class VitestAdapter {
|
|
|
318
353
|
}
|
|
319
354
|
return { kind, links };
|
|
320
355
|
}
|
|
321
|
-
start(chain, args,
|
|
356
|
+
start(chain, args, lookup) {
|
|
322
357
|
const { base, links } = chain;
|
|
358
|
+
const { scopes, inBody } = lookup;
|
|
359
|
+
if (base.type === 'CallExpression') {
|
|
360
|
+
const returned = this.returned(base, links, args, lookup);
|
|
361
|
+
return returned && { kind: returned.kind, index: 0, modifiers: returned.links };
|
|
362
|
+
}
|
|
323
363
|
if (base.type !== 'Identifier')
|
|
324
364
|
return null;
|
|
325
365
|
// A name the file doesn't declare is one of Vitest's globals; a local variable or parameter with the same name,
|
|
@@ -335,10 +375,10 @@ export class VitestAdapter {
|
|
|
335
375
|
if (this.suiteParameters.get(declaration) === base.name)
|
|
336
376
|
return { kind: 'test', index: 0 };
|
|
337
377
|
const followed = this.imports.get(base.name)?.node === declaration &&
|
|
338
|
-
this.
|
|
378
|
+
this.worthFollowing(base.name, links, args) &&
|
|
339
379
|
this.follow(base.name, !inBody && mayDeclareTests(base.name, links));
|
|
340
380
|
if (!followed && !this.bindings.declarations.has(declaration)) {
|
|
341
|
-
const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration,
|
|
381
|
+
const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, lookup) : null;
|
|
342
382
|
if (alias)
|
|
343
383
|
return { kind: alias.kind, index: 0, modifiers: alias.links };
|
|
344
384
|
return this.assumeLocal(declaration, base.name, links, args)
|
|
@@ -353,37 +393,48 @@ export class VitestAdapter {
|
|
|
353
393
|
return kind ? { kind, index: 0 } : null;
|
|
354
394
|
}
|
|
355
395
|
/** The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`. */
|
|
356
|
-
alias(declarator,
|
|
396
|
+
alias(declarator, lookup) {
|
|
357
397
|
let alias = this.aliases.get(declarator);
|
|
358
398
|
if (alias === undefined) {
|
|
359
399
|
// Set first, so a variable defined through itself ends the lookup.
|
|
360
400
|
this.aliases.set(declarator, null);
|
|
361
|
-
alias = declarator.init ? this.aliasOf(declarator.init,
|
|
401
|
+
alias = declarator.init ? this.aliasOf(declarator.init, lookup) : null;
|
|
362
402
|
this.aliases.set(declarator, alias);
|
|
363
403
|
}
|
|
364
404
|
return alias;
|
|
365
405
|
}
|
|
366
|
-
aliasOf(expression,
|
|
406
|
+
aliasOf(expression, lookup) {
|
|
367
407
|
const node = unwrap(expression);
|
|
368
408
|
if (node.type === 'ConditionalExpression') {
|
|
369
409
|
// `isCI ? describe.concurrent : describe`: only the modifiers both branches have apply.
|
|
370
|
-
|
|
371
|
-
const alternate = this.aliasOf(node.alternate, scopes, inBody);
|
|
372
|
-
if (!consequent || consequent.kind !== alternate?.kind)
|
|
373
|
-
return null;
|
|
374
|
-
const same = (link, index) => {
|
|
375
|
-
const other = alternate.links.at(index);
|
|
376
|
-
return other?.name === link.name && !link.args && !link.template && !other.args && !other.template;
|
|
377
|
-
};
|
|
378
|
-
const shared = consequent.links.findIndex((link, index) => !same(link, index));
|
|
379
|
-
return { kind: consequent.kind, links: shared === -1 ? consequent.links : consequent.links.slice(0, shared) };
|
|
410
|
+
return shared(this.aliasOf(node.consequent, lookup), this.aliasOf(node.alternate, lookup));
|
|
380
411
|
}
|
|
381
412
|
const chain = readChain(node);
|
|
382
|
-
const resolved = chain ? this.resolve(chain, [],
|
|
413
|
+
const resolved = chain ? this.resolve(chain, [], lookup) : null;
|
|
383
414
|
const allowed = (link) => resolved !== null &&
|
|
384
415
|
(MODIFIERS[resolved.kind].has(link.name) || TABLES.has(link.name) || CONDITIONS.has(link.name));
|
|
385
416
|
return resolved?.links.every(allowed) ? resolved : null;
|
|
386
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
|
|
420
|
+
* function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
|
|
421
|
+
* named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
|
|
422
|
+
*/
|
|
423
|
+
returned(call, links, args, lookup) {
|
|
424
|
+
const callee = unwrap(call.callee);
|
|
425
|
+
if (callee.type !== 'Identifier')
|
|
426
|
+
return null;
|
|
427
|
+
const returns = lookup.returnsOf(callee.name, lookup.scopes);
|
|
428
|
+
if (returns)
|
|
429
|
+
return returnedFunction(returns, shared);
|
|
430
|
+
if (!isTestName(callee.name) || !this.looksLikeTest(links, args))
|
|
431
|
+
return null;
|
|
432
|
+
if (!this.assumed.has(callee.name)) {
|
|
433
|
+
this.assumed.add(callee.name);
|
|
434
|
+
this.problems.push(returnedTestObject(this.file, call));
|
|
435
|
+
}
|
|
436
|
+
return { kind: KINDS.get(callee.name) ?? 'test', links: [] };
|
|
437
|
+
}
|
|
387
438
|
/** Whether a local variable is read as a test object it could be; see `localTestObject`. */
|
|
388
439
|
assumeLocal(declaration, name, links, args) {
|
|
389
440
|
if (declaration.type !== 'VariableDeclarator' || !mayHoldTestObject(declaration) || !isTestName(name))
|
|
@@ -399,6 +450,14 @@ export class VitestAdapter {
|
|
|
399
450
|
looksLikeTest(links, args) {
|
|
400
451
|
return links.length === 0 ? looksLikeDeclaration(args) : TEST_MEMBERS.has(links[0].name);
|
|
401
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
|
|
455
|
+
* title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
|
|
456
|
+
* a package extends.
|
|
457
|
+
*/
|
|
458
|
+
worthFollowing(name, links, args) {
|
|
459
|
+
return this.looksLikeTest(links, args) || (isTestName(name) && looksLikeDeclaration(args));
|
|
460
|
+
}
|
|
402
461
|
/** Vitest as followed modules are read, with the rules of the scanned version. */
|
|
403
462
|
framework() {
|
|
404
463
|
const version = this.file.version;
|
|
@@ -489,6 +548,7 @@ export class VitestAdapter {
|
|
|
489
548
|
// (title, fn, timeout), (title, options, fn) or the removed (title, fn, options)
|
|
490
549
|
let options;
|
|
491
550
|
let body;
|
|
551
|
+
let refusal = null;
|
|
492
552
|
const [second, third] = [args.at(1), args.at(2)];
|
|
493
553
|
if (args.some((arg) => arg.type === 'SpreadElement')) {
|
|
494
554
|
// With spread arguments, as in test(...args), the function and options are unknown.
|
|
@@ -501,29 +561,49 @@ export class VitestAdapter {
|
|
|
501
561
|
body = second;
|
|
502
562
|
if (third !== undefined && this.isOptions(third, undefined, context)) {
|
|
503
563
|
if (atLeast(this.file.version, 4, 0)) {
|
|
504
|
-
|
|
505
|
-
kind: 'invalid',
|
|
506
|
-
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))),
|
|
507
|
-
};
|
|
564
|
+
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)));
|
|
508
565
|
}
|
|
509
566
|
options = third;
|
|
510
567
|
}
|
|
511
568
|
}
|
|
512
569
|
const fn = isFunctionNode(body) ? body : null;
|
|
513
570
|
const read = this.readDeclarationOptions(options, context, problems);
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
571
|
+
const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
|
|
572
|
+
const flags = new Set(chain);
|
|
573
|
+
let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
|
|
574
|
+
// A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
|
|
575
|
+
// test's mode, and for everything on a describe.
|
|
576
|
+
for (const [flag, value] of read.flags) {
|
|
577
|
+
if (value)
|
|
578
|
+
flags.add(flag);
|
|
579
|
+
else if (kind === 'test' && flag !== 'sequential')
|
|
580
|
+
flags.delete(flag);
|
|
581
|
+
}
|
|
582
|
+
if (read.skip && kind === 'test') {
|
|
583
|
+
flags.delete('skip');
|
|
584
|
+
conditions = [read.skip];
|
|
585
|
+
}
|
|
586
|
+
else if (read.skip) {
|
|
587
|
+
conditions.push(read.skip);
|
|
588
|
+
}
|
|
589
|
+
// Vitest passes a describe's options down to the tests and describes inside, where their own options and `only`
|
|
590
|
+
// win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
|
|
591
|
+
const line = this.file.source.line(call.start);
|
|
592
|
+
const around = context.passedDown;
|
|
593
|
+
const passed = read.skip ? null : around;
|
|
594
|
+
const skips = passed?.condition.applies === 'always' && !flags.has('only');
|
|
522
595
|
const table = links.find((link) => TABLES.has(link.name)) ?? null;
|
|
523
596
|
const isTodo = flags.has('todo') || body === undefined;
|
|
524
|
-
const states = this.states(flags, conditions, isTodo,
|
|
525
|
-
|
|
526
|
-
|
|
597
|
+
const states = this.states(flags, conditions, isTodo && !skips, line);
|
|
598
|
+
const skipped = () => states.some((setting) => setting.state === 'skip' && !setting.condition);
|
|
599
|
+
// The skip is set where the describe that passes it down sets it. A skipped describe around already skips what is
|
|
600
|
+
// inside, as it does when its own `only` beats its skip; a todo describe gets the skip itself.
|
|
601
|
+
if (skips && !skipped() && (!passed.covered || (kind === 'describe' && isTodo))) {
|
|
602
|
+
states.push({ state: 'skip', condition: null, reason: null, hasDynamicReason: false, line: passed.line });
|
|
603
|
+
}
|
|
604
|
+
// The chain's mode wins over the options'. `{ concurrent: false }` is how Vitest 5 turns parallel off, in place of
|
|
605
|
+
// `sequential`.
|
|
606
|
+
const parallel = chain.has('concurrent') || (!chain.has('sequential') && read.concurrent === true);
|
|
527
607
|
const mode = parallel
|
|
528
608
|
? 'parallel'
|
|
529
609
|
: flags.has('sequential') || read.concurrent === false
|
|
@@ -541,18 +621,20 @@ export class VitestAdapter {
|
|
|
541
621
|
caseCount: table ? this.caseCount(call, table, context, problems) : null,
|
|
542
622
|
problems,
|
|
543
623
|
};
|
|
624
|
+
let declared;
|
|
544
625
|
if (kind === 'describe') {
|
|
545
626
|
// A table's cases are passed instead.
|
|
546
627
|
const first = table ? null : fn?.params.at(0);
|
|
547
628
|
if (fn && first?.type === 'Identifier')
|
|
548
629
|
this.suiteParameters.set(fn, first.name);
|
|
549
|
-
|
|
630
|
+
const own = read.skip ? { condition: read.skip, line } : around;
|
|
631
|
+
const passesDown = own && { ...own, covered: (around?.covered ?? false) || skipped() };
|
|
632
|
+
declared = { kind: 'describe', ...base, isShuffled: flags.has('shuffle'), passesDown };
|
|
550
633
|
}
|
|
551
|
-
|
|
552
|
-
kind: 'test',
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
};
|
|
634
|
+
else {
|
|
635
|
+
declared = { kind: 'test', ...base, testContext: skipNames(fn, table?.name === 'for' ? 1 : table ? null : 0) };
|
|
636
|
+
}
|
|
637
|
+
return refusal ? refused(call, refusal, context, () => declared) : declared;
|
|
556
638
|
}
|
|
557
639
|
/** Whether an argument holds options: an object, a name for one, or anything but a function before the body. */
|
|
558
640
|
isOptions(node, next, context) {
|
|
@@ -600,9 +682,9 @@ export class VitestAdapter {
|
|
|
600
682
|
readDeclarationOptions(node, context, problems) {
|
|
601
683
|
const details = { tags: [], annotations: [], locks: [], meta: null };
|
|
602
684
|
const result = {
|
|
603
|
-
flags:
|
|
685
|
+
flags: new Map(),
|
|
604
686
|
concurrent: null,
|
|
605
|
-
|
|
687
|
+
skip: null,
|
|
606
688
|
details,
|
|
607
689
|
};
|
|
608
690
|
if (!node)
|
|
@@ -618,15 +700,15 @@ export class VitestAdapter {
|
|
|
618
700
|
else if (option?.resolved.ok && flag === 'concurrent') {
|
|
619
701
|
result.concurrent = Boolean(option.resolved.value);
|
|
620
702
|
}
|
|
621
|
-
else if (option?.resolved.ok
|
|
622
|
-
result.flags.
|
|
703
|
+
else if (option?.resolved.ok) {
|
|
704
|
+
result.flags.set(flag, Boolean(option.resolved.value));
|
|
623
705
|
}
|
|
624
706
|
}
|
|
625
707
|
const skip = options.values.get('skip');
|
|
626
708
|
if (skip) {
|
|
627
|
-
result.
|
|
709
|
+
result.skip = skip.resolved.ok
|
|
628
710
|
? { applies: skip.resolved.value ? 'always' : 'never', text: null }
|
|
629
|
-
: { applies: 'sometimes', text: snippet(text(this.file, skip.node)) }
|
|
711
|
+
: { applies: 'sometimes', text: snippet(text(this.file, skip.node)) };
|
|
630
712
|
}
|
|
631
713
|
const tags = options.values.get('tags');
|
|
632
714
|
if (tags && atLeast(this.file.version, 4, 1)) {
|
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, Framework, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
|
package/dist/index.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,20 @@ 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 suite of inventory.suites)
|
|
52
|
+
await line('suite', suite);
|
|
53
|
+
for (const test of inventory.tests)
|
|
54
|
+
await line('test', test);
|
|
55
|
+
for (const diagnostic of inventory.diagnostics)
|
|
56
|
+
await line('diagnostic', diagnostic);
|
|
57
|
+
await line('summary', inventory.summary);
|
|
58
|
+
await flush();
|
|
38
59
|
}
|
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CallExpression, Node } from 'oxc-parser';
|
|
1
|
+
import type { CallExpression, Expression, Node } from 'oxc-parser';
|
|
2
2
|
import type { FunctionNode } from './ast.ts';
|
|
3
3
|
import type { Annotation, DiagnosticCode, DiagnosticLevel, MetaValue } from './types.ts';
|
|
4
4
|
export type State = 'skip' | 'fixme' | 'todo' | 'expectedToFail';
|
|
@@ -12,6 +12,11 @@ export interface StateSetting {
|
|
|
12
12
|
/** The reason could not be worked out to a string, so `reason` holds its source text. */
|
|
13
13
|
hasDynamicReason: boolean;
|
|
14
14
|
line: number;
|
|
15
|
+
/**
|
|
16
|
+
* Set by code in a test or hook body, which runs only when the test does: a state set while the file loads, on the
|
|
17
|
+
* test or a describe around it, keeps the test from getting there.
|
|
18
|
+
*/
|
|
19
|
+
afterLoad?: boolean;
|
|
15
20
|
}
|
|
16
21
|
export interface Span {
|
|
17
22
|
start: number;
|
|
@@ -64,6 +69,8 @@ export interface TestDeclaration extends Declaration {
|
|
|
64
69
|
export interface SuiteDeclaration extends Declaration {
|
|
65
70
|
kind: 'describe';
|
|
66
71
|
isShuffled: boolean;
|
|
72
|
+
/** What the describe passes down to the calls inside it, such as Vitest's `skip` option; null for nothing. */
|
|
73
|
+
passesDown: unknown;
|
|
67
74
|
}
|
|
68
75
|
export interface Condition {
|
|
69
76
|
/** `always` and `never` are conditions that could be worked out from the file. */
|
|
@@ -92,9 +99,15 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
92
99
|
kind: 'annotations';
|
|
93
100
|
items: Annotation[];
|
|
94
101
|
problems: Problem[];
|
|
95
|
-
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A call the runner refuses. When it is shaped like a declaration, as `describe.sequential('title', fn)` is in Vitest
|
|
105
|
+
* 5, `declaration` is what it would declare, so that its tests are listed as not loaded rather than missing.
|
|
106
|
+
*/
|
|
107
|
+
| {
|
|
96
108
|
kind: 'invalid';
|
|
97
109
|
problem: Problem;
|
|
110
|
+
declaration?: TestDeclaration | SuiteDeclaration;
|
|
98
111
|
} | {
|
|
99
112
|
kind: 'other';
|
|
100
113
|
};
|
|
@@ -105,6 +118,27 @@ export interface CallContext {
|
|
|
105
118
|
inTest: boolean;
|
|
106
119
|
/** Inside a test or hook body, where calls don't declare tests. */
|
|
107
120
|
inBody: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* What the describe the call is registered in passes down, as the framework returned it: the describe around a
|
|
123
|
+
* helper's call, not where the helper is written. Null outside any describe.
|
|
124
|
+
*/
|
|
125
|
+
passedDown: unknown;
|
|
126
|
+
/**
|
|
127
|
+
* What a function declared in the file, or imported from a local module, returns, for a call on its result such as
|
|
128
|
+
* `conditionalTest({ min: 22 })('title', fn)`; null when the name is not a function the scan can find.
|
|
129
|
+
*/
|
|
130
|
+
returnsOf(name: string, scopes: readonly Node[]): Returns | null;
|
|
131
|
+
}
|
|
132
|
+
/** The values a function returns, read by the adapter of the file that writes the function. */
|
|
133
|
+
export interface Returns {
|
|
134
|
+
adapter: Adapter;
|
|
135
|
+
/** Each value with the scopes around it; null when the function may return no value, or returns a promise. */
|
|
136
|
+
values: {
|
|
137
|
+
expression: Expression;
|
|
138
|
+
scopes: readonly Node[];
|
|
139
|
+
}[] | null;
|
|
140
|
+
/** What the functions the values call return, looked up in the file that writes the function. */
|
|
141
|
+
returnsOf: CallContext['returnsOf'];
|
|
108
142
|
}
|
|
109
143
|
/** A module read to see whether it exports a custom `test` object. */
|
|
110
144
|
export interface ModuleSource {
|
|
@@ -124,5 +158,10 @@ export interface Adapter {
|
|
|
124
158
|
fileTags: string[];
|
|
125
159
|
usesGlobals: boolean;
|
|
126
160
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
161
|
+
/**
|
|
162
|
+
* The test or describe function a value holds, in the adapter's own terms, as `it.skip` in `return it.skip`; null
|
|
163
|
+
* when it holds none. A function the value calls is followed to what it returns through `returnsOf`.
|
|
164
|
+
*/
|
|
165
|
+
testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): unknown;
|
|
127
166
|
}
|
|
128
167
|
export {};
|
package/dist/options.d.ts
CHANGED
|
@@ -3,12 +3,15 @@ import type { Framework } from './types.ts';
|
|
|
3
3
|
export type { CommentsMode };
|
|
4
4
|
/** @inline */
|
|
5
5
|
export interface ScanOptions {
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Glob patterns for the test files, relative to `root`. As in Playwright and Vitest, they match names that start with
|
|
8
|
+
* a dot too.
|
|
9
|
+
*/
|
|
7
10
|
patterns: string[];
|
|
8
11
|
framework: Framework;
|
|
9
12
|
/** The project root. Paths in the output are relative to it. Defaults to the current directory. */
|
|
10
13
|
root?: string;
|
|
11
|
-
/** Glob patterns to leave out, in addition to `node_modules`. */
|
|
14
|
+
/** Glob patterns to leave out, in addition to `node_modules` and `.git`. */
|
|
12
15
|
ignore?: string[];
|
|
13
16
|
/** The framework version whose rules apply, instead of the one read from the project. */
|
|
14
17
|
frameworkVersion?: string;
|
package/dist/records.js
CHANGED
|
@@ -2,12 +2,14 @@ import { REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
|
|
|
2
2
|
import { problem } from "./frameworks/common.js";
|
|
3
3
|
import { hashNodes, shortHash } from "./hash.js";
|
|
4
4
|
/**
|
|
5
|
-
* The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo
|
|
6
|
-
*
|
|
5
|
+
* The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo wins
|
|
6
|
+
* over an unconditional expected failure, since the test doesn't run, and one set while the file loads wins over one
|
|
7
|
+
* in a test or hook body, which then never runs; then the nearest conditional state.
|
|
7
8
|
*/
|
|
8
9
|
function chooseState(candidates) {
|
|
9
10
|
const always = candidates.filter((candidate) => candidate.setting.condition === null);
|
|
10
|
-
|
|
11
|
+
const stops = always.filter((candidate) => candidate.setting.state !== 'expectedToFail');
|
|
12
|
+
return (stops.find((candidate) => !candidate.setting.afterLoad) ?? stops.at(0) ?? always.at(0) ?? candidates.at(0) ?? null);
|
|
11
13
|
}
|
|
12
14
|
/** The scope and the scopes around it, innermost first. */
|
|
13
15
|
function outward(scope) {
|