test-inventory 0.3.0 → 0.4.1
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 +25 -0
- package/README.md +6 -0
- package/dist/diagnostics.js +2 -0
- package/dist/extract.d.ts +26 -2
- package/dist/extract.js +186 -8
- package/dist/filter.d.ts +2 -1
- package/dist/filter.js +23 -7
- package/dist/frameworks/common.d.ts +6 -0
- package/dist/frameworks/common.js +26 -1
- package/dist/frameworks/playwright.d.ts +4 -0
- package/dist/frameworks/playwright.js +54 -7
- package/dist/frameworks/vitest.d.ts +7 -0
- package/dist/frameworks/vitest.js +141 -24
- package/dist/index.d.ts +1 -1
- package/dist/inventory.js +1 -0
- package/dist/json.js +2 -0
- package/dist/model.d.ts +39 -2
- package/dist/records.d.ts +7 -2
- package/dist/records.js +69 -1
- package/dist/resolve.js +3 -3
- package/dist/scan-file.d.ts +2 -0
- package/dist/scan-file.js +12 -6
- package/dist/scan.js +7 -4
- package/dist/summary.js +1 -0
- package/dist/types.d.ts +95 -3
- package/package.json +1 -1
- 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
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, readCondition, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
|
|
4
|
+
import { atLeast, findFunction, hookFunction, 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,8 @@ 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']);
|
|
27
29
|
/** First members of chains that never declare a test, even when Playwright refuses the rest of the chain. */
|
|
28
30
|
const NOT_DECLARING = new Set(['step', 'abort', ...HOOKS, ...OTHER_MEMBERS]);
|
|
29
31
|
/** First members that show a name imported from a local file is used as Playwright's `test`. */
|
|
@@ -238,6 +240,9 @@ export class PlaywrightAdapter {
|
|
|
238
240
|
}
|
|
239
241
|
if (names[0] === 'info')
|
|
240
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
|
+
}
|
|
241
246
|
if (names[0] === 'expect' || names[0] === 'step' || key === 'abort')
|
|
242
247
|
return { kind: 'other' };
|
|
243
248
|
if (links.some((link) => link.args ?? link.template)) {
|
|
@@ -251,8 +256,12 @@ export class PlaywrightAdapter {
|
|
|
251
256
|
return this.describeCall(call, describe, context);
|
|
252
257
|
if (key === 'describe.configure')
|
|
253
258
|
return this.configure(call, context);
|
|
254
|
-
if (HOOKS.has(key))
|
|
255
|
-
|
|
259
|
+
if (HOOKS.has(key)) {
|
|
260
|
+
const fn = hookFunction(this.file, context.scopes, 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);
|
|
256
265
|
if (OTHER_MEMBERS.has(key))
|
|
257
266
|
return { kind: 'other' };
|
|
258
267
|
return this.invalid(call, names, "is not part of Playwright's API", context);
|
|
@@ -451,6 +460,7 @@ export class PlaywrightAdapter {
|
|
|
451
460
|
problems.push(titleProblem);
|
|
452
461
|
const line = this.file.source.line(call.start);
|
|
453
462
|
const fn = isFunctionNode(last) ? last : null;
|
|
463
|
+
const hashed = fn ?? namedFunction(this.file, context.scopes, last);
|
|
454
464
|
return {
|
|
455
465
|
kind: 'test',
|
|
456
466
|
title,
|
|
@@ -459,13 +469,23 @@ export class PlaywrightAdapter {
|
|
|
459
469
|
mode: null,
|
|
460
470
|
details: this.details(args.length === 3 ? args[1] : undefined, context, problems, title.text),
|
|
461
471
|
fn,
|
|
462
|
-
hashed
|
|
472
|
+
hashed,
|
|
463
473
|
isParameterized: false,
|
|
464
474
|
caseCount: null,
|
|
465
475
|
testContext: null,
|
|
476
|
+
fixtures: destructuredNames(hashed, 0),
|
|
477
|
+
usesBench: false,
|
|
466
478
|
problems,
|
|
467
479
|
};
|
|
468
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
|
+
}
|
|
469
489
|
modifier(call, state, context) {
|
|
470
490
|
const [condition, description] = [call.arguments.at(0), call.arguments.at(1)];
|
|
471
491
|
const problems = [];
|
|
@@ -542,10 +562,37 @@ export class PlaywrightAdapter {
|
|
|
542
562
|
problems.push(unresolvedOption(this.file, 'lock', unresolved, importedFrom, 'the lock is missing'));
|
|
543
563
|
}
|
|
544
564
|
}
|
|
545
|
-
|
|
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) {
|
|
546
575
|
return problems.length > 0 ? { kind: 'invalid', problem: problems[0] } : { kind: 'other' };
|
|
547
576
|
}
|
|
548
|
-
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 };
|
|
549
596
|
}
|
|
550
597
|
/** `test.info().annotations.push(...)` inside a test body. */
|
|
551
598
|
annotationsPush(call, links, context) {
|
|
@@ -561,7 +608,7 @@ export class PlaywrightAdapter {
|
|
|
561
608
|
}
|
|
562
609
|
/** Reads the `{ tag, annotation, lock }` details of a test or describe. */
|
|
563
610
|
details(node, context, problems, title) {
|
|
564
|
-
const details = { tags: titleTags(title), annotations: [], locks: [], meta: null };
|
|
611
|
+
const details = { tags: titleTags(title), annotations: [], locks: [], meta: null, run: {} };
|
|
565
612
|
if (!node)
|
|
566
613
|
return details;
|
|
567
614
|
const options = readOptions(this.file, context.scopes, node);
|
|
@@ -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;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
3
|
import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
|
|
4
|
-
import { atLeast,
|
|
4
|
+
import { atLeast, hookFunction, 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,9 @@ const HOOKS = new Map([
|
|
|
37
37
|
['afterAll', null],
|
|
38
38
|
['aroundAll', null],
|
|
39
39
|
]);
|
|
40
|
+
function isHook(name) {
|
|
41
|
+
return HOOKS.has(name);
|
|
42
|
+
}
|
|
40
43
|
/** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
|
|
41
44
|
const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
|
|
42
45
|
/** First members that show a name imported from a local file is used as Vitest's `test`. */
|
|
@@ -91,6 +94,23 @@ function hoistedValue(call, parents) {
|
|
|
91
94
|
const parent = parents.at(-1);
|
|
92
95
|
return parent?.type === 'AwaitExpression' ? parent : call;
|
|
93
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
|
+
}
|
|
94
114
|
/** The names a file binds to Vitest's `test`, `describe`, hooks and namespace. */
|
|
95
115
|
class VitestBindings {
|
|
96
116
|
kinds = new Map();
|
|
@@ -202,7 +222,7 @@ class VitestBindings {
|
|
|
202
222
|
if (kind) {
|
|
203
223
|
this.kinds.set(local, kind);
|
|
204
224
|
}
|
|
205
|
-
else if (
|
|
225
|
+
else if (isHook(imported)) {
|
|
206
226
|
this.hooks.set(local, imported);
|
|
207
227
|
}
|
|
208
228
|
else if (imported === 'bench' && atLeast(version, 5, 0)) {
|
|
@@ -227,7 +247,7 @@ class VitestBindings {
|
|
|
227
247
|
const kind = KINDS.get(key ?? '');
|
|
228
248
|
if (kind)
|
|
229
249
|
this.kinds.set(property.value.name, kind);
|
|
230
|
-
if (key !== null &&
|
|
250
|
+
if (key !== null && isHook(key))
|
|
231
251
|
this.hooks.set(property.value.name, key);
|
|
232
252
|
}
|
|
233
253
|
return true;
|
|
@@ -307,8 +327,8 @@ export class VitestAdapter {
|
|
|
307
327
|
if (!resolved)
|
|
308
328
|
return null;
|
|
309
329
|
const { kind, links } = resolved;
|
|
310
|
-
if (kind === 'test' && links.length === 1 &&
|
|
311
|
-
return this.hook(call, links[0].name);
|
|
330
|
+
if (kind === 'test' && links.length === 1 && isHook(links[0].name))
|
|
331
|
+
return this.hook(call, links[0].name, context);
|
|
312
332
|
if (links.length === 1 && DERIVATIONS.has(links[0].name))
|
|
313
333
|
return { kind: 'other' };
|
|
314
334
|
const last = links.at(-1);
|
|
@@ -492,27 +512,28 @@ export class VitestAdapter {
|
|
|
492
512
|
return null;
|
|
493
513
|
const declaration = this.file.values.declarationOf(callee.name, context.scopes);
|
|
494
514
|
const hook = declaration === null
|
|
495
|
-
?
|
|
515
|
+
? isHook(callee.name) && callee.name
|
|
496
516
|
: this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
|
|
497
|
-
return hook ? this.hook(call, hook) : null;
|
|
517
|
+
return hook ? this.hook(call, hook, context) : null;
|
|
498
518
|
}
|
|
499
|
-
hook(call, name) {
|
|
500
|
-
const fn =
|
|
519
|
+
hook(call, name, context) {
|
|
520
|
+
const fn = hookFunction(this.file, context.scopes, call.arguments);
|
|
501
521
|
const index = HOOKS.get(name);
|
|
502
|
-
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
|
+
};
|
|
503
529
|
}
|
|
504
530
|
contextSkip(call, context) {
|
|
505
531
|
const names = context.testContext;
|
|
506
532
|
if (!names)
|
|
507
533
|
return null;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
: callee.type === 'MemberExpression' &&
|
|
512
|
-
propertyName(callee) === 'skip' &&
|
|
513
|
-
callee.object.type === 'Identifier' &&
|
|
514
|
-
names.objects.has(callee.object.name);
|
|
515
|
-
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'))
|
|
516
537
|
return null;
|
|
517
538
|
const [first, second] = call.arguments;
|
|
518
539
|
// skip(), skip(note) or skip(condition, note); before Vitest 3.1 the first argument is always the note.
|
|
@@ -527,6 +548,29 @@ export class VitestAdapter {
|
|
|
527
548
|
problems: reason.problem ? [reason.problem] : [],
|
|
528
549
|
};
|
|
529
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
|
+
}
|
|
530
574
|
declaration(call, kind, links, context) {
|
|
531
575
|
const args = call.arguments;
|
|
532
576
|
const callee = snippet(text(this.file, call.callee));
|
|
@@ -548,6 +592,7 @@ export class VitestAdapter {
|
|
|
548
592
|
// (title, fn, timeout), (title, options, fn) or the removed (title, fn, options)
|
|
549
593
|
let options;
|
|
550
594
|
let body;
|
|
595
|
+
let timeout;
|
|
551
596
|
let refusal = null;
|
|
552
597
|
const [second, third] = [args.at(1), args.at(2)];
|
|
553
598
|
if (args.some((arg) => arg.type === 'SpreadElement')) {
|
|
@@ -565,9 +610,23 @@ export class VitestAdapter {
|
|
|
565
610
|
}
|
|
566
611
|
options = third;
|
|
567
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
|
+
}
|
|
568
620
|
}
|
|
569
621
|
const fn = isFunctionNode(body) ? body : null;
|
|
570
622
|
const read = this.readDeclarationOptions(options, context, problems);
|
|
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
|
+
}
|
|
571
630
|
const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
|
|
572
631
|
const flags = new Set(chain);
|
|
573
632
|
let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
|
|
@@ -632,7 +691,14 @@ export class VitestAdapter {
|
|
|
632
691
|
declared = { kind: 'describe', ...base, isShuffled: flags.has('shuffle'), passesDown };
|
|
633
692
|
}
|
|
634
693
|
else {
|
|
635
|
-
|
|
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
|
+
};
|
|
636
702
|
}
|
|
637
703
|
return refusal ? refused(call, refusal, context, () => declared) : declared;
|
|
638
704
|
}
|
|
@@ -680,7 +746,7 @@ export class VitestAdapter {
|
|
|
680
746
|
return fails;
|
|
681
747
|
}
|
|
682
748
|
readDeclarationOptions(node, context, problems) {
|
|
683
|
-
const details = { tags: [], annotations: [], locks: [], meta: null };
|
|
749
|
+
const details = { tags: [], annotations: [], locks: [], meta: null, run: {} };
|
|
684
750
|
const result = {
|
|
685
751
|
flags: new Map(),
|
|
686
752
|
concurrent: null,
|
|
@@ -723,8 +789,46 @@ export class VitestAdapter {
|
|
|
723
789
|
const meta = options.values.get('meta');
|
|
724
790
|
if (meta)
|
|
725
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);
|
|
726
801
|
return result;
|
|
727
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
|
+
}
|
|
728
832
|
meta(option, context, problems) {
|
|
729
833
|
const entries = readOptions(this.file, context.scopes, option.node);
|
|
730
834
|
problems.push(...unresolvedOptions(this.file, entries, 'its meta values are missing'));
|
|
@@ -781,19 +885,32 @@ function templateRows(template) {
|
|
|
781
885
|
function isMetaValue(value) {
|
|
782
886
|
return value !== undefined;
|
|
783
887
|
}
|
|
784
|
-
function
|
|
888
|
+
function contextNames(fn, index) {
|
|
785
889
|
const parameter = index === null ? undefined : fn?.params[index];
|
|
786
890
|
if (!parameter)
|
|
787
891
|
return null;
|
|
788
|
-
const names = { objects: new Set(),
|
|
892
|
+
const names = { objects: new Set(), skip: new Set(), annotate: new Set() };
|
|
789
893
|
if (parameter.type === 'Identifier')
|
|
790
894
|
names.objects.add(parameter.name);
|
|
791
895
|
if (parameter.type === 'ObjectPattern') {
|
|
792
896
|
for (const property of parameter.properties) {
|
|
793
|
-
|
|
794
|
-
|
|
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);
|
|
795
902
|
}
|
|
796
903
|
}
|
|
797
904
|
}
|
|
798
905
|
return names;
|
|
799
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
|
@@ -3,4 +3,4 @@ export { scanSource } from './scan-source.ts';
|
|
|
3
3
|
export { filterInventory, pickTestFields } from './filter.ts';
|
|
4
4
|
export type { InventoryFilter, PickedInventory } from './filter.ts';
|
|
5
5
|
export type { CommentsMode, ScanOptions, ScanSourceOptions } from './options.ts';
|
|
6
|
-
export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, Framework, Inventory, InventoryLine, 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/inventory.js
CHANGED
package/dist/json.js
CHANGED
|
@@ -48,6 +48,8 @@ export async function writeInventoryLines(inventory, write) {
|
|
|
48
48
|
const line = (type, record) => emit(`${JSON.stringify({ type, ...record })}\n`);
|
|
49
49
|
const { schemaVersion, tool, framework, frameworkVersion } = inventory;
|
|
50
50
|
await line('inventory', { schemaVersion, tool, framework, frameworkVersion });
|
|
51
|
+
for (const file of inventory.files)
|
|
52
|
+
await line('file', file);
|
|
51
53
|
for (const suite of inventory.suites)
|
|
52
54
|
await line('suite', suite);
|
|
53
55
|
for (const test of inventory.tests)
|
package/dist/model.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CallExpression, Expression, Node } from 'oxc-parser';
|
|
2
2
|
import type { FunctionNode } from './ast.ts';
|
|
3
|
-
import type { Annotation, DiagnosticCode, DiagnosticLevel, MetaValue } from './types.ts';
|
|
3
|
+
import type { Annotation, DiagnosticCode, DiagnosticLevel, HookCounts, MetaValue } from './types.ts';
|
|
4
4
|
export type State = 'skip' | 'fixme' | 'todo' | 'expectedToFail';
|
|
5
5
|
export type Mode = 'parallel' | 'serial' | 'default';
|
|
6
6
|
/** A state set by a declaration, a describe or a call such as `test.skip(condition)`. */
|
|
@@ -45,6 +45,22 @@ export interface Details {
|
|
|
45
45
|
annotations: Annotation[];
|
|
46
46
|
locks: string[];
|
|
47
47
|
meta: Record<string, MetaValue> | null;
|
|
48
|
+
run: RunSettings;
|
|
49
|
+
}
|
|
50
|
+
/** Run settings written in one place; a key that is missing is not set there. */
|
|
51
|
+
export interface RunSettings {
|
|
52
|
+
timeout?: number;
|
|
53
|
+
/** A retry setting replaces the whole `retry` of the describes around, delay and condition included. */
|
|
54
|
+
retry?: {
|
|
55
|
+
count: number;
|
|
56
|
+
delay: number | null;
|
|
57
|
+
condition: string | null;
|
|
58
|
+
};
|
|
59
|
+
repeats?: number;
|
|
60
|
+
/** Playwright `test.slow()`: the conditions of the calls, any of which makes the test slow; null when one always does. */
|
|
61
|
+
slow?: {
|
|
62
|
+
conditions: string[] | null;
|
|
63
|
+
};
|
|
48
64
|
}
|
|
49
65
|
interface Declaration {
|
|
50
66
|
title: Title | null;
|
|
@@ -65,6 +81,10 @@ export interface TestDeclaration extends Declaration {
|
|
|
65
81
|
title: Title;
|
|
66
82
|
/** Framework-specific names through which the test body can change its own state, such as Vitest's `context.skip`. */
|
|
67
83
|
testContext: unknown;
|
|
84
|
+
/** Names the test function takes from its context, without the framework's own keys. */
|
|
85
|
+
fixtures: string[];
|
|
86
|
+
/** Uses Vitest 5's `bench` fixture. */
|
|
87
|
+
usesBench: boolean;
|
|
68
88
|
}
|
|
69
89
|
export interface SuiteDeclaration extends Declaration {
|
|
70
90
|
kind: 'describe';
|
|
@@ -85,16 +105,33 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
85
105
|
hasDynamicReason: boolean;
|
|
86
106
|
problems: Problem[];
|
|
87
107
|
}
|
|
88
|
-
/** `test.describe.configure()`: a mode, locks (Playwright 1.64),
|
|
108
|
+
/** `test.describe.configure()`: a mode, locks (Playwright 1.64), a timeout and retries. */
|
|
89
109
|
| {
|
|
90
110
|
kind: 'configure';
|
|
91
111
|
mode: Mode | null;
|
|
92
112
|
locks: string[];
|
|
113
|
+
run: RunSettings;
|
|
114
|
+
problems: Problem[];
|
|
115
|
+
}
|
|
116
|
+
/** Playwright `test.setTimeout()` or `test.slow()`, which apply where they are called. */
|
|
117
|
+
| {
|
|
118
|
+
kind: 'run';
|
|
119
|
+
settings: RunSettings;
|
|
120
|
+
/** Source text of the first argument. */ argument: string | null;
|
|
93
121
|
problems: Problem[];
|
|
94
122
|
} | {
|
|
95
123
|
kind: 'hook';
|
|
124
|
+
name: keyof HookCounts;
|
|
96
125
|
fn: FunctionNode | null;
|
|
97
126
|
testContext: unknown;
|
|
127
|
+
fixtures: string[];
|
|
128
|
+
}
|
|
129
|
+
/** Playwright `test.step()` or `test.step.skip()`. */
|
|
130
|
+
| {
|
|
131
|
+
kind: 'step';
|
|
132
|
+
title: Title;
|
|
133
|
+
isSkipped: boolean;
|
|
134
|
+
fn: FunctionNode | null;
|
|
98
135
|
} | {
|
|
99
136
|
kind: 'annotations';
|
|
100
137
|
items: Annotation[];
|
package/dist/records.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Extraction } from './extract.ts';
|
|
2
2
|
import type { ParsedFile } from './parse.ts';
|
|
3
|
-
import type { Diagnostic, Framework, SuiteRecord, TestRecord } from './types.ts';
|
|
3
|
+
import type { Diagnostic, FileRecord, Framework, HookCounts, SuiteRecord, TestRecord } from './types.ts';
|
|
4
4
|
/** @inline */
|
|
5
5
|
export type CommentsMode = 'all' | 'non-active' | 'none';
|
|
6
6
|
export interface FileRecords {
|
|
7
|
+
file: FileRecord;
|
|
7
8
|
suites: SuiteRecord[];
|
|
8
9
|
tests: TestRecord[];
|
|
9
10
|
diagnostics: Diagnostic[];
|
|
@@ -13,7 +14,11 @@ interface RecordOptions {
|
|
|
13
14
|
framework: Framework;
|
|
14
15
|
comments: CommentsMode;
|
|
15
16
|
isTypeTest: boolean;
|
|
17
|
+
/** A Vitest 5 benchmark file, such as `*.bench.ts`. */
|
|
18
|
+
isBenchmarkFile: boolean;
|
|
16
19
|
}
|
|
20
|
+
/** The record of a scanned file, from its records and the hooks at its top level. */
|
|
21
|
+
export declare function fileRecord(relativeFilePath: string, hooks: HookCounts, records: Pick<FileRecords, 'suites' | 'tests'>): FileRecord;
|
|
17
22
|
/** An error for each id used by more than one record. Ordinals keep ids apart, so only a hash collision causes one. */
|
|
18
23
|
export declare function duplicateIdDiagnostics(records: readonly {
|
|
19
24
|
id: string;
|
package/dist/records.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
|
|
2
|
+
import { anySlow, eitherCondition, noHooks, } from "./extract.js";
|
|
2
3
|
import { problem } from "./frameworks/common.js";
|
|
3
4
|
import { hashNodes, shortHash } from "./hash.js";
|
|
4
5
|
/**
|
|
@@ -29,6 +30,47 @@ function bodyNodes(fn) {
|
|
|
29
30
|
const body = fn.body;
|
|
30
31
|
return body.type === 'BlockStatement' ? body.body : [body];
|
|
31
32
|
}
|
|
33
|
+
function addHooks(scopes) {
|
|
34
|
+
const total = noHooks();
|
|
35
|
+
for (const scope of scopes) {
|
|
36
|
+
for (const name of Object.keys(total))
|
|
37
|
+
total[name] += scope.hooks[name];
|
|
38
|
+
}
|
|
39
|
+
return total;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The run settings that apply, from the places that set them, nearest first: the first that sets a key wins, and any
|
|
43
|
+
* `test.slow()` makes the test slow.
|
|
44
|
+
*/
|
|
45
|
+
function runKeys(places) {
|
|
46
|
+
const first = (key) => places.find((place) => place[key] !== undefined)?.[key];
|
|
47
|
+
const retry = first('retry');
|
|
48
|
+
const slows = places.flatMap((place) => place.slow ?? []);
|
|
49
|
+
return {
|
|
50
|
+
timeout: first('timeout') ?? null,
|
|
51
|
+
isSlow: slows.length > 0,
|
|
52
|
+
slowCondition: eitherCondition(anySlow(slows).conditions ?? []) || null,
|
|
53
|
+
retries: retry?.count ?? null,
|
|
54
|
+
retryDelay: retry?.delay ?? null,
|
|
55
|
+
retryCondition: retry?.condition ?? null,
|
|
56
|
+
repeats: first('repeats') ?? null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The record of a scanned file, from its records and the hooks at its top level. */
|
|
60
|
+
export function fileRecord(relativeFilePath, hooks, records) {
|
|
61
|
+
return {
|
|
62
|
+
relativeFilePath,
|
|
63
|
+
fileName: relativeFilePath.slice(relativeFilePath.lastIndexOf('/') + 1),
|
|
64
|
+
hooks,
|
|
65
|
+
suiteCount: records.suites.length,
|
|
66
|
+
testCount: records.tests.length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Whether a function's block holds no statements; comments are not statements. */
|
|
70
|
+
function hasNoStatements(fn) {
|
|
71
|
+
const body = fn?.body;
|
|
72
|
+
return body?.type === 'BlockStatement' && body.body.length === 0;
|
|
73
|
+
}
|
|
32
74
|
/** The number of times the runner repeats a test: its own cases times those of each describe table around it. */
|
|
33
75
|
function repetitions(counts) {
|
|
34
76
|
let product = 1;
|
|
@@ -71,7 +113,12 @@ class RecordBuilder {
|
|
|
71
113
|
this.diagnostics.push(...duplicateIdDiagnostics([...suites, ...tests], this.options.relativeFilePath));
|
|
72
114
|
// Diagnostics for the whole file have no line, which sorts as 0, before the others.
|
|
73
115
|
const diagnostics = this.diagnostics.sort((a, b) => Number(a.lineStart) - Number(b.lineStart) || Number(a.columnStart) - Number(b.columnStart));
|
|
74
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
file: fileRecord(this.options.relativeFilePath, extraction.hooks, { suites, tests }),
|
|
118
|
+
suites,
|
|
119
|
+
tests,
|
|
120
|
+
diagnostics,
|
|
121
|
+
};
|
|
75
122
|
}
|
|
76
123
|
diagnostic(item, testId = null) {
|
|
77
124
|
return toDiagnostic(item, this.options.relativeFilePath, this.parsed.source, testId);
|
|
@@ -136,6 +183,8 @@ class RecordBuilder {
|
|
|
136
183
|
annotations: scope.details.annotations,
|
|
137
184
|
locks: unique(scope.details.locks),
|
|
138
185
|
meta: scope.details.meta,
|
|
186
|
+
hooks: { ...scope.hooks },
|
|
187
|
+
...runKeys([scope.details.run]),
|
|
139
188
|
};
|
|
140
189
|
}
|
|
141
190
|
test(pending) {
|
|
@@ -172,6 +221,10 @@ class RecordBuilder {
|
|
|
172
221
|
const tables = [declaration, ...describes.map((scope) => suiteOf(scope).declaration)].filter((item) => item.isParameterized);
|
|
173
222
|
for (const item of pending.problems)
|
|
174
223
|
this.diagnostics.push(this.diagnostic(item, id));
|
|
224
|
+
if (declaration.usesBench && !this.options.isBenchmarkFile) {
|
|
225
|
+
const message = 'The test uses the bench fixture, which Vitest only provides in benchmark files such as *.bench.ts; elsewhere the test fails.';
|
|
226
|
+
this.diagnostics.push(this.diagnostic(problem('bench-outside-benchmark-file', located, message), id));
|
|
227
|
+
}
|
|
175
228
|
if (pending.isInLoop) {
|
|
176
229
|
this.diagnostics.push(this.diagnostic(problem('test-in-loop', located, 'The test is declared in a loop; it is listed once, while the runner creates one test per iteration.'), id));
|
|
177
230
|
}
|
|
@@ -221,6 +274,21 @@ class RecordBuilder {
|
|
|
221
274
|
...declaration.details.locks,
|
|
222
275
|
]),
|
|
223
276
|
meta: Object.keys(meta).length > 0 ? meta : null,
|
|
277
|
+
hooks: addHooks(scopes),
|
|
278
|
+
// Calls in the body and in `beforeEach` hooks run last, so they win over what the describes set.
|
|
279
|
+
...runKeys([
|
|
280
|
+
pending.run,
|
|
281
|
+
...scopes.map((scope) => scope.hookRun),
|
|
282
|
+
declaration.details.run,
|
|
283
|
+
...scopes.map((scope) => scope.details.run),
|
|
284
|
+
]),
|
|
285
|
+
fixtures: unique([...declaration.fixtures, ...[...scopes].reverse().flatMap((scope) => scope.hookFixtures)]),
|
|
286
|
+
expectCount: pending.expectCount,
|
|
287
|
+
expectedAssertions: pending.expectedAssertions,
|
|
288
|
+
expectsAssertions: pending.expectsAssertions,
|
|
289
|
+
hasEmptyBody: hasNoStatements(declaration.hashed),
|
|
290
|
+
steps: pending.steps,
|
|
291
|
+
isBenchmark: this.options.isBenchmarkFile || declaration.usesBench,
|
|
224
292
|
isParameterized: tables.length > 0,
|
|
225
293
|
caseCount: tables.length > 0 ? repetitions(tables.map((item) => item.caseCount)) : null,
|
|
226
294
|
isInLoop: pending.isInLoop,
|