test-inventory 0.6.0 → 0.7.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 +126 -0
- package/dist/case-titles.d.ts +14 -0
- package/dist/case-titles.js +227 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -0
- package/dist/config-runs.d.ts +31 -5
- package/dist/config-runs.js +181 -41
- package/dist/config.js +65 -3
- package/dist/dependency-hash.d.ts +12 -4
- package/dist/dependency-hash.js +85 -10
- package/dist/diagnostics.js +1 -0
- package/dist/extract.d.ts +19 -1
- package/dist/extract.js +350 -31
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +4 -2
- package/dist/framework-version.js +17 -3
- package/dist/frameworks/common.d.ts +6 -0
- package/dist/frameworks/common.js +20 -0
- package/dist/frameworks/playwright.d.ts +3 -1
- package/dist/frameworks/playwright.js +14 -5
- package/dist/frameworks/vitest.d.ts +12 -2
- package/dist/frameworks/vitest.js +101 -16
- package/dist/imported-values.d.ts +22 -0
- package/dist/imported-values.js +135 -0
- package/dist/model.d.ts +19 -2
- package/dist/options.d.ts +11 -0
- package/dist/options.js +7 -0
- package/dist/parse.d.ts +6 -0
- package/dist/parse.js +204 -0
- package/dist/records.d.ts +2 -0
- package/dist/records.js +17 -6
- package/dist/resolve.js +28 -8
- package/dist/scan-file.d.ts +5 -0
- package/dist/scan-file.js +8 -5
- package/dist/scan-source.js +3 -1
- package/dist/scan.js +10 -4
- package/dist/types.d.ts +15 -3
- package/dist/values.d.ts +39 -1
- package/dist/values.js +425 -18
- package/package.json +1 -1
- package/schema/v1.json +28 -1
|
@@ -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, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, takesWhole, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
|
|
4
|
+
import { atLeast, boundMembers, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, takesWhole, 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([
|
|
@@ -297,7 +297,9 @@ export class PlaywrightAdapter {
|
|
|
297
297
|
const declaration = this.file.values.declarationOf(base.name, scopes);
|
|
298
298
|
const bound = declaration !== null && this.bindings.declarations.has(declaration);
|
|
299
299
|
const local = declaration !== null && this.imports.get(base.name)?.node === declaration;
|
|
300
|
-
const alias = declaration?.type === 'VariableDeclarator' && !bound && !local
|
|
300
|
+
const alias = declaration?.type === 'VariableDeclarator' && !bound && !local
|
|
301
|
+
? this.alias(declaration, base.name, lookup)
|
|
302
|
+
: null;
|
|
301
303
|
// The module from `require()` is both the namespace and `test`, so `pw.test(...)` reads as the namespace.
|
|
302
304
|
if (bound && this.bindings.namespaces.has(base.name) && links[0]?.name === 'test' && !links[0].args) {
|
|
303
305
|
return links.slice(1);
|
|
@@ -366,9 +368,14 @@ export class PlaywrightAdapter {
|
|
|
366
368
|
}
|
|
367
369
|
/**
|
|
368
370
|
* The members of `test` a variable holds: those of a describe function, as in `const serialDescribe =
|
|
369
|
-
* test.describe.serial`,
|
|
371
|
+
* test.describe.serial`, none for a test object a function returns, and the member `name` takes when it is
|
|
372
|
+
* destructured, as in `const { beforeEach } = test`. Playwright binds every member to its test object, so a
|
|
373
|
+
* destructured one works as it does on the object; a test object built with a destructured `extend` is not followed.
|
|
370
374
|
*/
|
|
371
|
-
alias(declarator, lookup) {
|
|
375
|
+
alias(declarator, name, lookup) {
|
|
376
|
+
const destructured = boundMembers(declarator.id, name);
|
|
377
|
+
if (!destructured)
|
|
378
|
+
return null;
|
|
372
379
|
let members = this.aliases.get(declarator);
|
|
373
380
|
if (members === undefined) {
|
|
374
381
|
// Set first, so a variable defined through itself ends the lookup.
|
|
@@ -376,7 +383,7 @@ export class PlaywrightAdapter {
|
|
|
376
383
|
members = declarator.init ? this.members(declarator.init, lookup) : null;
|
|
377
384
|
this.aliases.set(declarator, members);
|
|
378
385
|
}
|
|
379
|
-
return members;
|
|
386
|
+
return members && [...members, ...destructured];
|
|
380
387
|
}
|
|
381
388
|
/** The members of `test` a value holds: none for a test object, or those of a describe function. */
|
|
382
389
|
members(expression, lookup) {
|
|
@@ -480,6 +487,7 @@ export class PlaywrightAdapter {
|
|
|
480
487
|
hashed,
|
|
481
488
|
isParameterized: false,
|
|
482
489
|
caseCount: null,
|
|
490
|
+
cases: null,
|
|
483
491
|
testContext: null,
|
|
484
492
|
fixtures: destructuredNames(hashed, 0),
|
|
485
493
|
takesAllFixtures: takesWhole(hashed, 0),
|
|
@@ -545,6 +553,7 @@ export class PlaywrightAdapter {
|
|
|
545
553
|
hashed: fn,
|
|
546
554
|
isParameterized: false,
|
|
547
555
|
caseCount: null,
|
|
556
|
+
cases: null,
|
|
548
557
|
problems,
|
|
549
558
|
};
|
|
550
559
|
}
|
|
@@ -37,7 +37,11 @@ export declare class VitestAdapter implements Adapter {
|
|
|
37
37
|
*/
|
|
38
38
|
private resolve;
|
|
39
39
|
private start;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
|
|
42
|
+
* takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
|
|
43
|
+
* `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
|
|
44
|
+
*/
|
|
41
45
|
private alias;
|
|
42
46
|
private aliasOf;
|
|
43
47
|
/**
|
|
@@ -75,10 +79,16 @@ export declare class VitestAdapter implements Adapter {
|
|
|
75
79
|
/** Vitest's order: `only` wins over `skip`, which wins over `todo`. */
|
|
76
80
|
private states;
|
|
77
81
|
private readDeclarationOptions;
|
|
78
|
-
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }
|
|
82
|
+
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`. */
|
|
79
83
|
private retry;
|
|
80
84
|
private meta;
|
|
81
85
|
private caseCount;
|
|
86
|
+
/**
|
|
87
|
+
* The cases of a table whose values the scan can work out, as Vitest passes them: `.each` spreads a case that is an
|
|
88
|
+
* array into the callback's arguments when every case is one, and `.for` passes each case whole. A tagged template
|
|
89
|
+
* gives one object per row, keyed by the names in its first line.
|
|
90
|
+
*/
|
|
91
|
+
private tableCases;
|
|
82
92
|
private arrayLength;
|
|
83
93
|
private tagsUsed;
|
|
84
94
|
}
|
|
@@ -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, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
4
|
+
import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, boundMembers, destructuredNames, takesWhole, 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)
|
|
@@ -27,6 +27,7 @@ const MODIFIERS = {
|
|
|
27
27
|
};
|
|
28
28
|
const TABLES = new Set(['each', 'for']);
|
|
29
29
|
const CONDITIONS = new Set(['skipIf', 'runIf']);
|
|
30
|
+
const STATE_FLAGS = ['only', 'todo', 'fails'];
|
|
30
31
|
const DERIVATIONS = new Set(['extend', 'override', 'scoped']);
|
|
31
32
|
/** Hooks, with the position of the test context among their callback's parameters. */
|
|
32
33
|
const HOOKS = new Map([
|
|
@@ -401,7 +402,7 @@ export class VitestAdapter {
|
|
|
401
402
|
this.worthFollowing(base.name, links, args) &&
|
|
402
403
|
this.follow(base.name, !inBody && mayDeclareTests(base.name, links));
|
|
403
404
|
if (!followed && !this.bindings.declarations.has(declaration)) {
|
|
404
|
-
const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, lookup) : null;
|
|
405
|
+
const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, base.name, lookup) : null;
|
|
405
406
|
if (alias)
|
|
406
407
|
return { kind: alias.kind, index: 0, modifiers: alias.links };
|
|
407
408
|
return this.assumeLocal(declaration, base.name, links, args)
|
|
@@ -415,8 +416,15 @@ export class VitestAdapter {
|
|
|
415
416
|
const kind = this.bindings.kinds.get(base.name);
|
|
416
417
|
return kind ? { kind, index: 0 } : null;
|
|
417
418
|
}
|
|
418
|
-
/**
|
|
419
|
-
|
|
419
|
+
/**
|
|
420
|
+
* The function and modifiers a variable holds, as in `const skipOnCi = test.skipIf(isCI)`, with the member `name`
|
|
421
|
+
* takes when it is destructured, as in `const { beforeEach } = test`. The members Vitest reads through `this`, such as
|
|
422
|
+
* `each` and `skipIf`, return a test function only when called, so their calls are never read through a variable.
|
|
423
|
+
*/
|
|
424
|
+
alias(declarator, name, lookup) {
|
|
425
|
+
const destructured = boundMembers(declarator.id, name);
|
|
426
|
+
if (!destructured)
|
|
427
|
+
return null;
|
|
420
428
|
let alias = this.aliases.get(declarator);
|
|
421
429
|
if (alias === undefined) {
|
|
422
430
|
// Set first, so a variable defined through itself ends the lookup.
|
|
@@ -424,7 +432,8 @@ export class VitestAdapter {
|
|
|
424
432
|
alias = declarator.init ? this.aliasOf(declarator.init, lookup) : null;
|
|
425
433
|
this.aliases.set(declarator, alias);
|
|
426
434
|
}
|
|
427
|
-
|
|
435
|
+
const links = destructured.map((member) => ({ name: member, args: null, template: null }));
|
|
436
|
+
return alias && { kind: alias.kind, links: [...alias.links, ...links] };
|
|
428
437
|
}
|
|
429
438
|
aliasOf(expression, lookup) {
|
|
430
439
|
const node = unwrap(expression);
|
|
@@ -634,6 +643,17 @@ export class VitestAdapter {
|
|
|
634
643
|
const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
|
|
635
644
|
const flags = new Set(chain);
|
|
636
645
|
let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
|
|
646
|
+
// From Vitest 5.0.5 a test's `skip`, `skipIf` and `runIf` each set its `skip`, so the last of them decides, and a
|
|
647
|
+
// `skipIf` or `runIf` replaces the skip of a tag or of a describe around. The scan applies that to all of Vitest 5.
|
|
648
|
+
const lastSkip = links.filter((link) => link.name === 'skip' || CONDITIONS.has(link.name)).at(-1);
|
|
649
|
+
const skipsByChain = kind === 'test' && lastSkip !== undefined && atLeast(this.file.version, 5, 0);
|
|
650
|
+
const conditionSetsSkip = skipsByChain && CONDITIONS.has(lastSkip.name);
|
|
651
|
+
if (skipsByChain) {
|
|
652
|
+
flags.delete('skip');
|
|
653
|
+
conditions = conditionSetsSkip ? [this.linkCondition(lastSkip, context)] : [];
|
|
654
|
+
if (!conditionSetsSkip)
|
|
655
|
+
flags.add('skip');
|
|
656
|
+
}
|
|
637
657
|
// A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
|
|
638
658
|
// test's mode, and for everything on a describe.
|
|
639
659
|
for (const [flag, value] of read.flags) {
|
|
@@ -653,7 +673,7 @@ export class VitestAdapter {
|
|
|
653
673
|
// win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
|
|
654
674
|
const line = this.file.source.line(call.start);
|
|
655
675
|
const around = context.passedDown;
|
|
656
|
-
const passed = read.skip ? null : around;
|
|
676
|
+
const passed = read.skip || conditionSetsSkip ? null : around;
|
|
657
677
|
const skips = passed?.condition.applies === 'always' && !flags.has('only');
|
|
658
678
|
const table = links.find((link) => TABLES.has(link.name)) ?? null;
|
|
659
679
|
const isTodo = flags.has('todo') || body === undefined;
|
|
@@ -683,6 +703,7 @@ export class VitestAdapter {
|
|
|
683
703
|
hashed: fn ?? namedFunction(this.file, context.scopes, body),
|
|
684
704
|
isParameterized: table !== null,
|
|
685
705
|
caseCount: table ? this.caseCount(call, table, context, problems) : null,
|
|
706
|
+
cases: table ? this.tableCases(table, context) : null,
|
|
686
707
|
problems,
|
|
687
708
|
};
|
|
688
709
|
let declared;
|
|
@@ -704,6 +725,10 @@ export class VitestAdapter {
|
|
|
704
725
|
fixtures: fixturesOf(base.hashed, index, this.file.version),
|
|
705
726
|
takesAllFixtures: takesWhole(base.hashed, index),
|
|
706
727
|
usesBench: atLeast(this.file.version, 5, 0) && destructuredNames(base.hashed, index).includes('bench'),
|
|
728
|
+
ownStateOptions: [
|
|
729
|
+
...(read.skip || conditionSetsSkip ? ['skip'] : []),
|
|
730
|
+
...STATE_FLAGS.filter((flag) => read.flags.has(flag)),
|
|
731
|
+
],
|
|
707
732
|
};
|
|
708
733
|
}
|
|
709
734
|
return refusal ? refused(call, refusal, context, () => declared) : declared;
|
|
@@ -763,8 +788,14 @@ export class VitestAdapter {
|
|
|
763
788
|
return result;
|
|
764
789
|
const options = readOptions(this.file, context.scopes, node);
|
|
765
790
|
problems.push(...unresolvedOptions(this.file, options, 'its flags, tags and meta are missing'));
|
|
791
|
+
// From Vitest 5.0.5 an option set to `undefined` replaces nothing; the scan applies that to all of Vitest 5.
|
|
792
|
+
const given = (key) => {
|
|
793
|
+
const option = options.values.get(key);
|
|
794
|
+
const isUndefined = option?.resolved.ok === true && option.resolved.value === undefined;
|
|
795
|
+
return isUndefined && atLeast(this.file.version, 5, 0) ? undefined : option;
|
|
796
|
+
};
|
|
766
797
|
for (const flag of ['only', 'todo', 'fails', 'concurrent', 'sequential', 'shuffle']) {
|
|
767
|
-
const option =
|
|
798
|
+
const option = given(flag);
|
|
768
799
|
if (option && !option.resolved.ok) {
|
|
769
800
|
const { importedFrom } = option.resolved;
|
|
770
801
|
problems.push(unresolvedOption(this.file, flag, option.node, importedFrom, 'it is not applied'));
|
|
@@ -776,7 +807,7 @@ export class VitestAdapter {
|
|
|
776
807
|
result.flags.set(flag, Boolean(option.resolved.value));
|
|
777
808
|
}
|
|
778
809
|
}
|
|
779
|
-
const skip =
|
|
810
|
+
const skip = given('skip');
|
|
780
811
|
if (skip) {
|
|
781
812
|
result.skip = skip.resolved.ok
|
|
782
813
|
? { applies: skip.resolved.value ? 'always' : 'never', text: null }
|
|
@@ -806,7 +837,7 @@ export class VitestAdapter {
|
|
|
806
837
|
this.retry(retry, details, context, problems);
|
|
807
838
|
return result;
|
|
808
839
|
}
|
|
809
|
-
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }
|
|
840
|
+
/** `retry` as a number, or since Vitest 4.1 as `{ count, delay, condition }`. */
|
|
810
841
|
retry(option, details, context, problems) {
|
|
811
842
|
const node = unwrap(option.node);
|
|
812
843
|
if (node.type !== 'ObjectExpression') {
|
|
@@ -815,8 +846,14 @@ export class VitestAdapter {
|
|
|
815
846
|
details.run.retry = { count, delay: null, condition: null };
|
|
816
847
|
return;
|
|
817
848
|
}
|
|
818
|
-
if (!atLeast(this.file.version, 4, 1))
|
|
849
|
+
if (!atLeast(this.file.version, 4, 1)) {
|
|
850
|
+
// Before 4.1 the runner compares the attempt number with the object, so it never runs the test.
|
|
851
|
+
const source = snippet(text(this.file, node));
|
|
852
|
+
const message = `Option retry: ${source} is an object, which Vitest reads from 4.1; before, it never runs the test.`;
|
|
853
|
+
problems.push(problem('unresolved-option', node, message, source));
|
|
854
|
+
details.run.retry = null;
|
|
819
855
|
return;
|
|
856
|
+
}
|
|
820
857
|
const values = readOptions(this.file, context.scopes, node);
|
|
821
858
|
problems.push(...unresolvedOptions(this.file, values, 'the retry is not applied'));
|
|
822
859
|
const read = (key) => {
|
|
@@ -859,6 +896,43 @@ export class VitestAdapter {
|
|
|
859
896
|
}
|
|
860
897
|
return count;
|
|
861
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* The cases of a table whose values the scan can work out, as Vitest passes them: `.each` spreads a case that is an
|
|
901
|
+
* array into the callback's arguments when every case is one, and `.for` passes each case whole. A tagged template
|
|
902
|
+
* gives one object per row, keyed by the names in its first line.
|
|
903
|
+
*/
|
|
904
|
+
tableCases(table, context) {
|
|
905
|
+
let cases;
|
|
906
|
+
if (table.template) {
|
|
907
|
+
const values = table.template.expressions.map((item) => this.file.values.resolve(item, context.scopes));
|
|
908
|
+
if (!values.every((item) => item.ok))
|
|
909
|
+
return null;
|
|
910
|
+
const header = templateHeader(table.template);
|
|
911
|
+
// Without values, Vitest passes the template's strings as the table.
|
|
912
|
+
cases =
|
|
913
|
+
values.length === 0
|
|
914
|
+
? table.template.quasis.map((quasi) => quasi.value.cooked ?? undefined)
|
|
915
|
+
: Array.from({ length: Math.floor(values.length / header.length) }, (_, row) => Object.fromEntries(header.map((name, column) => [name, values[row * header.length + column].value])));
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
const first = table.args.at(0);
|
|
919
|
+
const resolved = first && first.type !== 'SpreadElement' ? this.file.values.resolve(first, context.scopes) : null;
|
|
920
|
+
if (!resolved?.ok || !Array.isArray(resolved.value))
|
|
921
|
+
return null;
|
|
922
|
+
cases = resolved.value;
|
|
923
|
+
}
|
|
924
|
+
if (table.name === 'for') {
|
|
925
|
+
return cases.map((item) => ({
|
|
926
|
+
items: item === null || item === undefined ? [] : Array.isArray(item) ? item : [item],
|
|
927
|
+
args: [item],
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
930
|
+
const arraysOnly = cases.every((item) => Array.isArray(item));
|
|
931
|
+
return cases.map((item) => {
|
|
932
|
+
const items = Array.isArray(item) ? item : [item];
|
|
933
|
+
return { items, args: arraysOnly ? items : [item] };
|
|
934
|
+
});
|
|
935
|
+
}
|
|
862
936
|
arrayLength(node, context) {
|
|
863
937
|
if (node === undefined || node.type === 'SpreadElement')
|
|
864
938
|
return null;
|
|
@@ -880,13 +954,24 @@ export class VitestAdapter {
|
|
|
880
954
|
this.problems.push(problem('config-may-change-state', node, "The file uses Vitest tags, and tags defined in the Vitest config can skip tests or change how they run; the scan can't work out the config's tags."));
|
|
881
955
|
}
|
|
882
956
|
}
|
|
883
|
-
/**
|
|
957
|
+
/**
|
|
958
|
+
* The column names of a tagged-template table such as ``each`side | qty ${'buy'} | ${100}` ``, as Vitest's
|
|
959
|
+
* `formatTemplateString` reads them: the text of the template with its spaces taken out, its first line split at `|`.
|
|
960
|
+
* Outer pipes, as in `| side | qty |`, give empty names too.
|
|
961
|
+
*/
|
|
962
|
+
function templateHeader(template) {
|
|
963
|
+
// A string with an invalid escape, as in `\xZZ`, has no cooked value, which joins as nothing.
|
|
964
|
+
const strings = template.quasis.map((quasi) => quasi.value.cooked ?? '');
|
|
965
|
+
return strings.join('').trim().replace(/ /g, '').split('\n')[0].split('|');
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Rows of a tagged-template table: as many as the values fill, an incomplete last row left out, as Vitest does. A
|
|
969
|
+
* template without values isn't split, so its text is the one case.
|
|
970
|
+
*/
|
|
884
971
|
function templateRows(template) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
return null;
|
|
889
|
-
return template.expressions.length / columns;
|
|
972
|
+
if (template.expressions.length === 0)
|
|
973
|
+
return 1;
|
|
974
|
+
return Math.floor(template.expressions.length / templateHeader(template).length);
|
|
890
975
|
}
|
|
891
976
|
function isMetaValue(value) {
|
|
892
977
|
return value !== undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModuleReader } from './model.ts';
|
|
2
|
+
import { type ImportReader } from './values.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Works out the values project modules export, for the files of one scan: each module is parsed once and each of its
|
|
5
|
+
* exports worked out once, however many test files import it. Installed packages are never read, since the reader
|
|
6
|
+
* only gives project files.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ExportedValues {
|
|
9
|
+
private readonly modules;
|
|
10
|
+
private readonly done;
|
|
11
|
+
/** Results of the current lookup, also those that `done` can't keep, so that each module is read once in it. */
|
|
12
|
+
private readonly current;
|
|
13
|
+
private readonly pending;
|
|
14
|
+
/** How many times a lookup was cut short by a cycle or the depth limit. */
|
|
15
|
+
private cutShort;
|
|
16
|
+
/** What a file that reads its imports with `readModule` gets for `import { name } from source`. */
|
|
17
|
+
reader(readModule: ModuleReader | null): ImportReader | null;
|
|
18
|
+
/** What `module` exports under `name`; null when it doesn't export it. */
|
|
19
|
+
private exported;
|
|
20
|
+
private read;
|
|
21
|
+
private open;
|
|
22
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ownExport, parseModule, starReexport } from "./frameworks/bindings.js";
|
|
2
|
+
import { Values } from "./values.js";
|
|
3
|
+
const UNRESOLVED = { ok: false, importedFrom: null };
|
|
4
|
+
/** How many modules deep a name is followed through re-exports and imports, as for test objects. */
|
|
5
|
+
const MAX_DEPTH = 16;
|
|
6
|
+
/**
|
|
7
|
+
* Works out the values project modules export, for the files of one scan: each module is parsed once and each of its
|
|
8
|
+
* exports worked out once, however many test files import it. Installed packages are never read, since the reader
|
|
9
|
+
* only gives project files.
|
|
10
|
+
*/
|
|
11
|
+
export class ExportedValues {
|
|
12
|
+
modules = new WeakMap();
|
|
13
|
+
done = new Map();
|
|
14
|
+
/** Results of the current lookup, also those that `done` can't keep, so that each module is read once in it. */
|
|
15
|
+
current = new Map();
|
|
16
|
+
pending = new Set();
|
|
17
|
+
/** How many times a lookup was cut short by a cycle or the depth limit. */
|
|
18
|
+
cutShort = 0;
|
|
19
|
+
/** What a file that reads its imports with `readModule` gets for `import { name } from source`. */
|
|
20
|
+
reader(readModule) {
|
|
21
|
+
if (!readModule)
|
|
22
|
+
return null;
|
|
23
|
+
return (source, name) => {
|
|
24
|
+
const module = readModule(source);
|
|
25
|
+
if ('reason' in module)
|
|
26
|
+
return UNRESOLVED;
|
|
27
|
+
return this.exported(module, name) ?? UNRESOLVED;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** What `module` exports under `name`; null when it doesn't export it. */
|
|
31
|
+
exported(module, name) {
|
|
32
|
+
const key = `${module.relativeFilePath}\0${name}`;
|
|
33
|
+
if (this.done.has(key))
|
|
34
|
+
return this.done.get(key);
|
|
35
|
+
if (this.current.has(key)) {
|
|
36
|
+
// It was worked out on a path that was cut short, so what is built on it depends on that path too.
|
|
37
|
+
this.cutShort++;
|
|
38
|
+
return this.current.get(key);
|
|
39
|
+
}
|
|
40
|
+
// A path that comes back to a name being worked out, as `export *` modules that re-export each other do, gives
|
|
41
|
+
// nothing; another path may still give it.
|
|
42
|
+
if (this.pending.has(key) || this.pending.size >= MAX_DEPTH) {
|
|
43
|
+
this.cutShort++;
|
|
44
|
+
return this.pending.has(key) ? null : UNRESOLVED;
|
|
45
|
+
}
|
|
46
|
+
const before = this.cutShort;
|
|
47
|
+
this.pending.add(key);
|
|
48
|
+
const result = this.read(module, name);
|
|
49
|
+
this.pending.delete(key);
|
|
50
|
+
// A result found while a path was cut short depends on where the lookup began, so only this lookup keeps it.
|
|
51
|
+
if (this.cutShort === before)
|
|
52
|
+
this.done.set(key, result);
|
|
53
|
+
else
|
|
54
|
+
this.current.set(key, result);
|
|
55
|
+
if (this.pending.size === 0)
|
|
56
|
+
this.current.clear();
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
read(module, name) {
|
|
60
|
+
const opened = this.open(module);
|
|
61
|
+
if (!opened)
|
|
62
|
+
return UNRESOLVED;
|
|
63
|
+
if (opened.json)
|
|
64
|
+
return jsonExport(opened.json.value, name);
|
|
65
|
+
const program = opened.program;
|
|
66
|
+
for (const statement of program.body) {
|
|
67
|
+
const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : null;
|
|
68
|
+
if (declaration?.type === 'TSEnumDeclaration' && declaration.id.name === name) {
|
|
69
|
+
return opened.values.resolveName(name, [program]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const own = ownExport(program, name);
|
|
73
|
+
if (own?.kind === 'default')
|
|
74
|
+
return opened.values.resolve(own.value, [program]);
|
|
75
|
+
if (own?.kind === 'function')
|
|
76
|
+
return UNRESOLVED;
|
|
77
|
+
if (own?.kind === 'specifier' && own.source !== null) {
|
|
78
|
+
const from = module.readModule(own.source);
|
|
79
|
+
return 'reason' in from ? UNRESOLVED : (this.exported(from, own.local) ?? UNRESOLVED);
|
|
80
|
+
}
|
|
81
|
+
if (own) {
|
|
82
|
+
return opened.values.resolveName(own.kind === 'specifier' ? own.local : name, [program]);
|
|
83
|
+
}
|
|
84
|
+
// `export *` passes on every name but the default. When two modules export the name with different values, Node
|
|
85
|
+
// refuses to load the module and Vite's module runner keeps the first, so the value isn't known.
|
|
86
|
+
if (name === 'default')
|
|
87
|
+
return null;
|
|
88
|
+
let found = null;
|
|
89
|
+
for (const statement of program.body) {
|
|
90
|
+
const source = starReexport(statement);
|
|
91
|
+
const from = source === null ? null : module.readModule(source);
|
|
92
|
+
const result = from && !('reason' in from) ? this.exported(from, name) : null;
|
|
93
|
+
if (!result)
|
|
94
|
+
continue;
|
|
95
|
+
if (found && !(found.ok && result.ok && JSON.stringify(found.value) === JSON.stringify(result.value))) {
|
|
96
|
+
return UNRESOLVED;
|
|
97
|
+
}
|
|
98
|
+
found = result;
|
|
99
|
+
}
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
open(module) {
|
|
103
|
+
if (this.modules.has(module))
|
|
104
|
+
return this.modules.get(module);
|
|
105
|
+
let opened = null;
|
|
106
|
+
if (module.relativeFilePath.endsWith('.json')) {
|
|
107
|
+
const value = parseJson(module.code);
|
|
108
|
+
opened = value === undefined ? null : { program: null, json: { value }, values: new Values() };
|
|
109
|
+
}
|
|
110
|
+
else if (!module.bytes) {
|
|
111
|
+
const parsed = parseModule(module);
|
|
112
|
+
const values = new Values(this.reader(module.readModule));
|
|
113
|
+
opened = parsed.error ? null : { program: parsed.program, json: null, values };
|
|
114
|
+
}
|
|
115
|
+
this.modules.set(module, opened);
|
|
116
|
+
return opened;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function parseJson(code) {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(code);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** A JSON module's default export is its value; Vite and Playwright also export its top-level keys by name. */
|
|
128
|
+
function jsonExport(value, name) {
|
|
129
|
+
if (name === 'default')
|
|
130
|
+
return { ok: true, value };
|
|
131
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value) && Object.hasOwn(value, name)) {
|
|
132
|
+
return { ok: true, value: value[name] };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
package/dist/model.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { CallExpression, Expression, Node } from 'oxc-parser';
|
|
2
2
|
import type { FunctionNode } from './ast.ts';
|
|
3
3
|
import type { Annotation, DiagnosticCode, DiagnosticLevel, HookCounts, MetaValue } from './types.ts';
|
|
4
|
+
import type { Value } from './values.ts';
|
|
4
5
|
export type State = 'skip' | 'fixme' | 'todo' | 'expectedToFail';
|
|
6
|
+
/** The Vitest options that set a test's state. */
|
|
7
|
+
export type StateOption = 'skip' | 'only' | 'todo' | 'fails';
|
|
5
8
|
export type Mode = 'parallel' | 'serial' | 'default';
|
|
6
9
|
/** A state set by a declaration, a describe or a call such as `test.skip(condition)`. */
|
|
7
10
|
export interface StateSetting {
|
|
@@ -51,12 +54,15 @@ export interface Details {
|
|
|
51
54
|
/** Run settings written in one place; a key that is missing is not set there. */
|
|
52
55
|
export interface RunSettings {
|
|
53
56
|
timeout?: number;
|
|
54
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* A retry setting replaces the whole `retry` of the describes around, delay and condition included; null when it is
|
|
59
|
+
* set to a value the runner can't use.
|
|
60
|
+
*/
|
|
55
61
|
retry?: {
|
|
56
62
|
count: number;
|
|
57
63
|
delay: number | null;
|
|
58
64
|
condition: string | null;
|
|
59
|
-
};
|
|
65
|
+
} | null;
|
|
60
66
|
repeats?: number;
|
|
61
67
|
/** Playwright `test.slow()`: the conditions of the calls, any of which makes the test slow; null when one always does. */
|
|
62
68
|
slow?: {
|
|
@@ -77,8 +83,15 @@ interface Declaration {
|
|
|
77
83
|
isParameterized: boolean;
|
|
78
84
|
/** Number of cases of `.each` or `.for` when the table is written out; null otherwise. */
|
|
79
85
|
caseCount: number | null;
|
|
86
|
+
/** The cases of `.each` or `.for` when the scan can work out every value in them; null otherwise. */
|
|
87
|
+
cases: TableCase[] | null;
|
|
80
88
|
problems: Problem[];
|
|
81
89
|
}
|
|
90
|
+
/** One case of a `.each` or `.for` table: the items its title is formatted with, and the arguments its callback gets. */
|
|
91
|
+
export interface TableCase {
|
|
92
|
+
items: Value[];
|
|
93
|
+
args: Value[];
|
|
94
|
+
}
|
|
82
95
|
export interface TestDeclaration extends Declaration {
|
|
83
96
|
kind: 'test';
|
|
84
97
|
title: Title;
|
|
@@ -90,6 +103,8 @@ export interface TestDeclaration extends Declaration {
|
|
|
90
103
|
takesAllFixtures: boolean;
|
|
91
104
|
/** Uses Vitest 5's `bench` fixture. */
|
|
92
105
|
usesBench: boolean;
|
|
106
|
+
/** Vitest: the state options the test sets itself, to `false` too, which replace those of its tags. */
|
|
107
|
+
ownStateOptions?: StateOption[];
|
|
93
108
|
}
|
|
94
109
|
export interface SuiteDeclaration extends Declaration {
|
|
95
110
|
kind: 'describe';
|
|
@@ -186,6 +201,8 @@ export interface Returns {
|
|
|
186
201
|
/** A module read to see whether it exports a custom `test` object. */
|
|
187
202
|
export interface ModuleSource {
|
|
188
203
|
code: string;
|
|
204
|
+
/** The file's bytes when it isn't UTF-8 text, such as an image. */
|
|
205
|
+
bytes?: Uint8Array;
|
|
189
206
|
relativeFilePath: string;
|
|
190
207
|
/** Reads the modules this module imports in turn. */
|
|
191
208
|
readModule: ModuleReader;
|
package/dist/options.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface ScanOptions {
|
|
|
17
17
|
frameworkVersion?: string;
|
|
18
18
|
/** Which tests get the comments above them. Defaults to `non-active`. */
|
|
19
19
|
comments?: CommentsMode;
|
|
20
|
+
/**
|
|
21
|
+
* One test or describe per case of a `.each` or `.for` table and per pass of a loop whose values the scan can work
|
|
22
|
+
* out, each with the title the runner gives it. Defaults to `false`: one record per declaration.
|
|
23
|
+
*/
|
|
24
|
+
expandCases?: boolean;
|
|
20
25
|
}
|
|
21
26
|
/** @inline */
|
|
22
27
|
export interface ScanSourceOptions {
|
|
@@ -28,6 +33,11 @@ export interface ScanSourceOptions {
|
|
|
28
33
|
frameworkVersion?: string | null;
|
|
29
34
|
/** Which tests get the comments above them. Defaults to `non-active`. */
|
|
30
35
|
comments?: CommentsMode;
|
|
36
|
+
/**
|
|
37
|
+
* One test or describe per case of a `.each` or `.for` table and per pass of a loop whose values the scan can work
|
|
38
|
+
* out, each with the title the runner gives it. Defaults to `false`: one record per declaration.
|
|
39
|
+
*/
|
|
40
|
+
expandCases?: boolean;
|
|
31
41
|
}
|
|
32
42
|
/** An invalid option, told apart from other errors so that the command line can show it as a usage error. */
|
|
33
43
|
export declare class OptionsError extends TypeError {
|
|
@@ -36,6 +46,7 @@ export declare function invalid(option: string, expected: string): OptionsError;
|
|
|
36
46
|
export declare function checkObject(value: unknown, name: string): Record<string, unknown>;
|
|
37
47
|
export declare function checkFramework(value: unknown, option: string): Framework;
|
|
38
48
|
export declare function checkComments(value: unknown, option: string): CommentsMode;
|
|
49
|
+
export declare function checkFlag(value: unknown, option: string): boolean;
|
|
39
50
|
export declare function checkVersion(value: unknown, option: string): string | null;
|
|
40
51
|
export declare function checkString(value: unknown, option: string): string;
|
|
41
52
|
export declare function checkStrings(value: unknown, option: string, required: boolean): string[];
|
package/dist/options.js
CHANGED
|
@@ -25,6 +25,13 @@ export function checkComments(value, option) {
|
|
|
25
25
|
throw invalid(option, '"all", "non-active" or "none"');
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
+
export function checkFlag(value, option) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return false;
|
|
31
|
+
if (typeof value !== 'boolean')
|
|
32
|
+
throw invalid(option, 'true or false');
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
28
35
|
export function checkVersion(value, option) {
|
|
29
36
|
if (value === undefined || value === null)
|
|
30
37
|
return null;
|
package/dist/parse.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface ParsedFile {
|
|
|
15
15
|
end: number;
|
|
16
16
|
} | null;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The offset of the first bracket nested more than `limit` deep, or null. Parentheses, brackets, braces and template
|
|
20
|
+
* substitutions count; strings, comments, regular expressions and template text are skipped. A closing bracket closes
|
|
21
|
+
* up to the one it matches, so a literal read wrongly unbalances only the brackets around it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function tooDeeplyNested(text: string, limit: number): number | null;
|
|
18
24
|
/** The first parser error, located at its first label, or at the start of the file when it has none. */
|
|
19
25
|
export declare function firstError(errors: readonly OxcError[]): ParsedFile['error'];
|
|
20
26
|
export declare function parseFile(fileName: string, code: string): ParsedFile;
|