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
package/dist/parse.js
CHANGED
|
@@ -222,10 +222,25 @@ export function parseFile(fileName, code) {
|
|
|
222
222
|
error: { message, start: tooDeep, end: tooDeep + 1 },
|
|
223
223
|
};
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
let result;
|
|
226
|
+
let program;
|
|
227
|
+
try {
|
|
228
|
+
result = parseSync(fileName, text, parserOptions(fileName));
|
|
229
|
+
// The tree is handed over when it is first read.
|
|
230
|
+
program = result.program;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
// The parser can fail to hand over the tree of invalid code, as for a JSX name written as a Unicode escape.
|
|
234
|
+
return {
|
|
235
|
+
source: new SourceText(text),
|
|
236
|
+
program: parseSync(fileName, '', parserOptions(fileName)).program,
|
|
237
|
+
comments: [],
|
|
238
|
+
error: { message: error.message, start: 0, end: 0 },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
226
241
|
return {
|
|
227
242
|
source: new SourceText(text),
|
|
228
|
-
program
|
|
243
|
+
program,
|
|
229
244
|
comments: result.comments,
|
|
230
245
|
error: firstError(result.errors),
|
|
231
246
|
};
|
package/dist/records.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
|
|
1
|
+
import { LEVELS, REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
|
|
2
2
|
import { anySlow, eitherCondition, noHooks, } from "./extract.js";
|
|
3
3
|
import { problem } from "./frameworks/common.js";
|
|
4
4
|
import { hashNodes, shortHash } from "./hash.js";
|
|
@@ -187,6 +187,8 @@ class RecordBuilder {
|
|
|
187
187
|
diagnostics = [];
|
|
188
188
|
/** The line of the first problem for which the runner refuses the file; null when it loads. */
|
|
189
189
|
rejectedAt = null;
|
|
190
|
+
/** Jest: the describes with a test of their own that uses `.only`, which then keep their mode from their others. */
|
|
191
|
+
focused = new Set();
|
|
190
192
|
constructor(parsed, options) {
|
|
191
193
|
this.parsed = parsed;
|
|
192
194
|
this.options = options;
|
|
@@ -203,12 +205,17 @@ class RecordBuilder {
|
|
|
203
205
|
...extraction.problems.filter((item) => tags === null || isTagMissing || item.code !== 'config-may-change-state'),
|
|
204
206
|
...undefinedUses.map((use) => use.problem),
|
|
205
207
|
];
|
|
206
|
-
const rejecting = [...problems, ...testProblems, ...duplicates.map((item) => item.problem)].filter((item) => REJECTS_FILE[this.options.framework].has(item.code) &&
|
|
208
|
+
const rejecting = [...problems, ...testProblems, ...duplicates.map((item) => item.problem)].filter((item) => REJECTS_FILE[this.options.framework].has(item.code) &&
|
|
209
|
+
item.code !== 'undefined-tag' &&
|
|
210
|
+
item.afterLoad !== true &&
|
|
211
|
+
// A warning of a code that rejects files, as for Jest's .each without a body, leaves the file loading.
|
|
212
|
+
(item.level ?? LEVELS[item.code]) === 'error');
|
|
207
213
|
// Each of these problems points at the code that causes it. Vitest meets the tags in the order they are listed.
|
|
208
214
|
const firstTag = undefinedUses.find((use) => use.isSure)?.problem;
|
|
209
215
|
const lines = [...rejecting, ...(firstTag ? [firstTag] : [])].map((item) => this.parsed.source.line(item.node.start));
|
|
210
216
|
if (lines.length > 0)
|
|
211
217
|
this.rejectedAt = Math.min(...lines);
|
|
218
|
+
this.focused = new Set(extraction.tests.filter((test) => test.declaration.isOnly).map((test) => test.scope));
|
|
212
219
|
const suites = extraction.suites.map((suite) => this.suite(suite));
|
|
213
220
|
const tests = extraction.tests.map((test) => this.test(test, suites));
|
|
214
221
|
for (const item of problems) {
|
|
@@ -336,6 +343,29 @@ class RecordBuilder {
|
|
|
336
343
|
merged[option] = undefined;
|
|
337
344
|
return merged;
|
|
338
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* A Jest test's state, as jest-circus decides it: a describe takes its own `.only` or `.skip`, else the mode around
|
|
348
|
+
* it; a test without a mode of its own takes its describe's, unless that describe is `.only` and has a test of its
|
|
349
|
+
* own that uses `.only`. A skipped describe skips every test in it, a todo or a `.only` too, and a `.only` there
|
|
350
|
+
* focuses nothing.
|
|
351
|
+
*/
|
|
352
|
+
jestState(pending, scopes) {
|
|
353
|
+
const { declaration } = pending;
|
|
354
|
+
const marked = scopes.find((scope) => scope.isOnly || scope.states.some((setting) => setting.state === 'skip'));
|
|
355
|
+
const skippedBy = marked && !marked.isOnly ? marked.states.find((setting) => setting.state === 'skip') : undefined;
|
|
356
|
+
const own = (state) => declaration.states.find((setting) => setting.state === state);
|
|
357
|
+
const passesOnly = marked?.isOnly === true && !this.focused.has(pending.scope);
|
|
358
|
+
if (skippedBy) {
|
|
359
|
+
return {
|
|
360
|
+
chosen: own('skip')
|
|
361
|
+
? { setting: own('skip'), source: 'test' }
|
|
362
|
+
: { setting: skippedBy, source: 'suite' },
|
|
363
|
+
isOnly: false,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const setting = own('skip') ?? own('todo') ?? own('expectedToFail');
|
|
367
|
+
return { chosen: setting ? { setting, source: 'test' } : null, isOnly: declaration.isOnly || passesOnly };
|
|
368
|
+
}
|
|
339
369
|
test(pending, suites) {
|
|
340
370
|
const { declaration } = pending;
|
|
341
371
|
const dependencies = this.options.dependencies(pending);
|
|
@@ -356,14 +386,17 @@ class RecordBuilder {
|
|
|
356
386
|
// In Vitest 5 the options of a test's tags win over those the describes pass down; before, the describes' win.
|
|
357
387
|
const tagsFirst = this.options.config?.tags?.beatsDescribes ?? true;
|
|
358
388
|
const tagged = (own, tag, around) => tagsFirst ? [...own, ...tag, ...around] : [...own, ...around, ...tag];
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
389
|
+
const jest = this.options.framework === 'jest' ? this.jestState(pending, scopes) : null;
|
|
390
|
+
const chosen = jest
|
|
391
|
+
? jest.chosen
|
|
392
|
+
: chooseState([
|
|
393
|
+
...declaredStates(pending.states.slice(0, declaration.states.length), fromTags, declaration),
|
|
394
|
+
...pending.states.slice(declaration.states.length).map((setting) => ({ setting, source: 'test' })),
|
|
395
|
+
...scopes.flatMap((scope) => scope.states.map((setting) => ({
|
|
396
|
+
setting,
|
|
397
|
+
source: scope.kind === 'file' ? 'file' : 'suite',
|
|
398
|
+
}))),
|
|
399
|
+
]);
|
|
367
400
|
// When the runner refuses the file, no test in it runs, whatever else is written.
|
|
368
401
|
const rejected = this.rejectedAt !== null;
|
|
369
402
|
const state = rejected ? 'notLoaded' : (chosen?.setting.state ?? 'active');
|
|
@@ -376,7 +409,9 @@ class RecordBuilder {
|
|
|
376
409
|
for (const values of [fromTags?.meta, ...describes.map((scope) => scope.details.meta), declaration.details.meta]) {
|
|
377
410
|
Object.assign(meta, values);
|
|
378
411
|
}
|
|
379
|
-
const isOnly =
|
|
412
|
+
const isOnly = jest
|
|
413
|
+
? jest.isOnly
|
|
414
|
+
: declaration.isOnly || scopes.some((scope) => scope.isOnly) || fromTags?.only === true;
|
|
380
415
|
const comments = this.options.comments === 'all' || (this.options.comments === 'non-active' && state !== 'active')
|
|
381
416
|
? pending.comments
|
|
382
417
|
: [];
|
package/dist/resolve.js
CHANGED
|
@@ -272,39 +272,35 @@ function pnpmWorkspacePatterns(text) {
|
|
|
272
272
|
}
|
|
273
273
|
return patterns;
|
|
274
274
|
}
|
|
275
|
-
/** The folder of each package
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
return packages;
|
|
304
|
-
}
|
|
305
|
-
if (path.dirname(folder) === folder)
|
|
306
|
-
return new Map();
|
|
275
|
+
/** The folder of each package of the workspace a folder is the root of, by name; null when it isn't one. */
|
|
276
|
+
function workspaceAt(folder) {
|
|
277
|
+
const workspaces = readJson(path.join(folder, 'package.json'))?.workspaces;
|
|
278
|
+
const fromManifest = Array.isArray(workspaces) ? workspaces : record(workspaces)?.packages;
|
|
279
|
+
const pnpm = readText(path.join(folder, 'pnpm-workspace.yaml'));
|
|
280
|
+
// pnpm reads only pnpm-workspace.yaml, and npm, Yarn and Bun only package.json, so a repository that moved to
|
|
281
|
+
// pnpm may still have an old list there. Packages from both are found.
|
|
282
|
+
const lists = [Array.isArray(fromManifest) ? fromManifest : null, pnpm ? pnpmWorkspacePatterns(pnpm) : null];
|
|
283
|
+
const patterns = lists.some((list) => list !== null)
|
|
284
|
+
? lists.flat().filter((item) => typeof item === 'string')
|
|
285
|
+
: null;
|
|
286
|
+
if (!patterns)
|
|
287
|
+
return null;
|
|
288
|
+
// Packages are inside the workspace. A pattern or a link that leads out of its folder could send the search through
|
|
289
|
+
// the whole disk, so neither is followed.
|
|
290
|
+
const inside = patterns.filter((item) => !path.isAbsolute(item.replace(/^!/, '')) && !/(^|\/)\.\.(\/|$)/.test(item));
|
|
291
|
+
const manifests = globSync(inside.filter((item) => !item.startsWith('!')).map((item) => `${item.replace(/\/+$/, '')}/package.json`), {
|
|
292
|
+
cwd: folder,
|
|
293
|
+
absolute: true,
|
|
294
|
+
followSymbolicLinks: false,
|
|
295
|
+
ignore: ['**/node_modules/**', ...inside.filter((item) => item.startsWith('!')).map((item) => item.slice(1))],
|
|
296
|
+
});
|
|
297
|
+
const packages = new Map();
|
|
298
|
+
for (const manifest of manifests) {
|
|
299
|
+
const name = readJson(manifest)?.name;
|
|
300
|
+
if (typeof name === 'string')
|
|
301
|
+
packages.set(name, path.dirname(manifest));
|
|
307
302
|
}
|
|
303
|
+
return packages;
|
|
308
304
|
}
|
|
309
305
|
/**
|
|
310
306
|
* A workspace package followed to its TypeScript source: through its link in node_modules, or, before an install, from
|
|
@@ -351,12 +347,19 @@ export function moduleResolver(root) {
|
|
|
351
347
|
}
|
|
352
348
|
return options;
|
|
353
349
|
};
|
|
350
|
+
// The packages of the workspace a folder belongs to, empty outside one; each folder on the way up is looked at once.
|
|
354
351
|
const packagesOf = (directory) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
workspaces.
|
|
352
|
+
const passed = [];
|
|
353
|
+
let packages;
|
|
354
|
+
for (let folder = directory; !packages; folder = path.dirname(folder)) {
|
|
355
|
+
packages = workspaces.get(folder);
|
|
356
|
+
if (packages)
|
|
357
|
+
break;
|
|
358
|
+
passed.push(folder);
|
|
359
|
+
packages = workspaceAt(folder) ?? (path.dirname(folder) === folder ? new Map() : null);
|
|
359
360
|
}
|
|
361
|
+
for (const folder of passed)
|
|
362
|
+
workspaces.set(folder, packages);
|
|
360
363
|
return packages;
|
|
361
364
|
};
|
|
362
365
|
const reader = (fromFile) => (specifier) => {
|
package/dist/scan-file.js
CHANGED
|
@@ -6,19 +6,23 @@ import { ExportedValues } from "./imported-values.js";
|
|
|
6
6
|
import { atLeast, problem } from "./frameworks/common.js";
|
|
7
7
|
import { PlaywrightAdapter } from "./frameworks/playwright.js";
|
|
8
8
|
import { VitestAdapter } from "./frameworks/vitest.js";
|
|
9
|
+
import { JestAdapter } from "./frameworks/jest.js";
|
|
9
10
|
import { parseFile } from "./parse.js";
|
|
10
11
|
import { buildRecords, fileRecord } from "./records.js";
|
|
11
12
|
import { snippet } from "./source.js";
|
|
12
13
|
import { Values } from "./values.js";
|
|
13
14
|
const TYPE_TEST = /\.(test|spec)-d\.[cm]?[jt]sx?$/;
|
|
14
15
|
const BENCHMARK = /\.(bench|benchmark)\.[cm]?[jt]sx?$/;
|
|
16
|
+
const ADAPTERS = {
|
|
17
|
+
playwright: (parsed, file, readModule) => new PlaywrightAdapter(parsed.program, file, readModule),
|
|
18
|
+
vitest: (parsed, file, readModule) => new VitestAdapter(parsed, file, readModule),
|
|
19
|
+
jest: (parsed, file, readModule) => new JestAdapter(parsed, file, readModule),
|
|
20
|
+
};
|
|
15
21
|
/** A file read for the scan: the scanned file, or a module a helper function is imported from. */
|
|
16
22
|
function frame(parsed, relativeFilePath, settings, readModule, exports) {
|
|
17
23
|
const values = new Values(exports.reader(readModule));
|
|
18
24
|
const file = { source: parsed.source, values, version: settings.version };
|
|
19
|
-
const adapter = settings.framework
|
|
20
|
-
? new PlaywrightAdapter(parsed.program, file, readModule)
|
|
21
|
-
: new VitestAdapter(parsed, file, readModule);
|
|
25
|
+
const adapter = ADAPTERS[settings.framework](parsed, file, readModule);
|
|
22
26
|
return { parsed, adapter, values: file.values, readModule, relativeFilePath };
|
|
23
27
|
}
|
|
24
28
|
/** The records of a file the scan read no tests from. */
|
package/dist/scan.js
CHANGED
|
@@ -4,19 +4,19 @@ import path from 'node:path';
|
|
|
4
4
|
import { glob } from 'tinyglobby';
|
|
5
5
|
import { ConfigDependencies } from "./config-dependencies.js";
|
|
6
6
|
import { ConfigRuns, fileConfig } from "./config-runs.js";
|
|
7
|
-
import { Configs } from "./config.js";
|
|
7
|
+
import { Configs, settingAt } from "./config.js";
|
|
8
8
|
import { DependencyGraph } from "./dependency-hash.js";
|
|
9
9
|
import { ExportedValues } from "./imported-values.js";
|
|
10
10
|
import { fileDiagnostic } from "./diagnostics.js";
|
|
11
11
|
import { findFiles, normalizePattern, readFailure } from "./files.js";
|
|
12
|
-
import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
|
|
12
|
+
import { detectFramework, detectFrameworkVersion, FRAMEWORK_NAMES, OLDEST } from "./framework-version.js";
|
|
13
13
|
import { createInventory } from "./inventory.js";
|
|
14
14
|
import { checkComments, checkFlag, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
|
|
15
15
|
import { moduleResolver } from "./resolve.js";
|
|
16
16
|
import { noRecords, scanFile } from "./scan-file.js";
|
|
17
17
|
import { globalsCheck } from "./vitest-config.js";
|
|
18
18
|
function versionDiagnostics(framework, version, source, range) {
|
|
19
|
-
const name = framework
|
|
19
|
+
const name = FRAMEWORK_NAMES[framework];
|
|
20
20
|
if (version === null) {
|
|
21
21
|
return [
|
|
22
22
|
fileDiagnostic('unknown-framework-version', source, `The ${name} version could not be read from the project, so the rules of the newest supported version apply.`),
|
|
@@ -37,6 +37,17 @@ function versionDiagnostics(framework, version, source, range) {
|
|
|
37
37
|
fileDiagnostic('approximate-framework-version', source, `Only the range "${range}" was found for ${name}, so the rules of the newest ${major}.x release apply.`),
|
|
38
38
|
];
|
|
39
39
|
}
|
|
40
|
+
/** Why Jest gives a file that uses its globals none: a project that runs it sets `injectGlobals` to false. */
|
|
41
|
+
function jestGlobals(placement) {
|
|
42
|
+
const off = placement.projects?.find(({ settings }) => {
|
|
43
|
+
const setting = settingAt(settings, ['injectGlobals']);
|
|
44
|
+
return setting?.kind === 'value' && setting.value === false;
|
|
45
|
+
});
|
|
46
|
+
if (!off)
|
|
47
|
+
return null;
|
|
48
|
+
const config = off.config.record.relativeFilePath;
|
|
49
|
+
return `The file uses describe, it or test without importing them, but ${config} sets injectGlobals to false, so Jest doesn't define them; import them from '@jest/globals'.`;
|
|
50
|
+
}
|
|
40
51
|
/** For a pattern that names a folder, which matches no file, a pattern that matches the test files in it. */
|
|
41
52
|
function folderHint(root, patterns, framework) {
|
|
42
53
|
const folder = patterns.find((pattern) => {
|
|
@@ -100,7 +111,7 @@ function noFiles(root, patterns, framework, placements) {
|
|
|
100
111
|
if ([...placements.values()].some((placement) => placement.config)) {
|
|
101
112
|
return 'No test files were found from the configs in the root.';
|
|
102
113
|
}
|
|
103
|
-
const name = framework
|
|
114
|
+
const name = FRAMEWORK_NAMES[framework];
|
|
104
115
|
return `No patterns were given, and no ${name} config is in the root or a folder below it, so there are no test files to find.`;
|
|
105
116
|
}
|
|
106
117
|
/**
|
|
@@ -112,7 +123,6 @@ function noFiles(root, patterns, framework, placements) {
|
|
|
112
123
|
export async function scan(options) {
|
|
113
124
|
const checked = checkObject(options, 'options');
|
|
114
125
|
const patterns = checked.patterns === undefined ? null : checkStrings(checked.patterns, 'options.patterns', true);
|
|
115
|
-
const framework = checkFramework(checked.framework, 'options.framework');
|
|
116
126
|
const ignore = checkStrings(checked.ignore, 'options.ignore', false);
|
|
117
127
|
const comments = checkComments(checked.comments, 'options.comments');
|
|
118
128
|
const expandCases = checkFlag(checked.expandCases, 'options.expandCases');
|
|
@@ -123,6 +133,9 @@ export async function scan(options) {
|
|
|
123
133
|
const root = path.resolve(checked.root ?? process.cwd());
|
|
124
134
|
if (!existsSync(root) || !statSync(root).isDirectory())
|
|
125
135
|
throw invalid('options.root', `a directory, and ${root} is not one`);
|
|
136
|
+
const framework = checked.framework === undefined
|
|
137
|
+
? detectFramework(root, ignore)
|
|
138
|
+
: checkFramework(checked.framework, 'options.framework');
|
|
126
139
|
// Without patterns, the configs choose among all JavaScript and TypeScript files.
|
|
127
140
|
const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
|
|
128
141
|
const scanned = found.files.map((file) => file.relativeFilePath);
|
|
@@ -216,7 +229,9 @@ export async function scan(options) {
|
|
|
216
229
|
}
|
|
217
230
|
results.push({ ...result, file: { ...result.file, config } });
|
|
218
231
|
}
|
|
219
|
-
const missingGlobals =
|
|
232
|
+
const missingGlobals = framework === 'jest'
|
|
233
|
+
? (file) => jestGlobals(placements.get(file))
|
|
234
|
+
: globalsCheck(root, configs, (file) => placements.get(file));
|
|
220
235
|
let globals = null;
|
|
221
236
|
for (const { absolutePath, relativeFilePath } of globalsFiles) {
|
|
222
237
|
const message = missingGlobals(absolutePath);
|
package/dist/types.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ type Position = number;
|
|
|
11
11
|
/** @pattern ^[0-9a-f]{16}$ */
|
|
12
12
|
type Hash = string;
|
|
13
13
|
/** @inline */
|
|
14
|
-
export type Framework = 'playwright' | 'vitest';
|
|
14
|
+
export type Framework = 'playwright' | 'vitest' | 'jest';
|
|
15
15
|
/**
|
|
16
16
|
* The state the runner applies to a test, after describes and file-level calls are taken into account. `notLoaded`:
|
|
17
17
|
* the runner refuses to load the file, so none of its tests run.
|
|
@@ -38,7 +38,7 @@ export type SettingSource = 'test' | 'suite' | 'file' | 'config';
|
|
|
38
38
|
* @inline
|
|
39
39
|
*/
|
|
40
40
|
export type DiagnosticLevel = 'error' | 'warning';
|
|
41
|
-
export type DiagnosticCode = 'unresolved-test-import' | 'parse-error' | 'read-error' | 'framework-mismatch' | 'unresolved-tag' | 'unresolved-annotation' | 'unresolved-cases' | 'cases-not-expanded' | 'test-in-loop' | 'dynamic-title' | 'duplicate-title' | 'duplicate-id' | 'internal-error' | 'invalid-chain' | 'skip-without-body' | 'test-without-body' | 'describe-not-called' | 'async-describe' | 'nested-test' | 'tag-without-at' | 'removed-api' | 'only-in-skipped-describe' | 'config-may-change-state' | 'undefined-tag' | 'symlink-ignored' | 'invalid-import' | 'globals-not-enabled' | 'unknown-framework-version' | 'unsupported-framework-version' | 'local-test-object' | 'state-in-helper' | 'unresolved-option' | 'dynamic-reason' | 'no-files-matched' | 'unsupported-file' | 'approximate-framework-version' | 'test-in-function' | 'empty-title' | 'bench-outside-benchmark-file' | 'config-value-unknown' | 'file-not-run' | 'forbidden-only' | 'dependency-not-followed';
|
|
41
|
+
export type DiagnosticCode = 'unresolved-test-import' | 'parse-error' | 'read-error' | 'framework-mismatch' | 'unresolved-tag' | 'unresolved-annotation' | 'unresolved-cases' | 'cases-not-expanded' | 'test-in-loop' | 'dynamic-title' | 'duplicate-title' | 'duplicate-id' | 'internal-error' | 'invalid-chain' | 'skip-without-body' | 'test-without-body' | 'describe-not-called' | 'async-describe' | 'describe-throws' | 'nested-test' | 'tag-without-at' | 'removed-api' | 'only-in-skipped-describe' | 'config-may-change-state' | 'undefined-tag' | 'symlink-ignored' | 'invalid-import' | 'globals-not-enabled' | 'unknown-framework-version' | 'unsupported-framework-version' | 'local-test-object' | 'state-in-helper' | 'unresolved-option' | 'dynamic-reason' | 'no-files-matched' | 'unsupported-file' | 'approximate-framework-version' | 'test-in-function' | 'empty-title' | 'bench-outside-benchmark-file' | 'config-value-unknown' | 'file-not-run' | 'forbidden-only' | 'dependency-not-followed';
|
|
42
42
|
export type MetaValue = string | number | boolean | null | MetaValue[] | {
|
|
43
43
|
[key: string]: MetaValue;
|
|
44
44
|
};
|
|
@@ -242,7 +242,10 @@ export interface SuiteRecord {
|
|
|
242
242
|
/** Source text of the `test.slow()` condition; null when unconditional or not slow. */
|
|
243
243
|
slowCondition: string | null;
|
|
244
244
|
retries: Count | null;
|
|
245
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of
|
|
247
|
+
* `jest.retryTimes()` (30.0); null otherwise.
|
|
248
|
+
*/
|
|
246
249
|
retryDelay: Count | null;
|
|
247
250
|
/** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
|
|
248
251
|
retryCondition: string | null;
|
|
@@ -339,7 +342,10 @@ export interface TestRecord {
|
|
|
339
342
|
slowCondition: string | null;
|
|
340
343
|
/** How many times the runner retries the test when it fails; null when the file sets none. */
|
|
341
344
|
retries: Count | null;
|
|
342
|
-
/**
|
|
345
|
+
/**
|
|
346
|
+
* Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of
|
|
347
|
+
* `jest.retryTimes()` (30.0); null otherwise.
|
|
348
|
+
*/
|
|
343
349
|
retryDelay: Count | null;
|
|
344
350
|
/** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
|
|
345
351
|
retryCondition: string | null;
|
package/dist/values.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export declare class Values {
|
|
|
60
60
|
init: Expression;
|
|
61
61
|
scopes: readonly Node[];
|
|
62
62
|
} | null;
|
|
63
|
+
/** The name a module exports that a name imported from it stands for, `default` for a default import; null otherwise. */
|
|
64
|
+
importedName(name: string, scopes: readonly Node[]): string | null;
|
|
63
65
|
/** The module a name is imported from where it is used; null when it isn't an import. */
|
|
64
66
|
importSource(name: string, scopes: readonly Node[]): string | null;
|
|
65
67
|
/**
|
|
@@ -81,6 +83,14 @@ export declare class Values {
|
|
|
81
83
|
/** `data.name` where `data` is `import * as data`: what the module exports as `name`; null for any other member. */
|
|
82
84
|
private namespaceMember;
|
|
83
85
|
private imported;
|
|
86
|
+
/**
|
|
87
|
+
* How many items a list has when not all its values are known, as `[NaN, Infinity]`: a list written out, its spreads
|
|
88
|
+
* included, held in a constant the module doesn't change, or the keys, values or entries of an object written out;
|
|
89
|
+
* null otherwise. `seen` stops at a constant defined through itself.
|
|
90
|
+
*/
|
|
91
|
+
lengthOf(node: Expression, scopes: readonly Node[], seen?: Set<string>): number | null;
|
|
92
|
+
/** How many keys an object written out has, held in a constant the module doesn't change; null otherwise. */
|
|
93
|
+
private keyCount;
|
|
84
94
|
/** What a name refers to where it is used; `scopes` lists the scope nodes around the use, outermost first. */
|
|
85
95
|
resolveName(name: string, scopes: readonly Node[]): Resolved;
|
|
86
96
|
/** A parameter of a function read at one of its calls: the argument given there. */
|
package/dist/values.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { childNodes, isFunctionNode, propertyName, unwrap } from "./ast.js";
|
|
1
|
+
import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "./ast.js";
|
|
2
2
|
const UNRESOLVED = { ok: false, importedFrom: null };
|
|
3
3
|
function isPrimitive(value) {
|
|
4
4
|
return typeof value !== 'object' || value === null;
|
|
@@ -314,6 +314,11 @@ export class Values {
|
|
|
314
314
|
const binding = this.lookup(name, scopes)?.binding;
|
|
315
315
|
return binding?.kind === 'const' ? { init: binding.init, scopes: binding.scopes } : null;
|
|
316
316
|
}
|
|
317
|
+
/** The name a module exports that a name imported from it stands for, `default` for a default import; null otherwise. */
|
|
318
|
+
importedName(name, scopes) {
|
|
319
|
+
const binding = this.lookup(name, scopes)?.binding;
|
|
320
|
+
return binding?.kind === 'import' ? binding.imported : null;
|
|
321
|
+
}
|
|
317
322
|
/** The module a name is imported from where it is used; null when it isn't an import. */
|
|
318
323
|
importSource(name, scopes) {
|
|
319
324
|
const binding = this.lookup(name, scopes)?.binding;
|
|
@@ -421,6 +426,61 @@ export class Values {
|
|
|
421
426
|
const resolved = name !== null && this.imports ? this.imports(source, name) : UNRESOLVED;
|
|
422
427
|
return resolved.ok ? resolved : { ok: false, importedFrom: source };
|
|
423
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* How many items a list has when not all its values are known, as `[NaN, Infinity]`: a list written out, its spreads
|
|
431
|
+
* included, held in a constant the module doesn't change, or the keys, values or entries of an object written out;
|
|
432
|
+
* null otherwise. `seen` stops at a constant defined through itself.
|
|
433
|
+
*/
|
|
434
|
+
lengthOf(node, scopes, seen = new Set()) {
|
|
435
|
+
const resolved = this.resolve(node, scopes);
|
|
436
|
+
if (resolved.ok)
|
|
437
|
+
return Array.isArray(resolved.value) ? resolved.value.length : null;
|
|
438
|
+
const expression = unwrap(node);
|
|
439
|
+
if (expression.type === 'Identifier') {
|
|
440
|
+
const binding = this.lookup(expression.name, scopes)?.binding;
|
|
441
|
+
if (binding?.kind !== 'const' || seen.has(expression.name))
|
|
442
|
+
return null;
|
|
443
|
+
if (this.changedAtLoad(binding.scopes[0]).has(expression.name))
|
|
444
|
+
return null;
|
|
445
|
+
return this.lengthOf(binding.init, binding.scopes, new Set([...seen, expression.name]));
|
|
446
|
+
}
|
|
447
|
+
if (expression.type === 'CallExpression' && isObjectListing(expression)) {
|
|
448
|
+
const target = expression.arguments[0];
|
|
449
|
+
return this.keyCount(target, scopes, seen);
|
|
450
|
+
}
|
|
451
|
+
if (expression.type !== 'ArrayExpression')
|
|
452
|
+
return null;
|
|
453
|
+
let length = 0;
|
|
454
|
+
for (const element of expression.elements) {
|
|
455
|
+
const inner = element?.type === 'SpreadElement' ? this.lengthOf(element.argument, scopes, seen) : 1;
|
|
456
|
+
if (inner === null)
|
|
457
|
+
return null;
|
|
458
|
+
length += inner;
|
|
459
|
+
}
|
|
460
|
+
return length;
|
|
461
|
+
}
|
|
462
|
+
/** How many keys an object written out has, held in a constant the module doesn't change; null otherwise. */
|
|
463
|
+
keyCount(node, scopes, seen) {
|
|
464
|
+
const expression = unwrap(node);
|
|
465
|
+
if (expression.type === 'Identifier') {
|
|
466
|
+
const binding = this.lookup(expression.name, scopes)?.binding;
|
|
467
|
+
if (binding?.kind !== 'const' || seen.has(expression.name))
|
|
468
|
+
return null;
|
|
469
|
+
if (this.changedAtLoad(binding.scopes[0]).has(expression.name))
|
|
470
|
+
return null;
|
|
471
|
+
return this.keyCount(binding.init, binding.scopes, new Set([...seen, expression.name]));
|
|
472
|
+
}
|
|
473
|
+
if (expression.type !== 'ObjectExpression')
|
|
474
|
+
return null;
|
|
475
|
+
const keys = new Set();
|
|
476
|
+
for (const property of expression.properties) {
|
|
477
|
+
const key = property.type === 'Property' ? propertyKey(property) : null;
|
|
478
|
+
if (key === null)
|
|
479
|
+
return null;
|
|
480
|
+
keys.add(key);
|
|
481
|
+
}
|
|
482
|
+
return keys.size;
|
|
483
|
+
}
|
|
424
484
|
/** What a name refers to where it is used; `scopes` lists the scope nodes around the use, outermost first. */
|
|
425
485
|
resolveName(name, scopes) {
|
|
426
486
|
const found = this.lookup(name, scopes);
|
|
@@ -716,7 +776,33 @@ function collectBindings(scope, scopes) {
|
|
|
716
776
|
if (statement.declare)
|
|
717
777
|
break;
|
|
718
778
|
for (const declarator of statement.declarations) {
|
|
719
|
-
|
|
779
|
+
const required = declarator.init ? localRequire(declarator.init) : null;
|
|
780
|
+
if (required !== null && declarator.id.type === 'Identifier') {
|
|
781
|
+
// `const symbols = require('./symbols')` stands for what the module exports, as a default import does.
|
|
782
|
+
bindings.set(declarator.id.name, {
|
|
783
|
+
kind: 'import',
|
|
784
|
+
source: required,
|
|
785
|
+
imported: 'default',
|
|
786
|
+
declaration: declarator,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
else if (required !== null && declarator.id.type === 'ObjectPattern') {
|
|
790
|
+
for (const property of declarator.id.properties) {
|
|
791
|
+
const key = property.type === 'Property' ? propertyKey(property) : null;
|
|
792
|
+
if (key !== null && property.type === 'Property' && property.value.type === 'Identifier') {
|
|
793
|
+
bindings.set(property.value.name, {
|
|
794
|
+
kind: 'import',
|
|
795
|
+
source: required,
|
|
796
|
+
imported: key,
|
|
797
|
+
declaration: declarator,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
other(bindingNames(property.type === 'Property' ? property.value : property), declarator);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else if (statement.kind === 'const' && declarator.id.type === 'Identifier' && declarator.init) {
|
|
720
806
|
bindings.set(declarator.id.name, { kind: 'const', init: declarator.init, scopes, declaration: declarator });
|
|
721
807
|
}
|
|
722
808
|
else {
|
|
@@ -783,3 +869,22 @@ function collectBindings(scope, scopes) {
|
|
|
783
869
|
}
|
|
784
870
|
return bindings;
|
|
785
871
|
}
|
|
872
|
+
/** `Object.keys(x)`, `Object.values(x)` or `Object.entries(x)`, which list as many items as `x` has keys. */
|
|
873
|
+
function isObjectListing(call) {
|
|
874
|
+
const callee = unwrap(call.callee);
|
|
875
|
+
return (callee.type === 'MemberExpression' &&
|
|
876
|
+
callee.object.type === 'Identifier' &&
|
|
877
|
+
callee.object.name === 'Object' &&
|
|
878
|
+
['keys', 'values', 'entries'].includes(propertyName(callee)) &&
|
|
879
|
+
call.arguments.length === 1 &&
|
|
880
|
+
call.arguments[0].type !== 'SpreadElement');
|
|
881
|
+
}
|
|
882
|
+
/** The local file `require('./symbols')` loads, as an import of it; null for a package or any other expression. */
|
|
883
|
+
function localRequire(expression) {
|
|
884
|
+
const node = unwrap(expression);
|
|
885
|
+
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier' || node.callee.name !== 'require')
|
|
886
|
+
return null;
|
|
887
|
+
const [argument] = node.arguments;
|
|
888
|
+
const isLocal = argument.type === 'Literal' && typeof argument.value === 'string' && /^\.\.?\//.test(argument.value);
|
|
889
|
+
return isLocal ? argument.value : null;
|
|
890
|
+
}
|
package/package.json
CHANGED
package/schema/v1.json
CHANGED
|
@@ -98,7 +98,8 @@
|
|
|
98
98
|
"type": "string",
|
|
99
99
|
"enum": [
|
|
100
100
|
"playwright",
|
|
101
|
-
"vitest"
|
|
101
|
+
"vitest",
|
|
102
|
+
"jest"
|
|
102
103
|
]
|
|
103
104
|
},
|
|
104
105
|
{
|
|
@@ -607,7 +608,7 @@
|
|
|
607
608
|
"type": "null"
|
|
608
609
|
}
|
|
609
610
|
],
|
|
610
|
-
"description": "
|
|
611
|
+
"description": "Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of `jest.retryTimes()` (30.0); null otherwise."
|
|
611
612
|
},
|
|
612
613
|
"retryCondition": {
|
|
613
614
|
"type": [
|
|
@@ -1025,7 +1026,7 @@
|
|
|
1025
1026
|
"type": "null"
|
|
1026
1027
|
}
|
|
1027
1028
|
],
|
|
1028
|
-
"description": "
|
|
1029
|
+
"description": "Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of `jest.retryTimes()` (30.0); null otherwise."
|
|
1029
1030
|
},
|
|
1030
1031
|
"retryCondition": {
|
|
1031
1032
|
"type": [
|
|
@@ -1449,6 +1450,7 @@
|
|
|
1449
1450
|
"test-without-body",
|
|
1450
1451
|
"describe-not-called",
|
|
1451
1452
|
"async-describe",
|
|
1453
|
+
"describe-throws",
|
|
1452
1454
|
"nested-test",
|
|
1453
1455
|
"tag-without-at",
|
|
1454
1456
|
"removed-api",
|