test-inventory 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +9 -1
- package/dist/ast.d.ts +11 -0
- package/dist/ast.js +30 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +59 -12
- package/dist/diagnostics.js +2 -0
- package/dist/extract.d.ts +26 -2
- package/dist/extract.js +204 -48
- package/dist/filter.d.ts +38 -0
- package/dist/filter.js +169 -0
- package/dist/frameworks/bindings.d.ts +37 -6
- package/dist/frameworks/bindings.js +167 -33
- package/dist/frameworks/common.d.ts +10 -1
- package/dist/frameworks/common.js +47 -4
- package/dist/frameworks/playwright.d.ts +6 -1
- package/dist/frameworks/playwright.js +111 -32
- package/dist/frameworks/vitest.d.ts +8 -1
- package/dist/frameworks/vitest.js +245 -85
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/inventory.js +1 -0
- package/dist/json.d.ts +7 -1
- package/dist/json.js +32 -9
- package/dist/model.d.ts +62 -5
- package/dist/records.d.ts +7 -2
- package/dist/records.js +74 -4
- package/dist/scan-file.d.ts +2 -0
- package/dist/scan-file.js +12 -6
- package/dist/scan.js +2 -3
- package/dist/summary.js +1 -0
- package/dist/types.d.ts +109 -1
- package/package.json +2 -2
- package/schema/v1.json +326 -2
package/dist/extract.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { childNodes, isFunctionNode, isImportMetaVitest, propertyName, unwrap } from "./ast.js";
|
|
1
|
+
import { childNodes, isFunctionNode, isImportMetaVitest, propertyName, returnValues, unwrap, } from "./ast.js";
|
|
2
2
|
import { exportName } from "./frameworks/bindings.js";
|
|
3
|
-
import { problem } from "./frameworks/common.js";
|
|
3
|
+
import { problem, readCount } from "./frameworks/common.js";
|
|
4
4
|
import { snippet, splitLines } from "./source.js";
|
|
5
5
|
import { isScopeNode } from "./values.js";
|
|
6
6
|
/** How many helpers deep a call is followed into the functions it calls, far more than real suites nest. */
|
|
@@ -20,11 +20,49 @@ function calledWhereWritten(call, written) {
|
|
|
20
20
|
!call.inBody);
|
|
21
21
|
}
|
|
22
22
|
const LOOP_METHODS = new Set(['forEach', 'map']);
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* What `test.describe.configure()` sets on the describe or file it is called in: the last mode, timeout and retries
|
|
25
|
+
* win, locks add up.
|
|
26
|
+
*/
|
|
24
27
|
function configure(scope, settings) {
|
|
25
28
|
if (settings.mode)
|
|
26
29
|
scope.mode = settings.mode;
|
|
27
30
|
scope.details.locks.push(...settings.locks);
|
|
31
|
+
mergeRun(scope.details.run, settings.run);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Applies settings written later in the same place: the last value of each wins, and a test is slow when any
|
|
35
|
+
* `test.slow()` applies.
|
|
36
|
+
*/
|
|
37
|
+
export function mergeRun(target, later) {
|
|
38
|
+
const { slow, ...rest } = later;
|
|
39
|
+
Object.assign(target, rest);
|
|
40
|
+
if (slow)
|
|
41
|
+
target.slow = target.slow ? anySlow([target.slow, slow]) : slow;
|
|
42
|
+
}
|
|
43
|
+
/** Any of several `test.slow()` calls: always when one always applies, otherwise under any of their conditions. */
|
|
44
|
+
export function anySlow(slows) {
|
|
45
|
+
const lists = slows.map((slow) => slow.conditions);
|
|
46
|
+
if (lists.includes(null))
|
|
47
|
+
return { conditions: null };
|
|
48
|
+
return { conditions: [...new Set(lists.flat())] };
|
|
49
|
+
}
|
|
50
|
+
/** Conditions joined with `||`, adding parentheses where a part could otherwise be read differently. */
|
|
51
|
+
export function eitherCondition(parts) {
|
|
52
|
+
return parts.map((part) => (parts.length > 1 && /\?|,/.test(part) ? `(${part})` : part)).join(' || ');
|
|
53
|
+
}
|
|
54
|
+
/** A warning for a test or describe titled with an empty string, which reports can't tell apart. */
|
|
55
|
+
function emptyTitle(declaration, call) {
|
|
56
|
+
const { title } = declaration;
|
|
57
|
+
if (title?.text !== '' || title.isDynamic)
|
|
58
|
+
return [];
|
|
59
|
+
const what = declaration.kind === 'test' ? 'test' : 'describe';
|
|
60
|
+
return [problem('empty-title', call, `The ${what} has an empty title, so reports and filters can't tell it apart.`)];
|
|
61
|
+
}
|
|
62
|
+
/** Hooks that run around each test, whose fixtures the test uses too. */
|
|
63
|
+
const EACH_HOOKS = new Set(['beforeEach', 'afterEach', 'aroundEach']);
|
|
64
|
+
export function noHooks() {
|
|
65
|
+
return { beforeAll: 0, beforeEach: 0, afterEach: 0, afterAll: 0, aroundEach: 0, aroundAll: 0 };
|
|
28
66
|
}
|
|
29
67
|
/** Joins conditions with `&&`, adding parentheses where a part could otherwise be read differently. */
|
|
30
68
|
function joinConditions(parts) {
|
|
@@ -95,7 +133,10 @@ class Walker {
|
|
|
95
133
|
states: [],
|
|
96
134
|
mode: null,
|
|
97
135
|
isOnly: false,
|
|
98
|
-
details: { tags: adapter.fileTags, annotations: [], locks: [], meta: null },
|
|
136
|
+
details: { tags: adapter.fileTags, annotations: [], locks: [], meta: null, run: {} },
|
|
137
|
+
hooks: noHooks(),
|
|
138
|
+
hookRun: {},
|
|
139
|
+
hookFixtures: [],
|
|
99
140
|
suite: null,
|
|
100
141
|
};
|
|
101
142
|
}
|
|
@@ -104,6 +145,8 @@ class Walker {
|
|
|
104
145
|
scope: this.fileScope,
|
|
105
146
|
test: null,
|
|
106
147
|
hookScope: null,
|
|
148
|
+
hookName: null,
|
|
149
|
+
stepDepth: 0,
|
|
107
150
|
testContext: null,
|
|
108
151
|
guards: [],
|
|
109
152
|
isInLoop: false,
|
|
@@ -127,6 +170,7 @@ class Walker {
|
|
|
127
170
|
suites: this.suites,
|
|
128
171
|
tests: this.tests,
|
|
129
172
|
problems: [...this.main.adapter.problems, ...this.problems],
|
|
173
|
+
hooks: this.fileScope.hooks,
|
|
130
174
|
usesGlobals: this.main.adapter.usesGlobals,
|
|
131
175
|
};
|
|
132
176
|
}
|
|
@@ -147,6 +191,8 @@ class Walker {
|
|
|
147
191
|
visitNode(node, context) {
|
|
148
192
|
switch (node.type) {
|
|
149
193
|
case 'CallExpression': {
|
|
194
|
+
if (context.test && !context.quiet)
|
|
195
|
+
this.expectCall(node, context.test);
|
|
150
196
|
const helper = this.helperCall(node, context);
|
|
151
197
|
if (this.call(node, context) || this.loopCall(node, context))
|
|
152
198
|
return;
|
|
@@ -294,6 +340,7 @@ class Walker {
|
|
|
294
340
|
testContext: context.testContext,
|
|
295
341
|
inTest: context.test !== null,
|
|
296
342
|
inBody: context.test !== null || context.hookScope !== null,
|
|
343
|
+
passedDown: context.scope.suite?.declaration.passesDown ?? null,
|
|
297
344
|
returnsOf: (name, scopes) => this.returnsOf(name, scopes),
|
|
298
345
|
});
|
|
299
346
|
if (!api)
|
|
@@ -323,6 +370,7 @@ class Walker {
|
|
|
323
370
|
reason: api.reason,
|
|
324
371
|
hasDynamicReason: api.hasDynamicReason,
|
|
325
372
|
line: this.lineOf(call),
|
|
373
|
+
afterLoad: context.test !== null || context.hookScope !== null,
|
|
326
374
|
};
|
|
327
375
|
if (context.helper)
|
|
328
376
|
this.effectsOf(context.helper).push({ kind: 'state', setting, call });
|
|
@@ -340,13 +388,35 @@ class Walker {
|
|
|
340
388
|
configure(context.scope, api);
|
|
341
389
|
}
|
|
342
390
|
return true;
|
|
391
|
+
case 'run':
|
|
392
|
+
this.runSetting(call, api, context);
|
|
393
|
+
return true;
|
|
394
|
+
case 'step':
|
|
395
|
+
// The adapters only return steps from test bodies.
|
|
396
|
+
context.test.steps.push({
|
|
397
|
+
title: api.title.text,
|
|
398
|
+
hasDynamicTitle: api.title.isDynamic,
|
|
399
|
+
lineStart: this.lineOf(call),
|
|
400
|
+
depth: context.stepDepth,
|
|
401
|
+
isSkipped: api.isSkipped,
|
|
402
|
+
});
|
|
403
|
+
if (api.fn)
|
|
404
|
+
this.visitCallback(api.fn, { ...context, stepDepth: context.stepDepth + 1 });
|
|
405
|
+
return true;
|
|
343
406
|
case 'hook':
|
|
344
407
|
if (context.test)
|
|
345
408
|
return false;
|
|
409
|
+
// A function with a name registers its hooks where it is called, not where it is written.
|
|
410
|
+
if (!context.hookScope && context.declaredIn === null) {
|
|
411
|
+
context.scope.hooks[api.name] += 1;
|
|
412
|
+
if (EACH_HOOKS.has(api.name))
|
|
413
|
+
context.scope.hookFixtures.push(...api.fixtures);
|
|
414
|
+
}
|
|
346
415
|
if (api.fn) {
|
|
347
416
|
this.visitCallback(api.fn, {
|
|
348
417
|
...context,
|
|
349
418
|
hookScope: context.scope,
|
|
419
|
+
hookName: api.name,
|
|
350
420
|
testContext: api.testContext,
|
|
351
421
|
guards: [],
|
|
352
422
|
helper: null,
|
|
@@ -360,7 +430,7 @@ class Walker {
|
|
|
360
430
|
case 'invalid': {
|
|
361
431
|
const afterLoad = context.test !== null || context.hookScope !== null || context.helper !== null;
|
|
362
432
|
this.report(afterLoad ? { ...api.problem, afterLoad } : api.problem);
|
|
363
|
-
return true;
|
|
433
|
+
return api.declaration ? this.handle(call, api.declaration, context) : true;
|
|
364
434
|
}
|
|
365
435
|
case 'other':
|
|
366
436
|
return false;
|
|
@@ -409,6 +479,7 @@ class Walker {
|
|
|
409
479
|
call,
|
|
410
480
|
fn,
|
|
411
481
|
target: context.test ?? context.hookScope,
|
|
482
|
+
hookName: context.test ? null : context.hookName,
|
|
412
483
|
guards: context.guards,
|
|
413
484
|
});
|
|
414
485
|
}
|
|
@@ -430,33 +501,40 @@ class Walker {
|
|
|
430
501
|
return null;
|
|
431
502
|
}
|
|
432
503
|
/**
|
|
433
|
-
* What a function declared in
|
|
434
|
-
* declaration is looked up once, however many calls use it.
|
|
504
|
+
* What a function declared in a file, the one being walked unless said otherwise, or imported from a local module,
|
|
505
|
+
* returns; null otherwise. Each declaration is looked up once, however many calls use it.
|
|
435
506
|
*/
|
|
436
|
-
returnsOf(name, scopes) {
|
|
437
|
-
const declaration =
|
|
507
|
+
returnsOf(name, scopes, frame = this.frame) {
|
|
508
|
+
const declaration = frame.values.declarationOf(name, scopes);
|
|
438
509
|
if (declaration === null)
|
|
439
510
|
return null;
|
|
440
511
|
if (!this.returns.has(declaration)) {
|
|
441
|
-
const local = this.
|
|
442
|
-
const callee = local ? { fn: local, frame
|
|
512
|
+
const local = this.functionIn(frame, name, scopes);
|
|
513
|
+
const callee = local ? { fn: local, frame } : this.importedFunction(name, scopes, frame);
|
|
443
514
|
this.returns.set(declaration, callee && this.returnsIn(callee));
|
|
444
515
|
}
|
|
445
516
|
return this.returns.get(declaration) ?? null;
|
|
446
517
|
}
|
|
447
518
|
returnsIn({ fn, frame }) {
|
|
448
|
-
|
|
449
|
-
const values =
|
|
450
|
-
|
|
519
|
+
const scopes = [...this.scopeChain(fn, frame), fn];
|
|
520
|
+
const values = returnValues(fn)?.map(({ expression, around }) => ({
|
|
521
|
+
expression,
|
|
522
|
+
scopes: [...scopes, ...around.filter(isScopeNode)],
|
|
523
|
+
}));
|
|
524
|
+
return {
|
|
525
|
+
adapter: frame.adapter,
|
|
526
|
+
values: values ?? null,
|
|
527
|
+
returnsOf: (name, at) => this.returnsOf(name, at, frame),
|
|
528
|
+
};
|
|
451
529
|
}
|
|
452
530
|
/** The function a name imported from a local module stands for, in the file that writes it; null otherwise. */
|
|
453
|
-
importedFunction(name, scopes) {
|
|
454
|
-
const declaration =
|
|
455
|
-
const source =
|
|
531
|
+
importedFunction(name, scopes, from = this.frame) {
|
|
532
|
+
const declaration = from.values.declarationOf(name, scopes);
|
|
533
|
+
const source = from.values.importSource(name, scopes);
|
|
456
534
|
if (source === null)
|
|
457
535
|
return null;
|
|
458
536
|
const imported = declaration?.type === 'ImportSpecifier' ? exportName(declaration.imported) : 'default';
|
|
459
|
-
const frame = this.openFrom(
|
|
537
|
+
const frame = this.openFrom(from, source);
|
|
460
538
|
return frame ? this.exportedFunction(frame, imported, new Set(), 0) : null;
|
|
461
539
|
}
|
|
462
540
|
/** A module a file imports, read as a frame once; null when it can't be read or parsed. */
|
|
@@ -598,6 +676,89 @@ class Walker {
|
|
|
598
676
|
}
|
|
599
677
|
return chains.get(fn);
|
|
600
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Applies `test.setTimeout()` or `test.slow()` where it is called: in a test body to the test, in a `beforeEach` to
|
|
681
|
+
* the tests of its scope, at file or describe level to the scope. A function with a name applies them where it is
|
|
682
|
+
* called instead.
|
|
683
|
+
*/
|
|
684
|
+
runSetting(call, api, context) {
|
|
685
|
+
if (context.declaredIn !== null && !context.test) {
|
|
686
|
+
for (const item of api.problems)
|
|
687
|
+
this.report(item);
|
|
688
|
+
if (context.helper)
|
|
689
|
+
this.effectsOf(context.helper).push({ kind: 'run', settings: api.settings, argument: api.argument, call });
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const { settings, problems } = this.guardedRun(call, api.settings, api.argument, context.guards);
|
|
693
|
+
problems.unshift(...api.problems);
|
|
694
|
+
if (context.test) {
|
|
695
|
+
context.test.problems.push(...problems.map((item) => this.here(item)));
|
|
696
|
+
mergeRun(context.test.run, settings);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
for (const item of problems)
|
|
700
|
+
this.report(item);
|
|
701
|
+
if (context.hookScope) {
|
|
702
|
+
// `beforeAll` and `afterAll` set the hook's own timeout, and an `afterEach` runs after the test body.
|
|
703
|
+
if (context.hookName === 'beforeEach')
|
|
704
|
+
mergeRun(context.hookScope.hookRun, settings);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
mergeRun(context.scope.details.run, settings);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Run settings under the conditions around a call: a slow test keeps them as its condition, while a timeout set only
|
|
711
|
+
* sometimes is left out with a warning.
|
|
712
|
+
*/
|
|
713
|
+
guardedRun(call, written, argument, guards) {
|
|
714
|
+
const settings = { ...written };
|
|
715
|
+
const problems = [];
|
|
716
|
+
const guard = joinConditions(guards);
|
|
717
|
+
if (settings.slow && guards.length > 0) {
|
|
718
|
+
const own = settings.slow.conditions ?? [null];
|
|
719
|
+
settings.slow = {
|
|
720
|
+
conditions: own.map((condition) => joinConditions([...guards, ...(condition === null ? [] : [condition])])),
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (settings.timeout !== undefined && guard !== null) {
|
|
724
|
+
const message = `Option timeout: ${argument} is set only when ${guard}; it is not applied.`;
|
|
725
|
+
problems.push(problem('unresolved-option', call, message, snippet(this.text(call))));
|
|
726
|
+
delete settings.timeout;
|
|
727
|
+
}
|
|
728
|
+
return { settings, problems };
|
|
729
|
+
}
|
|
730
|
+
/** Run settings a helper sets when a test body or a `beforeEach` calls it, under the conditions around the call. */
|
|
731
|
+
applyHelperRun(call, effect) {
|
|
732
|
+
const { settings, problems } = this.guardedRun(call.call, effect.settings, effect.argument, effect.guards);
|
|
733
|
+
if (call.hookName === null) {
|
|
734
|
+
const test = call.target;
|
|
735
|
+
test.problems.push(...problems);
|
|
736
|
+
mergeRun(test.run, settings);
|
|
737
|
+
}
|
|
738
|
+
else if (call.hookName === 'beforeEach') {
|
|
739
|
+
this.problems.push(...problems);
|
|
740
|
+
mergeRun(call.target.hookRun, settings);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/** Counts `expect()`, `expect.soft()` and `expect.poll()`, and reads `expect.assertions(n)` and `expect.hasAssertions()`. */
|
|
744
|
+
expectCall(call, test) {
|
|
745
|
+
const callee = unwrap(call.callee);
|
|
746
|
+
const [root, member] = callee.type === 'MemberExpression' ? [unwrap(callee.object), propertyName(callee)] : [callee, 'call'];
|
|
747
|
+
if (root.type !== 'Identifier' || root.name !== 'expect')
|
|
748
|
+
return;
|
|
749
|
+
if (member === 'call' || member === 'soft' || member === 'poll')
|
|
750
|
+
test.expectCount += 1;
|
|
751
|
+
if (member === 'hasAssertions')
|
|
752
|
+
test.expectsAssertions = true;
|
|
753
|
+
const count = call.arguments.at(0);
|
|
754
|
+
if (member === 'assertions' && count && count.type !== 'SpreadElement') {
|
|
755
|
+
const file = { source: this.frame.parsed.source, values: this.frame.values, version: null };
|
|
756
|
+
const option = { node: count, resolved: this.frame.values.resolve(count, this.scopes) };
|
|
757
|
+
const problems = [];
|
|
758
|
+
test.expectedAssertions = readCount(file, 'expect.assertions', option, problems);
|
|
759
|
+
test.problems.push(...problems.map((item) => this.here(item)));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
601
762
|
/** Reports a problem not tied to a test, once, however many times a function with it is walked. */
|
|
602
763
|
report(found) {
|
|
603
764
|
const item = this.here(found);
|
|
@@ -679,8 +840,11 @@ class Walker {
|
|
|
679
840
|
return expand(effect.fn, [...guards, ...effect.guards], new Set([...path, fn]));
|
|
680
841
|
if (effect.kind === 'configure')
|
|
681
842
|
return [effect];
|
|
843
|
+
if (effect.kind === 'run')
|
|
844
|
+
return [{ ...effect, guards }];
|
|
682
845
|
const condition = joinConditions([...guards, ...(effect.setting.condition ? [effect.setting.condition] : [])]);
|
|
683
|
-
|
|
846
|
+
// Helpers are expanded here only for calls from tests and hooks.
|
|
847
|
+
return [{ ...effect, setting: { ...effect.setting, condition, afterLoad: true } }];
|
|
684
848
|
});
|
|
685
849
|
};
|
|
686
850
|
for (const call of this.helperCalls) {
|
|
@@ -690,6 +854,8 @@ class Walker {
|
|
|
690
854
|
// `describe.configure()` does nothing in a test or hook body.
|
|
691
855
|
if (effect.kind === 'state')
|
|
692
856
|
call.target.states.push(effect.setting);
|
|
857
|
+
if (effect.kind === 'run')
|
|
858
|
+
this.applyHelperRun(call, effect);
|
|
693
859
|
}
|
|
694
860
|
if (steps > MAX_CALLS) {
|
|
695
861
|
const callee = snippet(this.frame.parsed.source.slice(call.call.callee.start, call.call.callee.end));
|
|
@@ -744,6 +910,7 @@ class Walker {
|
|
|
744
910
|
if (context.quiet || this.nested(call, context))
|
|
745
911
|
return;
|
|
746
912
|
const statement = this.statement(call);
|
|
913
|
+
const empty = emptyTitle(declaration, call);
|
|
747
914
|
const test = {
|
|
748
915
|
scope: context.scope,
|
|
749
916
|
declaration,
|
|
@@ -757,17 +924,26 @@ class Walker {
|
|
|
757
924
|
isInCondition: context.isInCondition,
|
|
758
925
|
isInFunction: context.isInFunction,
|
|
759
926
|
comments: this.commentsAbove(statement.start),
|
|
760
|
-
|
|
927
|
+
run: {},
|
|
928
|
+
expectCount: 0,
|
|
929
|
+
expectedAssertions: null,
|
|
930
|
+
expectsAssertions: false,
|
|
931
|
+
steps: [],
|
|
932
|
+
problems: [...declaration.problems, ...empty].map((item) => this.here(item)),
|
|
761
933
|
};
|
|
762
934
|
this.tests.push(test);
|
|
763
935
|
if (context.declaredIn)
|
|
764
936
|
this.declaredIn.set(test, context.declaredIn);
|
|
765
937
|
this.cut(context.scope, call);
|
|
766
|
-
|
|
767
|
-
|
|
938
|
+
// A function declared in the file and passed by name runs as the test body too.
|
|
939
|
+
const body = declaration.fn ?? declaration.hashed;
|
|
940
|
+
if (body) {
|
|
941
|
+
this.visitCallback(body, {
|
|
768
942
|
...context,
|
|
769
943
|
test,
|
|
770
944
|
hookScope: null,
|
|
945
|
+
hookName: null,
|
|
946
|
+
stepDepth: 0,
|
|
771
947
|
testContext: declaration.testContext,
|
|
772
948
|
guards: [],
|
|
773
949
|
helper: null,
|
|
@@ -777,7 +953,7 @@ class Walker {
|
|
|
777
953
|
declareSuite(call, declaration, context) {
|
|
778
954
|
if (context.quiet || this.nested(call, context))
|
|
779
955
|
return;
|
|
780
|
-
for (const item of declaration.problems)
|
|
956
|
+
for (const item of [...declaration.problems, ...emptyTitle(declaration, call)])
|
|
781
957
|
this.report(item);
|
|
782
958
|
const scope = {
|
|
783
959
|
kind: 'describe',
|
|
@@ -786,6 +962,9 @@ class Walker {
|
|
|
786
962
|
mode: declaration.mode,
|
|
787
963
|
isOnly: declaration.isOnly,
|
|
788
964
|
details: declaration.details,
|
|
965
|
+
hooks: noHooks(),
|
|
966
|
+
hookRun: {},
|
|
967
|
+
hookFixtures: [],
|
|
789
968
|
suite: null,
|
|
790
969
|
};
|
|
791
970
|
const suite = {
|
|
@@ -807,6 +986,8 @@ class Walker {
|
|
|
807
986
|
scope,
|
|
808
987
|
test: null,
|
|
809
988
|
hookScope: null,
|
|
989
|
+
hookName: null,
|
|
990
|
+
stepDepth: 0,
|
|
810
991
|
testContext: null,
|
|
811
992
|
guards: [],
|
|
812
993
|
helper: null,
|
|
@@ -843,31 +1024,6 @@ class Walker {
|
|
|
843
1024
|
return lines;
|
|
844
1025
|
}
|
|
845
1026
|
}
|
|
846
|
-
/**
|
|
847
|
-
* The values a function returns, each with the scopes around it, outermost first; null when it may return no value, as
|
|
848
|
-
* with `return;` or no `return` at all. A `return` in a function inside it is that function's.
|
|
849
|
-
*/
|
|
850
|
-
function returnValues(fn, scopes) {
|
|
851
|
-
// Only declarations of overloads have no body.
|
|
852
|
-
const body = fn.body;
|
|
853
|
-
if (body.type !== 'BlockStatement')
|
|
854
|
-
return [{ expression: body, scopes }];
|
|
855
|
-
const values = [];
|
|
856
|
-
const visit = (node, around) => {
|
|
857
|
-
if (node.type === 'ReturnStatement') {
|
|
858
|
-
values.push(node.argument ? { expression: node.argument, scopes: around } : null);
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
if (node.type === 'FunctionDeclaration' || isFunctionNode(node))
|
|
862
|
-
return;
|
|
863
|
-
const inner = isScopeNode(node) ? [...around, node] : around;
|
|
864
|
-
for (const child of childNodes(node))
|
|
865
|
-
visit(child, inner);
|
|
866
|
-
};
|
|
867
|
-
visit(body, scopes);
|
|
868
|
-
const always = values.every((value) => value !== null);
|
|
869
|
-
return always && values.length > 0 ? values : null;
|
|
870
|
-
}
|
|
871
1027
|
/** Walks the scanned file; `openModule` reads the modules of imported helpers that the file calls. */
|
|
872
1028
|
export function extract(main, openModule) {
|
|
873
1029
|
return new Walker(main, openModule).run();
|
package/dist/filter.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Inventory, TestRecord, TestState } from './types.ts';
|
|
2
|
+
/** @inline */
|
|
3
|
+
export interface InventoryFilter {
|
|
4
|
+
/** Keeps tests in one of these states. */
|
|
5
|
+
states?: TestState[];
|
|
6
|
+
/** Keeps tests with one of these tags, as the output writes them. */
|
|
7
|
+
tags?: string[];
|
|
8
|
+
/** Keeps tests whose `fullName` matches this regular expression, ignoring case, as Playwright's `--grep` does. */
|
|
9
|
+
grep?: string;
|
|
10
|
+
/** Keeps tests that use `.only`, or are inside a describe that does. */
|
|
11
|
+
only?: boolean;
|
|
12
|
+
/** Keeps tests that don't run and give no reason. Tests in a file the runner refuses are left out. */
|
|
13
|
+
withoutReason?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** An inventory whose test records have only `id` and the keys picked with `pickTestFields()`. */
|
|
16
|
+
export type PickedInventory<K extends keyof TestRecord> = Omit<Inventory, 'tests'> & {
|
|
17
|
+
tests: Pick<TestRecord, 'id' | K>[];
|
|
18
|
+
};
|
|
19
|
+
/** Checks a filter and returns whether a test passes it. */
|
|
20
|
+
export declare function matcher(filter: InventoryFilter): (test: TestRecord) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Keeps the tests that pass every condition of the filter; a list keeps a test that has any of its values. The
|
|
23
|
+
* result describes only those tests: their files, the describes around them, their diagnostics, and a summary
|
|
24
|
+
* counted over them. Files without any tests stay, with their diagnostics, since tests may be missing from them.
|
|
25
|
+
* `summary.fileCount` stays the number of files scanned.
|
|
26
|
+
*
|
|
27
|
+
* Throws a `TypeError` for an invalid filter.
|
|
28
|
+
*/
|
|
29
|
+
export declare function filterInventory(inventory: Inventory, filter: InventoryFilter): Inventory;
|
|
30
|
+
/** Checks the keys to pick and returns them after `id`, each once. */
|
|
31
|
+
export declare function checkFields(fields: unknown): (keyof TestRecord)[];
|
|
32
|
+
/**
|
|
33
|
+
* Leaves only `id` and the given keys on each test record, in that order, to make the output smaller. Suites,
|
|
34
|
+
* diagnostics and the summary are unchanged. The result no longer matches the JSON Schema, which requires every key.
|
|
35
|
+
*
|
|
36
|
+
* Throws a `TypeError` for a key that test records don't have.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pickTestFields<K extends keyof TestRecord>(inventory: Inventory, fields: readonly K[]): PickedInventory<K>;
|
package/dist/filter.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { checkObject, OptionsError } from "./options.js";
|
|
2
|
+
import { summarize } from "./summary.js";
|
|
3
|
+
const STATES = ['active', 'skip', 'fixme', 'todo', 'expectedToFail', 'notLoaded'];
|
|
4
|
+
const FILTER_KEYS = [
|
|
5
|
+
'states',
|
|
6
|
+
'tags',
|
|
7
|
+
'grep',
|
|
8
|
+
'only',
|
|
9
|
+
'withoutReason',
|
|
10
|
+
];
|
|
11
|
+
// Typed as a record of every key, so a key added to TestRecord fails to compile until it is listed here.
|
|
12
|
+
const TEST_KEYS = Object.keys({
|
|
13
|
+
id: true,
|
|
14
|
+
bodyHash: true,
|
|
15
|
+
suiteId: true,
|
|
16
|
+
suitePath: true,
|
|
17
|
+
relativeFilePath: true,
|
|
18
|
+
fileName: true,
|
|
19
|
+
lineStart: true,
|
|
20
|
+
lineEnd: true,
|
|
21
|
+
columnStart: true,
|
|
22
|
+
columnEnd: true,
|
|
23
|
+
declaredAt: true,
|
|
24
|
+
title: true,
|
|
25
|
+
titleWithoutTags: true,
|
|
26
|
+
hasDynamicTitle: true,
|
|
27
|
+
fullName: true,
|
|
28
|
+
isTypeTest: true,
|
|
29
|
+
state: true,
|
|
30
|
+
isActive: true,
|
|
31
|
+
isSkipped: true,
|
|
32
|
+
isFixme: true,
|
|
33
|
+
isTodo: true,
|
|
34
|
+
isExpectedToFail: true,
|
|
35
|
+
isNotLoaded: true,
|
|
36
|
+
isOnly: true,
|
|
37
|
+
isConditional: true,
|
|
38
|
+
condition: true,
|
|
39
|
+
reason: true,
|
|
40
|
+
hasDynamicReason: true,
|
|
41
|
+
stateSource: true,
|
|
42
|
+
stateLine: true,
|
|
43
|
+
isParallel: true,
|
|
44
|
+
isSerial: true,
|
|
45
|
+
tags: true,
|
|
46
|
+
annotations: true,
|
|
47
|
+
comments: true,
|
|
48
|
+
locks: true,
|
|
49
|
+
meta: true,
|
|
50
|
+
hooks: true,
|
|
51
|
+
timeout: true,
|
|
52
|
+
isSlow: true,
|
|
53
|
+
slowCondition: true,
|
|
54
|
+
retries: true,
|
|
55
|
+
retryDelay: true,
|
|
56
|
+
retryCondition: true,
|
|
57
|
+
repeats: true,
|
|
58
|
+
fixtures: true,
|
|
59
|
+
expectCount: true,
|
|
60
|
+
expectedAssertions: true,
|
|
61
|
+
expectsAssertions: true,
|
|
62
|
+
hasEmptyBody: true,
|
|
63
|
+
steps: true,
|
|
64
|
+
isBenchmark: true,
|
|
65
|
+
isParameterized: true,
|
|
66
|
+
caseCount: true,
|
|
67
|
+
isInLoop: true,
|
|
68
|
+
isInCondition: true,
|
|
69
|
+
isInFunction: true,
|
|
70
|
+
});
|
|
71
|
+
function list(value, option, allowed) {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return [];
|
|
74
|
+
const valid = (item) => typeof item === 'string' && item !== '' && (allowed === null || allowed.includes(item));
|
|
75
|
+
if (Array.isArray(value) && value.every(valid))
|
|
76
|
+
return value;
|
|
77
|
+
const bad = Array.isArray(value) ? value.find((item) => !valid(item)) : value;
|
|
78
|
+
const expected = allowed === null ? 'non-empty strings' : allowed.map((item) => `"${item}"`).join(', ');
|
|
79
|
+
throw new OptionsError(`${option} can only hold ${expected}; got ${JSON.stringify(bad)}.`);
|
|
80
|
+
}
|
|
81
|
+
function flag(value, option) {
|
|
82
|
+
if (value === undefined)
|
|
83
|
+
return false;
|
|
84
|
+
if (typeof value !== 'boolean')
|
|
85
|
+
throw new OptionsError(`${option} must be true or false.`);
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function pattern(value) {
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
return null;
|
|
91
|
+
if (typeof value !== 'string' || value === '')
|
|
92
|
+
throw new OptionsError('filter.grep must be a non-empty string.');
|
|
93
|
+
try {
|
|
94
|
+
return new RegExp(value, 'i');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new OptionsError(`filter.grep must be a regular expression: ${error.message}.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Checks a filter and returns whether a test passes it. */
|
|
101
|
+
export function matcher(filter) {
|
|
102
|
+
const checked = checkObject(filter, 'filter');
|
|
103
|
+
// A misspelled key would return every test, an answer that looks filtered but isn't.
|
|
104
|
+
const unknown = Object.keys(checked).find((key) => !FILTER_KEYS.includes(key));
|
|
105
|
+
if (unknown !== undefined) {
|
|
106
|
+
throw new OptionsError(`filter can only have the keys ${FILTER_KEYS.join(', ')}; got "${unknown}".`);
|
|
107
|
+
}
|
|
108
|
+
const states = list(checked.states, 'filter.states', STATES);
|
|
109
|
+
const tags = list(checked.tags, 'filter.tags', null);
|
|
110
|
+
const grep = pattern(checked.grep);
|
|
111
|
+
const only = flag(checked.only, 'filter.only');
|
|
112
|
+
const withoutReason = flag(checked.withoutReason, 'filter.withoutReason');
|
|
113
|
+
return (test) => (states.length === 0 || states.includes(test.state)) &&
|
|
114
|
+
(tags.length === 0 || test.tags.some((tag) => tags.includes(tag))) &&
|
|
115
|
+
(grep === null || grep.test(test.fullName)) &&
|
|
116
|
+
(!only || test.isOnly) &&
|
|
117
|
+
(!withoutReason || (!test.isActive && !test.isNotLoaded && test.reason === null));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Keeps the tests that pass every condition of the filter; a list keeps a test that has any of its values. The
|
|
121
|
+
* result describes only those tests: their files, the describes around them, their diagnostics, and a summary
|
|
122
|
+
* counted over them. Files without any tests stay, with their diagnostics, since tests may be missing from them.
|
|
123
|
+
* `summary.fileCount` stays the number of files scanned.
|
|
124
|
+
*
|
|
125
|
+
* Throws a `TypeError` for an invalid filter.
|
|
126
|
+
*/
|
|
127
|
+
export function filterInventory(inventory, filter) {
|
|
128
|
+
const tests = inventory.tests.filter(matcher(filter));
|
|
129
|
+
const testIds = new Set(tests.map((test) => test.id));
|
|
130
|
+
const parents = new Map(inventory.suites.map((suite) => [suite.id, suite.parentId]));
|
|
131
|
+
const suiteIds = new Set();
|
|
132
|
+
for (const test of tests) {
|
|
133
|
+
for (let id = test.suiteId; id !== null && !suiteIds.has(id); id = parents.get(id)) {
|
|
134
|
+
suiteIds.add(id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const suites = inventory.suites.filter((suite) => suiteIds.has(suite.id));
|
|
138
|
+
const filesWithTests = new Set(inventory.tests.map((test) => test.relativeFilePath));
|
|
139
|
+
const filesWithMatches = new Set(tests.map((test) => test.relativeFilePath));
|
|
140
|
+
// A file, and a problem that is not about one test, stay unless every test of the file was left out: an error in
|
|
141
|
+
// a file with no tests, or in package.json, may mean tests are missing from the answer.
|
|
142
|
+
const keepsFile = (relativeFilePath) => !filesWithTests.has(relativeFilePath) || filesWithMatches.has(relativeFilePath);
|
|
143
|
+
const diagnostics = inventory.diagnostics.filter((diagnostic) => diagnostic.testId === null ? keepsFile(diagnostic.relativeFilePath) : testIds.has(diagnostic.testId));
|
|
144
|
+
const summary = summarize(inventory.summary.fileCount, suites, tests, diagnostics);
|
|
145
|
+
const files = inventory.files.filter((file) => keepsFile(file.relativeFilePath));
|
|
146
|
+
return { ...inventory, summary, files, suites, tests, diagnostics };
|
|
147
|
+
}
|
|
148
|
+
/** Checks the keys to pick and returns them after `id`, each once. */
|
|
149
|
+
export function checkFields(fields) {
|
|
150
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
151
|
+
throw new OptionsError('fields must be a non-empty array of test record keys.');
|
|
152
|
+
}
|
|
153
|
+
const bad = fields.findIndex((field) => !TEST_KEYS.includes(field));
|
|
154
|
+
if (bad !== -1) {
|
|
155
|
+
throw new OptionsError(`fields can only hold keys of a test record, such as "fullName"; got ${JSON.stringify(fields[bad])}.`);
|
|
156
|
+
}
|
|
157
|
+
return [...new Set(['id', ...fields])];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Leaves only `id` and the given keys on each test record, in that order, to make the output smaller. Suites,
|
|
161
|
+
* diagnostics and the summary are unchanged. The result no longer matches the JSON Schema, which requires every key.
|
|
162
|
+
*
|
|
163
|
+
* Throws a `TypeError` for a key that test records don't have.
|
|
164
|
+
*/
|
|
165
|
+
export function pickTestFields(inventory, fields) {
|
|
166
|
+
const keys = checkFields(fields);
|
|
167
|
+
const tests = inventory.tests.map((test) => Object.fromEntries(keys.map((key) => [key, test[key]])));
|
|
168
|
+
return { ...inventory, tests };
|
|
169
|
+
}
|