test-inventory 0.3.0 → 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 +10 -0
- package/README.md +6 -0
- package/dist/diagnostics.js +2 -0
- package/dist/extract.d.ts +26 -2
- package/dist/extract.js +178 -7
- package/dist/filter.d.ts +2 -1
- package/dist/filter.js +23 -7
- package/dist/frameworks/common.d.ts +4 -0
- package/dist/frameworks/common.js +22 -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 +137 -20
- 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/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 +95 -3
- package/package.json +1 -1
- package/schema/v1.json +326 -2
|
@@ -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, 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";
|
|
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,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,7 +327,7 @@ export class VitestAdapter {
|
|
|
307
327
|
if (!resolved)
|
|
308
328
|
return null;
|
|
309
329
|
const { kind, links } = resolved;
|
|
310
|
-
if (kind === 'test' && links.length === 1 &&
|
|
330
|
+
if (kind === 'test' && links.length === 1 && isHook(links[0].name))
|
|
311
331
|
return this.hook(call, links[0].name);
|
|
312
332
|
if (links.length === 1 && DERIVATIONS.has(links[0].name))
|
|
313
333
|
return { kind: 'other' };
|
|
@@ -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
517
|
return hook ? this.hook(call, hook) : null;
|
|
498
518
|
}
|
|
499
519
|
hook(call, name) {
|
|
500
520
|
const fn = findFunction(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,
|
package/dist/scan-file.d.ts
CHANGED
|
@@ -13,5 +13,7 @@ export interface FileSettings {
|
|
|
13
13
|
export interface FileResult extends FileRecords {
|
|
14
14
|
usesGlobals: boolean;
|
|
15
15
|
}
|
|
16
|
+
/** The records of a file the scan read no tests from. */
|
|
17
|
+
export declare function noRecords(relativeFilePath: string): Omit<FileRecords, 'diagnostics'>;
|
|
16
18
|
/** Reads one file: parse, extract, build records, and drop the AST. Never throws. */
|
|
17
19
|
export declare function scanFile(code: string, relativeFilePath: string, settings: FileSettings): FileResult;
|
package/dist/scan-file.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { fileDiagnostic, toDiagnostic } from "./diagnostics.js";
|
|
2
|
-
import { extract } from "./extract.js";
|
|
2
|
+
import { extract, noHooks } from "./extract.js";
|
|
3
3
|
import { parseModule } from "./frameworks/bindings.js";
|
|
4
|
-
import { problem } from "./frameworks/common.js";
|
|
4
|
+
import { atLeast, problem } from "./frameworks/common.js";
|
|
5
5
|
import { PlaywrightAdapter } from "./frameworks/playwright.js";
|
|
6
6
|
import { VitestAdapter } from "./frameworks/vitest.js";
|
|
7
7
|
import { parseFile } from "./parse.js";
|
|
8
|
-
import { buildRecords } from "./records.js";
|
|
8
|
+
import { buildRecords, fileRecord } from "./records.js";
|
|
9
9
|
import { snippet } from "./source.js";
|
|
10
10
|
import { Values } from "./values.js";
|
|
11
11
|
const TYPE_TEST = /\.(test|spec)-d\.[cm]?[jt]sx?$/;
|
|
12
|
+
const BENCHMARK = /\.(bench|benchmark)\.[cm]?[jt]sx?$/;
|
|
12
13
|
/** A file read for the scan: the scanned file, or a module a helper function is imported from. */
|
|
13
14
|
function frame(parsed, relativeFilePath, settings, readModule) {
|
|
14
15
|
const file = { source: parsed.source, values: new Values(), version: settings.version };
|
|
@@ -17,6 +18,10 @@ function frame(parsed, relativeFilePath, settings, readModule) {
|
|
|
17
18
|
: new VitestAdapter(parsed, file, readModule);
|
|
18
19
|
return { parsed, adapter, values: file.values, readModule, relativeFilePath };
|
|
19
20
|
}
|
|
21
|
+
/** The records of a file the scan read no tests from. */
|
|
22
|
+
export function noRecords(relativeFilePath) {
|
|
23
|
+
return { file: fileRecord(relativeFilePath, noHooks(), { suites: [], tests: [] }), suites: [], tests: [] };
|
|
24
|
+
}
|
|
20
25
|
/** Reads one file: parse, extract, build records, and drop the AST. Never throws. */
|
|
21
26
|
export function scanFile(code, relativeFilePath, settings) {
|
|
22
27
|
try {
|
|
@@ -27,7 +32,7 @@ export function scanFile(code, relativeFilePath, settings) {
|
|
|
27
32
|
diagnostics.push(toDiagnostic(problem('parse-error', { start, end }, `The file could not be parsed: ${message}`, snippet(parsed.source.slice(start, end)) || null), relativeFilePath, parsed.source));
|
|
28
33
|
// The parser keeps what it could read after some errors; after others it returns nothing.
|
|
29
34
|
if (parsed.program.body.length === 0)
|
|
30
|
-
return {
|
|
35
|
+
return { ...noRecords(relativeFilePath), diagnostics, usesGlobals: false };
|
|
31
36
|
}
|
|
32
37
|
const main = frame(parsed, relativeFilePath, settings, settings.readModule);
|
|
33
38
|
const extraction = extract(main, (module) => {
|
|
@@ -39,6 +44,8 @@ export function scanFile(code, relativeFilePath, settings) {
|
|
|
39
44
|
framework: settings.framework,
|
|
40
45
|
comments: settings.comments,
|
|
41
46
|
isTypeTest: settings.framework === 'vitest' && TYPE_TEST.test(relativeFilePath),
|
|
47
|
+
// Vitest 5's default `benchmark.include`.
|
|
48
|
+
isBenchmarkFile: settings.framework === 'vitest' && atLeast(settings.version, 5, 0) && BENCHMARK.test(relativeFilePath),
|
|
42
49
|
});
|
|
43
50
|
return {
|
|
44
51
|
...records,
|
|
@@ -50,8 +57,7 @@ export function scanFile(code, relativeFilePath, settings) {
|
|
|
50
57
|
const raw = error instanceof Error ? error.message : String(error);
|
|
51
58
|
const message = settings.root === null ? raw : raw.replaceAll(settings.root, '.');
|
|
52
59
|
return {
|
|
53
|
-
|
|
54
|
-
tests: [],
|
|
60
|
+
...noRecords(relativeFilePath),
|
|
55
61
|
diagnostics: [
|
|
56
62
|
fileDiagnostic('internal-error', relativeFilePath, `Reading the file failed unexpectedly: ${message}`),
|
|
57
63
|
],
|
package/dist/scan.js
CHANGED
|
@@ -7,7 +7,7 @@ import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
|
|
|
7
7
|
import { createInventory } from "./inventory.js";
|
|
8
8
|
import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, } from "./options.js";
|
|
9
9
|
import { moduleResolver } from "./resolve.js";
|
|
10
|
-
import { scanFile } from "./scan-file.js";
|
|
10
|
+
import { noRecords, scanFile } from "./scan-file.js";
|
|
11
11
|
import { globalsCheck } from "./vitest-config.js";
|
|
12
12
|
function versionDiagnostics(framework, version, source, range) {
|
|
13
13
|
const name = framework === 'playwright' ? 'Playwright' : 'Vitest';
|
|
@@ -86,8 +86,7 @@ export async function scan(options) {
|
|
|
86
86
|
}
|
|
87
87
|
catch (error) {
|
|
88
88
|
results.push({
|
|
89
|
-
|
|
90
|
-
tests: [],
|
|
89
|
+
...noRecords(relativeFilePath),
|
|
91
90
|
diagnostics: [
|
|
92
91
|
fileDiagnostic('read-error', relativeFilePath, `The file could not be read: ${readFailure(error)}.`),
|
|
93
92
|
],
|
package/dist/summary.js
CHANGED
|
@@ -27,6 +27,7 @@ export function summarize(fileCount, suites, tests, diagnostics) {
|
|
|
27
27
|
inConditionCount: count('isInCondition'),
|
|
28
28
|
inFunctionCount: count('isInFunction'),
|
|
29
29
|
typeTestCount: count('isTypeTest'),
|
|
30
|
+
benchmarkCount: count('isBenchmark'),
|
|
30
31
|
diagnosticCount: diagnostics.length,
|
|
31
32
|
tagCounts: countBy(tests, (test) => test.tags),
|
|
32
33
|
annotationCounts: countBy(tests, (test) => test.annotations.map((annotation) => annotation.type)),
|