test-inventory 0.7.0 → 0.8.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 +51 -0
- package/README.md +4 -3
- package/dist/case-titles.d.ts +13 -0
- package/dist/case-titles.js +102 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -5
- package/dist/config-dependencies.js +8 -3
- package/dist/config-runs.d.ts +22 -0
- package/dist/config-runs.js +263 -3
- package/dist/config.d.ts +18 -1
- package/dist/config.js +160 -14
- package/dist/dependency-hash.d.ts +13 -5
- package/dist/dependency-hash.js +56 -12
- package/dist/diagnostics.js +2 -0
- package/dist/extract.js +53 -19
- package/dist/files.js +3 -2
- package/dist/framework-version.d.ts +9 -0
- package/dist/framework-version.js +47 -2
- package/dist/frameworks/bindings.d.ts +14 -0
- package/dist/frameworks/bindings.js +99 -6
- package/dist/frameworks/common.js +7 -0
- package/dist/frameworks/jest.d.ts +46 -0
- package/dist/frameworks/jest.js +442 -0
- package/dist/frameworks/test-functions.d.ts +169 -0
- package/dist/frameworks/test-functions.js +492 -0
- package/dist/frameworks/vitest.d.ts +7 -62
- package/dist/frameworks/vitest.js +37 -388
- package/dist/imported-values.js +15 -1
- package/dist/model.d.ts +5 -2
- package/dist/options.d.ts +5 -1
- package/dist/options.js +2 -2
- package/dist/parse.js +17 -2
- package/dist/records.js +46 -11
- package/dist/resolve.js +39 -36
- package/dist/scan-file.js +7 -3
- package/dist/scan.js +21 -6
- package/dist/types.d.ts +10 -4
- package/dist/values.d.ts +10 -0
- package/dist/values.js +107 -2
- package/package.json +1 -1
- package/schema/v1.json +5 -3
|
@@ -1,71 +1,18 @@
|
|
|
1
|
-
import type { CallExpression
|
|
2
|
-
import type {
|
|
1
|
+
import type { CallExpression } from 'oxc-parser';
|
|
2
|
+
import type { ApiCall, CallContext, ModuleReader } from '../model.ts';
|
|
3
3
|
import type { ParsedFile } from '../parse.ts';
|
|
4
|
+
import type { HookCounts } from '../types.ts';
|
|
4
5
|
import { type FileContext, type Link } from './common.ts';
|
|
5
|
-
type Kind
|
|
6
|
-
|
|
7
|
-
interface TestFunction {
|
|
8
|
-
kind: Kind;
|
|
9
|
-
links: Link[];
|
|
10
|
-
}
|
|
11
|
-
export declare class VitestAdapter implements Adapter {
|
|
12
|
-
readonly problems: Problem[];
|
|
13
|
-
readonly fileTags: string[];
|
|
14
|
-
readonly fileTagSpans: Span[];
|
|
15
|
-
usesGlobals: boolean;
|
|
16
|
-
private readonly file;
|
|
17
|
-
private readonly readModule;
|
|
18
|
-
private readonly bindings;
|
|
19
|
-
private readonly imports;
|
|
20
|
-
private readonly followed;
|
|
21
|
-
private readonly reported;
|
|
22
|
-
/** Variables and names of functions read as test objects on trust, each reported once. */
|
|
23
|
-
private readonly assumed;
|
|
24
|
-
private readonly aliases;
|
|
25
|
-
/** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
|
|
26
|
-
private readonly suiteParameters;
|
|
6
|
+
import { SuiteAdapter, type Kind } from './test-functions.ts';
|
|
7
|
+
export declare class VitestAdapter extends SuiteAdapter {
|
|
27
8
|
private warnedAboutTags;
|
|
28
9
|
constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
|
|
10
|
+
protected isAliasLink(kind: Kind, link: Link): boolean;
|
|
29
11
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
30
|
-
testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): TestFunction | null;
|
|
31
12
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
32
13
|
private benchCall;
|
|
33
14
|
private chainCall;
|
|
34
|
-
|
|
35
|
-
* The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
|
|
36
|
-
* followed is reported only outside test and hook bodies, where a call can declare tests.
|
|
37
|
-
*/
|
|
38
|
-
private resolve;
|
|
39
|
-
private start;
|
|
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
|
-
*/
|
|
45
|
-
private alias;
|
|
46
|
-
private aliasOf;
|
|
47
|
-
/**
|
|
48
|
-
* The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
|
|
49
|
-
* function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
|
|
50
|
-
* named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
|
|
51
|
-
*/
|
|
52
|
-
private returned;
|
|
53
|
-
/** Whether a local variable is read as a test object it could be; see `localTestObject`. */
|
|
54
|
-
private assumeLocal;
|
|
55
|
-
private looksLikeTest;
|
|
56
|
-
/**
|
|
57
|
-
* Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
|
|
58
|
-
* title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
|
|
59
|
-
* a package extends.
|
|
60
|
-
*/
|
|
61
|
-
private worthFollowing;
|
|
62
|
-
/** Vitest as followed modules are read, with the rules of the scanned version. */
|
|
63
|
-
private framework;
|
|
64
|
-
/** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
|
|
65
|
-
private follow;
|
|
66
|
-
/** Hooks called directly, such as `beforeEach(...)` imported from Vitest or used as a global. */
|
|
67
|
-
private hookCall;
|
|
68
|
-
private hook;
|
|
15
|
+
protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
|
|
69
16
|
private contextSkip;
|
|
70
17
|
/**
|
|
71
18
|
* `annotate(message, type?)` from the test context (3.2), as an annotation of type `notice` when no type is given; the
|
|
@@ -89,7 +36,5 @@ export declare class VitestAdapter implements Adapter {
|
|
|
89
36
|
* gives one object per row, keyed by the names in its first line.
|
|
90
37
|
*/
|
|
91
38
|
private tableCases;
|
|
92
|
-
private arrayLength;
|
|
93
39
|
private tagsUsed;
|
|
94
40
|
}
|
|
95
|
-
export {};
|
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
|
|
2
2
|
import { snippet } from "../source.js";
|
|
3
|
-
import {
|
|
4
|
-
import { atLeast, hookFunction,
|
|
5
|
-
|
|
6
|
-
function shared(a, b) {
|
|
7
|
-
if (!a || a.kind !== b?.kind)
|
|
8
|
-
return null;
|
|
9
|
-
const same = (link, index) => {
|
|
10
|
-
const other = b.links.at(index);
|
|
11
|
-
return other?.name === link.name && !link.args && !link.template && !other.args && !other.template;
|
|
12
|
-
};
|
|
13
|
-
const differs = a.links.findIndex((link, index) => !same(link, index));
|
|
14
|
-
return { kind: a.kind, links: differs === -1 ? a.links : a.links.slice(0, differs) };
|
|
15
|
-
}
|
|
16
|
-
// Maps rather than object literals, so names such as `constructor` don't match inherited properties.
|
|
17
|
-
const KINDS = new Map([
|
|
18
|
-
['test', 'test'],
|
|
19
|
-
['it', 'test'],
|
|
20
|
-
['describe', 'describe'],
|
|
21
|
-
['suite', 'describe'],
|
|
22
|
-
]);
|
|
23
|
-
const MISMATCHED_SOURCES = new Set(['@playwright/test', 'playwright/test', '@jest/globals']);
|
|
3
|
+
import { vitestCaseTitle } from "../case-titles.js";
|
|
4
|
+
import { atLeast, hookFunction, problem, readChain, readCondition, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, origin, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
|
|
5
|
+
import { SuiteAdapter } from "./test-functions.js";
|
|
24
6
|
const MODIFIERS = {
|
|
25
7
|
test: new Set(['concurrent', 'sequential', 'only', 'skip', 'todo', 'fails']),
|
|
26
8
|
describe: new Set(['concurrent', 'sequential', 'shuffle', 'skip', 'only', 'todo']),
|
|
@@ -44,7 +26,25 @@ function isHook(name) {
|
|
|
44
26
|
/** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
|
|
45
27
|
const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
|
|
46
28
|
/** First members that show a name imported from a local file is used as Vitest's `test`. */
|
|
47
|
-
const TEST_MEMBERS =
|
|
29
|
+
const TEST_MEMBERS = [...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS];
|
|
30
|
+
const VITEST = {
|
|
31
|
+
name: 'Vitest',
|
|
32
|
+
source: 'vitest',
|
|
33
|
+
mismatched: new Set(['@playwright/test', 'playwright/test', '@jest/globals']),
|
|
34
|
+
starts: new Map([
|
|
35
|
+
['test', { kind: 'test', links: [] }],
|
|
36
|
+
['it', { kind: 'test', links: [] }],
|
|
37
|
+
['describe', { kind: 'describe', links: [] }],
|
|
38
|
+
['suite', { kind: 'describe', links: [] }],
|
|
39
|
+
]),
|
|
40
|
+
hooks: new Set(HOOKS.keys()),
|
|
41
|
+
derivations: DERIVATIONS,
|
|
42
|
+
modifiers: new Set([...MODIFIERS.test, ...MODIFIERS.describe]),
|
|
43
|
+
carriesKinds: true,
|
|
44
|
+
isModule: isImportMetaVitest,
|
|
45
|
+
defaultImport: "Vitest has no default export; import { test, describe } from 'vitest' instead.",
|
|
46
|
+
required: 'Vitest cannot be loaded with require(); tests that use it are missing.',
|
|
47
|
+
};
|
|
48
48
|
/** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
|
|
49
49
|
const HOISTED_OBJECTS = new Set(['vi', 'vitest']);
|
|
50
50
|
const HOISTED_METHODS = new Set(['mock', 'unmock', 'hoisted']);
|
|
@@ -112,186 +112,16 @@ function fixturesOf(fn, index, version) {
|
|
|
112
112
|
return !since || !atLeast(version, ...since);
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
class VitestBindings {
|
|
117
|
-
kinds = new Map();
|
|
118
|
-
hooks = new Map();
|
|
119
|
-
namespaces = new Set();
|
|
120
|
-
/** Names `bench` is imported as from Vitest 5, which no longer exports it. */
|
|
121
|
-
benches = new Set();
|
|
122
|
-
/** The import specifiers and declarators that bind those names, to tell them from local names that shadow them. */
|
|
123
|
-
declarations = new Set();
|
|
124
|
-
problems = [];
|
|
125
|
-
/** Names imported from other modules, which may be test objects there. */
|
|
126
|
-
imported;
|
|
127
|
-
follow;
|
|
128
|
-
/** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
|
|
129
|
-
functions;
|
|
130
|
-
constructor(program, version, follow) {
|
|
131
|
-
this.imported = moduleImports(program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
132
|
-
this.follow = follow;
|
|
133
|
-
this.functions = new ModuleFunctions(program, this.imported, follow);
|
|
134
|
-
for (const statement of program.body) {
|
|
135
|
-
if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
|
|
136
|
-
continue;
|
|
137
|
-
const source = statement.source.value;
|
|
138
|
-
if (MISMATCHED_SOURCES.has(source)) {
|
|
139
|
-
this.problems.push(problem('framework-mismatch', statement, `The file imports '${source}', but the scan is for Vitest.`));
|
|
140
|
-
}
|
|
141
|
-
if (source !== 'vitest')
|
|
142
|
-
continue;
|
|
143
|
-
for (const specifier of statement.specifiers) {
|
|
144
|
-
this.declarations.add(specifier);
|
|
145
|
-
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
146
|
-
this.namespaces.add(specifier.local.name);
|
|
147
|
-
}
|
|
148
|
-
else if (specifier.type === 'ImportDefaultSpecifier') {
|
|
149
|
-
this.problems.push(problem('invalid-import', specifier, "Vitest has no default export; import { test, describe } from 'vitest' instead."));
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
this.bindImported(exportName(specifier.imported), specifier.local.name, version);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const declarators = allDeclarators(program);
|
|
157
|
-
for (const declarator of declarators) {
|
|
158
|
-
if (declarator.init && requiredModule(declarator.init) === 'vitest') {
|
|
159
|
-
this.problems.push(problem('invalid-import', declarator, 'Vitest cannot be loaded with require(); tests that use it are missing.'));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
bindAll(declarators, (declarator) => {
|
|
163
|
-
const bound = this.bind(declarator);
|
|
164
|
-
if (bound)
|
|
165
|
-
this.declarations.add(declarator);
|
|
166
|
-
return bound;
|
|
167
|
-
});
|
|
168
|
-
this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
|
|
169
|
-
}
|
|
170
|
-
isTestName(name) {
|
|
171
|
-
return this.kinds.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
|
|
175
|
-
* a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
|
|
176
|
-
* follows in their scopes.
|
|
177
|
-
*/
|
|
178
|
-
isTestObject(expression, followsCalls = true) {
|
|
179
|
-
const node = unwrap(expression);
|
|
180
|
-
if (node.type === 'Identifier') {
|
|
181
|
-
const { name } = node;
|
|
182
|
-
return (this.kinds.get(name) === 'test' || this.importsTestObject(name) || (followsCalls && this.holdsTestObject(name)));
|
|
183
|
-
}
|
|
184
|
-
if (node.type === 'MemberExpression') {
|
|
185
|
-
return KINDS.get(propertyName(node) ?? '') === 'test' && this.isNamespace(node.object);
|
|
186
|
-
}
|
|
187
|
-
// `process.env.CI ? vitestTest : skippedTest` is a test object either way.
|
|
188
|
-
if (node.type === 'ConditionalExpression') {
|
|
189
|
-
return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
|
|
190
|
-
}
|
|
191
|
-
if (node.type !== 'CallExpression')
|
|
192
|
-
return false;
|
|
193
|
-
const callee = unwrap(node.callee);
|
|
194
|
-
// `defineBrokerTest()`, a function that returns a test object.
|
|
195
|
-
if (callee.type === 'Identifier')
|
|
196
|
-
return followsCalls && this.returnsTestObject(callee.name);
|
|
197
|
-
return (callee.type === 'MemberExpression' &&
|
|
198
|
-
DERIVATIONS.has(propertyName(callee) ?? '') &&
|
|
199
|
-
this.isTestObject(callee.object, followsCalls));
|
|
200
|
-
}
|
|
201
|
-
returnsTestObject(name) {
|
|
202
|
-
return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
|
|
203
|
-
}
|
|
204
|
-
holdsTestObject(name) {
|
|
205
|
-
return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
|
|
206
|
-
}
|
|
207
|
-
reexports(source, imported, exported) {
|
|
208
|
-
if (source !== 'vitest')
|
|
209
|
-
return this.follow(source, imported, exported);
|
|
210
|
-
return exported === 'test object' && KINDS.has(imported);
|
|
211
|
-
}
|
|
212
|
-
/** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
|
|
213
|
-
importsTestObject(name) {
|
|
214
|
-
const from = this.imported.get(name);
|
|
215
|
-
return from !== undefined && this.follow(from.source, from.imported);
|
|
216
|
-
}
|
|
217
|
-
isNamespace(expression) {
|
|
218
|
-
const node = unwrap(expression);
|
|
219
|
-
return node.type === 'Identifier' && this.namespaces.has(node.name);
|
|
220
|
-
}
|
|
221
|
-
bindImported(imported, local, version) {
|
|
222
|
-
const kind = KINDS.get(imported);
|
|
223
|
-
if (kind) {
|
|
224
|
-
this.kinds.set(local, kind);
|
|
225
|
-
}
|
|
226
|
-
else if (isHook(imported)) {
|
|
227
|
-
this.hooks.set(local, imported);
|
|
228
|
-
}
|
|
229
|
-
else if (imported === 'bench' && atLeast(version, 5, 0)) {
|
|
230
|
-
this.benches.add(local);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
bind(declarator) {
|
|
234
|
-
const { id, init } = declarator;
|
|
235
|
-
if (!init)
|
|
236
|
-
return false;
|
|
237
|
-
if (id.type === 'Identifier' && this.isTestObject(init, false)) {
|
|
238
|
-
this.kinds.set(id.name, 'test');
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
if (id.type !== 'ObjectPattern' || !isImportMetaVitest(init))
|
|
242
|
-
return false;
|
|
243
|
-
// In-source tests: const { it, describe } = import.meta.vitest
|
|
244
|
-
for (const property of id.properties) {
|
|
245
|
-
if (property.type === 'RestElement' || property.value.type !== 'Identifier')
|
|
246
|
-
continue;
|
|
247
|
-
const key = propertyKey(property);
|
|
248
|
-
const kind = KINDS.get(key ?? '');
|
|
249
|
-
if (kind)
|
|
250
|
-
this.kinds.set(property.value.name, kind);
|
|
251
|
-
if (key !== null && isHook(key))
|
|
252
|
-
this.hooks.set(property.value.name, key);
|
|
253
|
-
}
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
export class VitestAdapter {
|
|
258
|
-
problems;
|
|
259
|
-
fileTags = [];
|
|
260
|
-
fileTagSpans = [];
|
|
261
|
-
usesGlobals = false;
|
|
262
|
-
file;
|
|
263
|
-
readModule;
|
|
264
|
-
bindings;
|
|
265
|
-
imports;
|
|
266
|
-
followed = new Map();
|
|
267
|
-
reported = new Set();
|
|
268
|
-
/** Variables and names of functions read as test objects on trust, each reported once. */
|
|
269
|
-
assumed = new Set();
|
|
270
|
-
aliases = new Map();
|
|
271
|
-
/** Describe callbacks and the name of their first parameter, which Vitest calls with its `test`. */
|
|
272
|
-
suiteParameters = new Map();
|
|
115
|
+
export class VitestAdapter extends SuiteAdapter {
|
|
273
116
|
warnedAboutTags = false;
|
|
274
117
|
constructor(parsed, file, readModule) {
|
|
275
|
-
|
|
276
|
-
this.readModule = readModule;
|
|
277
|
-
// A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
|
|
278
|
-
// a call through the import itself is what reports an import that can't be followed.
|
|
279
|
-
const followed = new Map();
|
|
280
|
-
this.bindings = new VitestBindings(parsed.program, file.version, (source, imported, exported = 'test object') => {
|
|
281
|
-
const key = `${source}\0${exported}\0${imported}`;
|
|
282
|
-
if (!followed.has(key)) {
|
|
283
|
-
followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
|
|
284
|
-
}
|
|
285
|
-
return followed.get(key) === true;
|
|
286
|
-
});
|
|
287
|
-
this.problems = [...this.bindings.problems];
|
|
118
|
+
super(parsed, file, readModule, VITEST, TEST_MEMBERS);
|
|
288
119
|
if (atLeast(file.version, 5, 0)) {
|
|
289
120
|
for (const call of nestedHoistedCalls(parsed.program)) {
|
|
290
121
|
const callee = snippet(text(this.file, call.callee));
|
|
291
122
|
this.problems.push(problem('removed-api', call, `Vitest 5 fails a file that calls ${callee}(...) anywhere but its top level, because the call is hoisted there.`, callee));
|
|
292
123
|
}
|
|
293
124
|
}
|
|
294
|
-
this.imports = moduleImports(parsed.program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
295
125
|
if (atLeast(file.version, 4, 1)) {
|
|
296
126
|
// Vitest's own pattern, over the whole text as Vitest reads it, so the same tags come out.
|
|
297
127
|
for (const match of parsed.source.text.matchAll(/(\/\/|\*)\s*@module-tag\s+([\w\-/]+)\b/g)) {
|
|
@@ -302,19 +132,21 @@ export class VitestAdapter {
|
|
|
302
132
|
}
|
|
303
133
|
}
|
|
304
134
|
}
|
|
135
|
+
isAliasLink(kind, link) {
|
|
136
|
+
return MODIFIERS[kind].has(link.name) || TABLES.has(link.name) || CONDITIONS.has(link.name);
|
|
137
|
+
}
|
|
305
138
|
classify(call, context) {
|
|
306
139
|
return (this.benchCall(call, context) ??
|
|
307
140
|
this.hookCall(call, context) ??
|
|
308
141
|
this.contextSkip(call, context) ??
|
|
309
142
|
this.chainCall(call, context));
|
|
310
143
|
}
|
|
311
|
-
testFunction(expression, scopes, returnsOf) {
|
|
312
|
-
return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
|
|
313
|
-
}
|
|
314
144
|
/** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
|
|
315
145
|
benchCall(call, context) {
|
|
316
146
|
const base = readChain(call.callee)?.base;
|
|
317
|
-
if (base?.type !== 'Identifier' ||
|
|
147
|
+
if (base?.type !== 'Identifier' || this.bindings.members.get(base.name) !== 'bench')
|
|
148
|
+
return null;
|
|
149
|
+
if (!atLeast(this.file.version, 5, 0))
|
|
318
150
|
return null;
|
|
319
151
|
const declaration = this.file.values.declarationOf(base.name, context.scopes);
|
|
320
152
|
if (declaration === null || !this.bindings.declarations.has(declaration))
|
|
@@ -358,176 +190,6 @@ export class VitestAdapter {
|
|
|
358
190
|
// Which of its parts the chain means is not known, so it declares a plain test or describe.
|
|
359
191
|
return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
|
|
360
192
|
}
|
|
361
|
-
/**
|
|
362
|
-
* The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
|
|
363
|
-
* followed is reported only outside test and hook bodies, where a call can declare tests.
|
|
364
|
-
*/
|
|
365
|
-
resolve(chain, args, lookup) {
|
|
366
|
-
const start = this.start(chain, args, lookup);
|
|
367
|
-
if (!start)
|
|
368
|
-
return null;
|
|
369
|
-
let kind = start.kind;
|
|
370
|
-
let links = [...(start.modifiers ?? []), ...chain.links.slice(start.index)];
|
|
371
|
-
// A test object derived inline, as in `test.extend({ ... }).skip(...)`.
|
|
372
|
-
links = links.slice(links.findLastIndex((link) => DERIVATIONS.has(link.name) && link.args !== null) + 1);
|
|
373
|
-
// Vitest's test object also carries `describe` and `suite`, and they carry `test` and `it`.
|
|
374
|
-
while (links.length > 0 && KINDS.has(links[0].name) && !links[0].args) {
|
|
375
|
-
kind = KINDS.get(links[0].name);
|
|
376
|
-
links = links.slice(1);
|
|
377
|
-
}
|
|
378
|
-
return { kind, links };
|
|
379
|
-
}
|
|
380
|
-
start(chain, args, lookup) {
|
|
381
|
-
const { base, links } = chain;
|
|
382
|
-
const { scopes, inBody } = lookup;
|
|
383
|
-
if (base.type === 'CallExpression') {
|
|
384
|
-
const returned = this.returned(base, links, args, lookup);
|
|
385
|
-
return returned && { kind: returned.kind, index: 0, modifiers: returned.links };
|
|
386
|
-
}
|
|
387
|
-
if (base.type !== 'Identifier')
|
|
388
|
-
return null;
|
|
389
|
-
// A name the file doesn't declare is one of Vitest's globals; a local variable or parameter with the same name,
|
|
390
|
-
// such as `(test) => test.result()`, is not Vitest's.
|
|
391
|
-
const declaration = this.file.values.declarationOf(base.name, scopes);
|
|
392
|
-
if (declaration === null) {
|
|
393
|
-
const global = KINDS.get(base.name);
|
|
394
|
-
if (global)
|
|
395
|
-
this.usesGlobals = true;
|
|
396
|
-
return global ? { kind: global, index: 0 } : null;
|
|
397
|
-
}
|
|
398
|
-
// A parameter's declaration is its function: `describe('market orders', (test) => { test(...) })`.
|
|
399
|
-
if (this.suiteParameters.get(declaration) === base.name)
|
|
400
|
-
return { kind: 'test', index: 0 };
|
|
401
|
-
const followed = this.imports.get(base.name)?.node === declaration &&
|
|
402
|
-
this.worthFollowing(base.name, links, args) &&
|
|
403
|
-
this.follow(base.name, !inBody && mayDeclareTests(base.name, links));
|
|
404
|
-
if (!followed && !this.bindings.declarations.has(declaration)) {
|
|
405
|
-
const alias = declaration.type === 'VariableDeclarator' ? this.alias(declaration, base.name, lookup) : null;
|
|
406
|
-
if (alias)
|
|
407
|
-
return { kind: alias.kind, index: 0, modifiers: alias.links };
|
|
408
|
-
return this.assumeLocal(declaration, base.name, links, args)
|
|
409
|
-
? { kind: KINDS.get(base.name) ?? 'test', index: 0 }
|
|
410
|
-
: null;
|
|
411
|
-
}
|
|
412
|
-
if (this.bindings.namespaces.has(base.name)) {
|
|
413
|
-
const kind = KINDS.get(links.at(0)?.name ?? '');
|
|
414
|
-
return kind ? { kind, index: 1 } : null;
|
|
415
|
-
}
|
|
416
|
-
const kind = this.bindings.kinds.get(base.name);
|
|
417
|
-
return kind ? { kind, index: 0 } : null;
|
|
418
|
-
}
|
|
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;
|
|
428
|
-
let alias = this.aliases.get(declarator);
|
|
429
|
-
if (alias === undefined) {
|
|
430
|
-
// Set first, so a variable defined through itself ends the lookup.
|
|
431
|
-
this.aliases.set(declarator, null);
|
|
432
|
-
alias = declarator.init ? this.aliasOf(declarator.init, lookup) : null;
|
|
433
|
-
this.aliases.set(declarator, alias);
|
|
434
|
-
}
|
|
435
|
-
const links = destructured.map((member) => ({ name: member, args: null, template: null }));
|
|
436
|
-
return alias && { kind: alias.kind, links: [...alias.links, ...links] };
|
|
437
|
-
}
|
|
438
|
-
aliasOf(expression, lookup) {
|
|
439
|
-
const node = unwrap(expression);
|
|
440
|
-
if (node.type === 'ConditionalExpression') {
|
|
441
|
-
// `isCI ? describe.concurrent : describe`: only the modifiers both branches have apply.
|
|
442
|
-
return shared(this.aliasOf(node.consequent, lookup), this.aliasOf(node.alternate, lookup));
|
|
443
|
-
}
|
|
444
|
-
const chain = readChain(node);
|
|
445
|
-
const resolved = chain ? this.resolve(chain, [], lookup) : null;
|
|
446
|
-
const allowed = (link) => resolved !== null &&
|
|
447
|
-
(MODIFIERS[resolved.kind].has(link.name) || TABLES.has(link.name) || CONDITIONS.has(link.name));
|
|
448
|
-
return resolved?.links.every(allowed) ? resolved : null;
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* The function a call returns, as `conditionalTest({ min: 22 })` returns `it` or `it.skip`: what all the values the
|
|
452
|
-
* function returns have in common, as for a variable chosen by a condition. When the function can't be found, a call
|
|
453
|
-
* named like a test object is read as one on trust, with a warning; see `returnedTestObject`.
|
|
454
|
-
*/
|
|
455
|
-
returned(call, links, args, lookup) {
|
|
456
|
-
const callee = unwrap(call.callee);
|
|
457
|
-
if (callee.type !== 'Identifier')
|
|
458
|
-
return null;
|
|
459
|
-
const returns = lookup.returnsOf(callee.name, lookup.scopes);
|
|
460
|
-
if (returns)
|
|
461
|
-
return returnedFunction(returns, shared);
|
|
462
|
-
if (!isTestName(callee.name) || !this.looksLikeTest(links, args))
|
|
463
|
-
return null;
|
|
464
|
-
if (!this.assumed.has(callee.name)) {
|
|
465
|
-
this.assumed.add(callee.name);
|
|
466
|
-
this.problems.push(returnedTestObject(this.file, call));
|
|
467
|
-
}
|
|
468
|
-
return { kind: KINDS.get(callee.name) ?? 'test', links: [] };
|
|
469
|
-
}
|
|
470
|
-
/** Whether a local variable is read as a test object it could be; see `localTestObject`. */
|
|
471
|
-
assumeLocal(declaration, name, links, args) {
|
|
472
|
-
if (declaration.type !== 'VariableDeclarator' || !mayHoldTestObject(declaration) || !isTestName(name))
|
|
473
|
-
return false;
|
|
474
|
-
if (!this.looksLikeTest(links, args))
|
|
475
|
-
return false;
|
|
476
|
-
if (!this.assumed.has(declaration)) {
|
|
477
|
-
this.assumed.add(declaration);
|
|
478
|
-
this.problems.push(localTestObject(this.file, name, declaration));
|
|
479
|
-
}
|
|
480
|
-
return true;
|
|
481
|
-
}
|
|
482
|
-
looksLikeTest(links, args) {
|
|
483
|
-
return links.length === 0 ? looksLikeDeclaration(args) : TEST_MEMBERS.has(links[0].name);
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* Whether a call through an imported name is worth following to the module: shaped like a test, or any call with a
|
|
487
|
-
* title and a body through a name that reads like a test function, as `it.effect('title', fn)` on a test object that
|
|
488
|
-
* a package extends.
|
|
489
|
-
*/
|
|
490
|
-
worthFollowing(name, links, args) {
|
|
491
|
-
return this.looksLikeTest(links, args) || (isTestName(name) && looksLikeDeclaration(args));
|
|
492
|
-
}
|
|
493
|
-
/** Vitest as followed modules are read, with the rules of the scanned version. */
|
|
494
|
-
framework() {
|
|
495
|
-
const version = this.file.version;
|
|
496
|
-
return {
|
|
497
|
-
name: 'Vitest',
|
|
498
|
-
isSource: (source) => source === 'vitest',
|
|
499
|
-
testObjects: (program, follow) => new VitestBindings(program, version, follow),
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
/** Follows the import of `name` once, and reports it once when it can't be followed and `report` is set. */
|
|
503
|
-
follow(name, report) {
|
|
504
|
-
const imported = this.imports.get(name);
|
|
505
|
-
let result = this.followed.get(name);
|
|
506
|
-
if (!result) {
|
|
507
|
-
result = followImport(name, imported, this.readModule, this.file, this.framework());
|
|
508
|
-
this.followed.set(name, result);
|
|
509
|
-
if (result.exported) {
|
|
510
|
-
this.bindings.kinds.set(name, KINDS.get(imported.imported) ?? 'test');
|
|
511
|
-
this.bindings.declarations.add(imported.node);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
if (report && result.problem && !this.reported.has(name)) {
|
|
515
|
-
this.reported.add(name);
|
|
516
|
-
this.problems.push(result.problem);
|
|
517
|
-
}
|
|
518
|
-
return result.exported;
|
|
519
|
-
}
|
|
520
|
-
/** Hooks called directly, such as `beforeEach(...)` imported from Vitest or used as a global. */
|
|
521
|
-
hookCall(call, context) {
|
|
522
|
-
const callee = unwrap(call.callee);
|
|
523
|
-
if (callee.type !== 'Identifier')
|
|
524
|
-
return null;
|
|
525
|
-
const declaration = this.file.values.declarationOf(callee.name, context.scopes);
|
|
526
|
-
const hook = declaration === null
|
|
527
|
-
? isHook(callee.name) && callee.name
|
|
528
|
-
: this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
|
|
529
|
-
return hook ? this.hook(call, hook, context) : null;
|
|
530
|
-
}
|
|
531
193
|
hook(call, name, context) {
|
|
532
194
|
const fn = hookFunction(this.file, context.scopes, call.arguments);
|
|
533
195
|
const index = HOOKS.get(name);
|
|
@@ -703,7 +365,7 @@ export class VitestAdapter {
|
|
|
703
365
|
hashed: fn ?? namedFunction(this.file, context.scopes, body),
|
|
704
366
|
isParameterized: table !== null,
|
|
705
367
|
caseCount: table ? this.caseCount(call, table, context, problems) : null,
|
|
706
|
-
cases: table ? this.tableCases(table, context) : null,
|
|
368
|
+
cases: table ? this.tableCases(table, title, context) : null,
|
|
707
369
|
problems,
|
|
708
370
|
};
|
|
709
371
|
let declared;
|
|
@@ -901,7 +563,7 @@ export class VitestAdapter {
|
|
|
901
563
|
* array into the callback's arguments when every case is one, and `.for` passes each case whole. A tagged template
|
|
902
564
|
* gives one object per row, keyed by the names in its first line.
|
|
903
565
|
*/
|
|
904
|
-
tableCases(table, context) {
|
|
566
|
+
tableCases(table, title, context) {
|
|
905
567
|
let cases;
|
|
906
568
|
if (table.template) {
|
|
907
569
|
const values = table.template.expressions.map((item) => this.file.values.resolve(item, context.scopes));
|
|
@@ -921,32 +583,19 @@ export class VitestAdapter {
|
|
|
921
583
|
return null;
|
|
922
584
|
cases = resolved.value;
|
|
923
585
|
}
|
|
586
|
+
const caseTitle = (items, index) => title.isDynamic ? null : vitestCaseTitle(title.text, items, index, this.file.version);
|
|
924
587
|
if (table.name === 'for') {
|
|
925
|
-
return cases.map((item) => ({
|
|
926
|
-
|
|
588
|
+
return cases.map((item, index) => ({
|
|
589
|
+
title: caseTitle(item === null || item === undefined ? [] : Array.isArray(item) ? item : [item], index),
|
|
927
590
|
args: [item],
|
|
928
591
|
}));
|
|
929
592
|
}
|
|
930
593
|
const arraysOnly = cases.every((item) => Array.isArray(item));
|
|
931
|
-
return cases.map((item) => {
|
|
594
|
+
return cases.map((item, index) => {
|
|
932
595
|
const items = Array.isArray(item) ? item : [item];
|
|
933
|
-
return { items, args: arraysOnly ? items : [item] };
|
|
596
|
+
return { title: caseTitle(items, index), args: arraysOnly ? items : [item] };
|
|
934
597
|
});
|
|
935
598
|
}
|
|
936
|
-
arrayLength(node, context) {
|
|
937
|
-
if (node === undefined || node.type === 'SpreadElement')
|
|
938
|
-
return null;
|
|
939
|
-
const resolved = this.file.values.resolve(node, context.scopes);
|
|
940
|
-
if (resolved.ok && Array.isArray(resolved.value))
|
|
941
|
-
return resolved.value.length;
|
|
942
|
-
const expression = unwrap(node);
|
|
943
|
-
// The cases may hold values that can't be worked out, such as functions, while the list itself is written out.
|
|
944
|
-
if (expression.type === 'ArrayExpression' &&
|
|
945
|
-
expression.elements.every((element) => element?.type !== 'SpreadElement')) {
|
|
946
|
-
return expression.elements.length;
|
|
947
|
-
}
|
|
948
|
-
return null;
|
|
949
|
-
}
|
|
950
599
|
tagsUsed(node) {
|
|
951
600
|
if (this.warnedAboutTags)
|
|
952
601
|
return;
|
package/dist/imported-values.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ownExport, parseModule, starReexport } from "./frameworks/bindings.js";
|
|
1
|
+
import { commonJsExport, commonJsNames, ownExport, parseModule, starReexport } from "./frameworks/bindings.js";
|
|
2
2
|
import { Values } from "./values.js";
|
|
3
3
|
const UNRESOLVED = { ok: false, importedFrom: null };
|
|
4
4
|
/** How many modules deep a name is followed through re-exports and imports, as for test objects. */
|
|
@@ -81,6 +81,20 @@ export class ExportedValues {
|
|
|
81
81
|
if (own) {
|
|
82
82
|
return opened.values.resolveName(own.kind === 'specifier' ? own.local : name, [program]);
|
|
83
83
|
}
|
|
84
|
+
const commonJs = commonJsExport(program, name);
|
|
85
|
+
if (commonJs)
|
|
86
|
+
return opened.values.resolve(commonJs, [program]);
|
|
87
|
+
// What `require()` gives for a module that exports its names one by one: an object of them.
|
|
88
|
+
const names = name === 'default' ? commonJsNames(program) : [];
|
|
89
|
+
if (names.length > 0) {
|
|
90
|
+
const entries = names.map((item) => [item, opened.values.resolve(commonJsExport(program, item), [program])]);
|
|
91
|
+
if (!entries.every(([, value]) => value.ok))
|
|
92
|
+
return UNRESOLVED;
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
value: Object.fromEntries(entries.map(([key, value]) => [key, value.value])),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
84
98
|
// `export *` passes on every name but the default. When two modules export the name with different values, Node
|
|
85
99
|
// refuses to load the module and Vite's module runner keeps the first, so the value isn't known.
|
|
86
100
|
if (name === 'default')
|
package/dist/model.d.ts
CHANGED
|
@@ -87,9 +87,10 @@ interface Declaration {
|
|
|
87
87
|
cases: TableCase[] | null;
|
|
88
88
|
problems: Problem[];
|
|
89
89
|
}
|
|
90
|
-
/** One case of a `.each` or `.for` table: the
|
|
90
|
+
/** One case of a `.each` or `.for` table: the title the runner gives it, and the arguments its callback gets. */
|
|
91
91
|
export interface TableCase {
|
|
92
|
-
|
|
92
|
+
/** Null when the scan can't format it the way the runner does, as for a title that shows an object. */
|
|
93
|
+
title: string | null;
|
|
93
94
|
args: Value[];
|
|
94
95
|
}
|
|
95
96
|
export interface TestDeclaration extends Declaration {
|
|
@@ -139,6 +140,8 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
139
140
|
settings: RunSettings;
|
|
140
141
|
/** Source text of the first argument. */ argument: string | null;
|
|
141
142
|
problems: Problem[];
|
|
143
|
+
/** Jest's `jest.retryTimes()`, which applies to every test of the file; a later call replaces it. */
|
|
144
|
+
fileWide?: boolean;
|
|
142
145
|
} | {
|
|
143
146
|
kind: 'hook';
|
|
144
147
|
name: keyof HookCounts;
|
package/dist/options.d.ts
CHANGED
|
@@ -8,7 +8,11 @@ export interface ScanOptions {
|
|
|
8
8
|
* a dot too. Without them, the test files are those the configs in the root and the folders below it run.
|
|
9
9
|
*/
|
|
10
10
|
patterns?: string[];
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The runner whose tests are listed. Without it, the one the project uses: whose package a package.json lists, or
|
|
13
|
+
* whose config is in the root or below. With none of them or several, the scan throws a `TypeError` that names them.
|
|
14
|
+
*/
|
|
15
|
+
framework?: Framework;
|
|
12
16
|
/** The project root. Paths in the output are relative to it. Defaults to the current directory. */
|
|
13
17
|
root?: string;
|
|
14
18
|
/** Glob patterns to leave out, in addition to `node_modules` and `.git`. */
|
package/dist/options.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const FRAMEWORKS = ['playwright', 'vitest'];
|
|
1
|
+
const FRAMEWORKS = ['playwright', 'vitest', 'jest'];
|
|
2
2
|
const COMMENTS = ['all', 'non-active', 'none'];
|
|
3
3
|
// A prerelease and build metadata each appear once, so a long input can't make the match backtrack for long.
|
|
4
4
|
const VERSION = /^v?(\d+)\.(\d+)(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
@@ -15,7 +15,7 @@ export function checkObject(value, name) {
|
|
|
15
15
|
}
|
|
16
16
|
export function checkFramework(value, option) {
|
|
17
17
|
if (!FRAMEWORKS.includes(value))
|
|
18
|
-
throw invalid(option, '"playwright" or "
|
|
18
|
+
throw invalid(option, '"playwright", "vitest" or "jest"');
|
|
19
19
|
return value;
|
|
20
20
|
}
|
|
21
21
|
export function checkComments(value, option) {
|