test-inventory 0.6.1 → 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 +100 -0
- package/README.md +4 -3
- package/dist/case-titles.d.ts +27 -0
- package/dist/case-titles.js +329 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -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 +3 -0
- package/dist/extract.d.ts +19 -1
- package/dist/extract.js +391 -41
- package/dist/files.js +3 -2
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +13 -2
- package/dist/framework-version.js +64 -5
- package/dist/frameworks/bindings.d.ts +14 -0
- package/dist/frameworks/bindings.js +99 -6
- package/dist/frameworks/common.d.ts +6 -0
- package/dist/frameworks/common.js +27 -0
- package/dist/frameworks/jest.d.ts +46 -0
- package/dist/frameworks/jest.js +442 -0
- package/dist/frameworks/playwright.d.ts +3 -1
- package/dist/frameworks/playwright.js +14 -5
- package/dist/frameworks/test-functions.d.ts +169 -0
- package/dist/frameworks/test-functions.js +492 -0
- package/dist/frameworks/vitest.d.ts +13 -58
- package/dist/frameworks/vitest.js +84 -378
- package/dist/imported-values.d.ts +22 -0
- package/dist/imported-values.js +149 -0
- package/dist/model.d.ts +11 -0
- package/dist/options.d.ts +16 -1
- package/dist/options.js +9 -2
- package/dist/parse.js +17 -2
- package/dist/records.js +57 -15
- package/dist/resolve.js +39 -36
- package/dist/scan-file.d.ts +5 -0
- package/dist/scan-file.js +15 -8
- package/dist/scan-source.js +3 -1
- package/dist/scan.js +31 -10
- package/dist/types.d.ts +24 -6
- package/dist/values.d.ts +49 -1
- package/dist/values.js +531 -19
- package/package.json +1 -1
- package/schema/v1.json +33 -4
package/dist/scan.js
CHANGED
|
@@ -4,18 +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
|
+
import { ExportedValues } from "./imported-values.js";
|
|
9
10
|
import { fileDiagnostic } from "./diagnostics.js";
|
|
10
11
|
import { findFiles, normalizePattern, readFailure } from "./files.js";
|
|
11
|
-
import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
|
|
12
|
+
import { detectFramework, detectFrameworkVersion, FRAMEWORK_NAMES, OLDEST } from "./framework-version.js";
|
|
12
13
|
import { createInventory } from "./inventory.js";
|
|
13
|
-
import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
|
|
14
|
+
import { checkComments, checkFlag, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
|
|
14
15
|
import { moduleResolver } from "./resolve.js";
|
|
15
16
|
import { noRecords, scanFile } from "./scan-file.js";
|
|
16
17
|
import { globalsCheck } from "./vitest-config.js";
|
|
17
18
|
function versionDiagnostics(framework, version, source, range) {
|
|
18
|
-
const name = framework
|
|
19
|
+
const name = FRAMEWORK_NAMES[framework];
|
|
19
20
|
if (version === null) {
|
|
20
21
|
return [
|
|
21
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.`),
|
|
@@ -36,6 +37,17 @@ function versionDiagnostics(framework, version, source, range) {
|
|
|
36
37
|
fileDiagnostic('approximate-framework-version', source, `Only the range "${range}" was found for ${name}, so the rules of the newest ${major}.x release apply.`),
|
|
37
38
|
];
|
|
38
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
|
+
}
|
|
39
51
|
/** For a pattern that names a folder, which matches no file, a pattern that matches the test files in it. */
|
|
40
52
|
function folderHint(root, patterns, framework) {
|
|
41
53
|
const folder = patterns.find((pattern) => {
|
|
@@ -99,7 +111,7 @@ function noFiles(root, patterns, framework, placements) {
|
|
|
99
111
|
if ([...placements.values()].some((placement) => placement.config)) {
|
|
100
112
|
return 'No test files were found from the configs in the root.';
|
|
101
113
|
}
|
|
102
|
-
const name = framework
|
|
114
|
+
const name = FRAMEWORK_NAMES[framework];
|
|
103
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.`;
|
|
104
116
|
}
|
|
105
117
|
/**
|
|
@@ -111,9 +123,9 @@ function noFiles(root, patterns, framework, placements) {
|
|
|
111
123
|
export async function scan(options) {
|
|
112
124
|
const checked = checkObject(options, 'options');
|
|
113
125
|
const patterns = checked.patterns === undefined ? null : checkStrings(checked.patterns, 'options.patterns', true);
|
|
114
|
-
const framework = checkFramework(checked.framework, 'options.framework');
|
|
115
126
|
const ignore = checkStrings(checked.ignore, 'options.ignore', false);
|
|
116
127
|
const comments = checkComments(checked.comments, 'options.comments');
|
|
128
|
+
const expandCases = checkFlag(checked.expandCases, 'options.expandCases');
|
|
117
129
|
const override = checkVersion(checked.frameworkVersion, 'options.frameworkVersion');
|
|
118
130
|
if (checked.root !== undefined && (typeof checked.root !== 'string' || checked.root === '')) {
|
|
119
131
|
throw invalid('options.root', 'a non-empty string');
|
|
@@ -121,7 +133,13 @@ export async function scan(options) {
|
|
|
121
133
|
const root = path.resolve(checked.root ?? process.cwd());
|
|
122
134
|
if (!existsSync(root) || !statSync(root).isDirectory())
|
|
123
135
|
throw invalid('options.root', `a directory, and ${root} is not one`);
|
|
124
|
-
const
|
|
136
|
+
const framework = checked.framework === undefined
|
|
137
|
+
? detectFramework(root, ignore)
|
|
138
|
+
: checkFramework(checked.framework, 'options.framework');
|
|
139
|
+
// Without patterns, the configs choose among all JavaScript and TypeScript files.
|
|
140
|
+
const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
|
|
141
|
+
const scanned = found.files.map((file) => file.relativeFilePath);
|
|
142
|
+
const detected = override === null ? detectFrameworkVersion(root, framework, scanned) : null;
|
|
125
143
|
const frameworkVersion = override ?? detected?.version ?? null;
|
|
126
144
|
const range = detected?.range ?? null;
|
|
127
145
|
// With only a range, the newest rules of its major apply, as the newest release in it is the likeliest installed.
|
|
@@ -130,11 +148,10 @@ export async function scan(options) {
|
|
|
130
148
|
: majorMinor(frameworkVersion);
|
|
131
149
|
const source = override === null ? (detected?.source ?? 'package.json') : '.';
|
|
132
150
|
const projectDiagnostics = versionDiagnostics(framework, frameworkVersion, source, range);
|
|
133
|
-
// Without patterns, the configs choose among all JavaScript and TypeScript files.
|
|
134
|
-
const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
|
|
135
151
|
projectDiagnostics.push(...found.diagnostics);
|
|
136
152
|
const readerFor = moduleResolver(root);
|
|
137
153
|
const graph = new DependencyGraph();
|
|
154
|
+
const exports = new ExportedValues();
|
|
138
155
|
const configs = new Configs(root, framework, readerFor, rules);
|
|
139
156
|
const runs = new ConfigRuns(configs, framework, rules, patterns === null);
|
|
140
157
|
const placements = new Map(found.files.map((file) => [file.absolutePath, runs.placementOf(file.absolutePath)]));
|
|
@@ -193,11 +210,13 @@ export async function scan(options) {
|
|
|
193
210
|
framework,
|
|
194
211
|
version: rules,
|
|
195
212
|
comments,
|
|
213
|
+
expandCases,
|
|
196
214
|
readModule: readerFor(absolutePath),
|
|
197
215
|
root,
|
|
198
216
|
isBenchmarkFile: placement.runs === null ? null : placement.runs === 'benchmark',
|
|
199
217
|
config: fileConfig(placement, framework, rules, absolutePath),
|
|
200
218
|
graph,
|
|
219
|
+
exports,
|
|
201
220
|
runsWith,
|
|
202
221
|
snapshotFiles: { ...snapshotFiles, moved: runsWith.moved },
|
|
203
222
|
});
|
|
@@ -210,7 +229,9 @@ export async function scan(options) {
|
|
|
210
229
|
}
|
|
211
230
|
results.push({ ...result, file: { ...result.file, config } });
|
|
212
231
|
}
|
|
213
|
-
const missingGlobals =
|
|
232
|
+
const missingGlobals = framework === 'jest'
|
|
233
|
+
? (file) => jestGlobals(placements.get(file))
|
|
234
|
+
: globalsCheck(root, configs, (file) => placements.get(file));
|
|
214
235
|
let globals = null;
|
|
215
236
|
for (const { absolutePath, relativeFilePath } of globalsFiles) {
|
|
216
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' | '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
|
};
|
|
@@ -222,6 +222,11 @@ export interface SuiteRecord {
|
|
|
222
222
|
isShuffled: boolean;
|
|
223
223
|
/** Declared with `.each` or `.for`. */
|
|
224
224
|
isParameterized: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* With `expandCases`, which case of its table or pass of its loop the describe is, from 0, as Vitest's `%#` counts;
|
|
227
|
+
* null without `expandCases`, or when the describe isn't one of several.
|
|
228
|
+
*/
|
|
229
|
+
caseIndex: Count | null;
|
|
225
230
|
tags: string[];
|
|
226
231
|
annotations: Annotation[];
|
|
227
232
|
/** Playwright only. */
|
|
@@ -237,7 +242,10 @@ export interface SuiteRecord {
|
|
|
237
242
|
/** Source text of the `test.slow()` condition; null when unconditional or not slow. */
|
|
238
243
|
slowCondition: string | null;
|
|
239
244
|
retries: Count | null;
|
|
240
|
-
/**
|
|
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
|
+
*/
|
|
241
249
|
retryDelay: Count | null;
|
|
242
250
|
/** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
|
|
243
251
|
retryCondition: string | null;
|
|
@@ -334,7 +342,10 @@ export interface TestRecord {
|
|
|
334
342
|
slowCondition: string | null;
|
|
335
343
|
/** How many times the runner retries the test when it fails; null when the file sets none. */
|
|
336
344
|
retries: Count | null;
|
|
337
|
-
/**
|
|
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
|
+
*/
|
|
338
349
|
retryDelay: Count | null;
|
|
339
350
|
/** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
|
|
340
351
|
retryCondition: string | null;
|
|
@@ -381,10 +392,17 @@ export interface TestRecord {
|
|
|
381
392
|
/** Declared with `.each` or `.for`, or inside a describe declared with them, so the runner repeats it. */
|
|
382
393
|
isParameterized: boolean;
|
|
383
394
|
/**
|
|
384
|
-
* How many times the runner repeats the test: the cases of its own table times those of each describe table
|
|
385
|
-
* it. Null when
|
|
395
|
+
* How many times the runner repeats the test: the cases of its own table times those of each describe table and the
|
|
396
|
+
* passes of each loop around it. Null when the scan can't work out a table or a loop's values, or the test is neither
|
|
397
|
+
* parameterized nor in a loop.
|
|
386
398
|
*/
|
|
387
399
|
caseCount: Count | null;
|
|
400
|
+
/**
|
|
401
|
+
* With `expandCases`, which case of its own table, or pass of the innermost loop around it inside its describe, the
|
|
402
|
+
* test is, from 0, as Vitest's `%#` counts; null without `expandCases`, or when the test isn't one of several. The
|
|
403
|
+
* case of a describe table or loop around the describe is the describe's `caseIndex`.
|
|
404
|
+
*/
|
|
405
|
+
caseIndex: Count | null;
|
|
388
406
|
/** Declared inside a loop, `forEach` or `map`. */
|
|
389
407
|
isInLoop: boolean;
|
|
390
408
|
/** Declared inside `if`, `?:`, `&&`, `switch` or a `catch` block. */
|
package/dist/values.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type Resolved = {
|
|
|
11
11
|
ok: false;
|
|
12
12
|
importedFrom: string | null;
|
|
13
13
|
};
|
|
14
|
+
/** What another module exports under a name, `default` included, for an import of the file. */
|
|
15
|
+
export type ImportReader = (source: string, name: string) => Resolved;
|
|
14
16
|
/** Whether a value can be used as a property name. */
|
|
15
17
|
export declare function isKey(value: Value): value is string | number;
|
|
16
18
|
export declare function isScopeNode(node: Node): boolean;
|
|
@@ -22,6 +24,11 @@ export declare class Values {
|
|
|
22
24
|
private readonly pending;
|
|
23
25
|
/** The functions being read at one of their calls, innermost last. */
|
|
24
26
|
private readonly calls;
|
|
27
|
+
private readonly imports;
|
|
28
|
+
private readonly assigned;
|
|
29
|
+
private readonly changed;
|
|
30
|
+
/** Without `imports`, a value imported from another module is not worked out. */
|
|
31
|
+
constructor(imports?: ImportReader | null);
|
|
25
32
|
/**
|
|
26
33
|
* Runs `read` with a function's parameters bound to the arguments of one call, worked out where the call is, so that
|
|
27
34
|
* titles and values built from them resolve, as in `test(\`${side} rejects a zero quantity\`)` inside
|
|
@@ -29,6 +36,18 @@ export declare class Values {
|
|
|
29
36
|
* takes the parameter's default value, worked out in `scopes`, the scopes around the function.
|
|
30
37
|
*/
|
|
31
38
|
withArguments<T>(fn: FunctionNode, args: readonly Resolved[], scopes: readonly Node[], read: () => T): T;
|
|
39
|
+
/**
|
|
40
|
+
* Runs `read` with the names a loop declares bound to one of the values it goes over, as in
|
|
41
|
+
* `for (const [tier, bps] of Object.entries(FEES))`. `declarator` is the loop's variable declarator.
|
|
42
|
+
*/
|
|
43
|
+
withLoopValue<T>(declarator: Node, pattern: Node, value: Resolved, body: Node, scopes: readonly Node[], read: () => T): T;
|
|
44
|
+
private assignedIn;
|
|
45
|
+
private withBound;
|
|
46
|
+
/**
|
|
47
|
+
* Binds the names of a parameter or a loop variable to the parts of `value` they take, as destructuring does. A
|
|
48
|
+
* missing `value`, after a spread argument, leaves them unknown, unless a default gives them one.
|
|
49
|
+
*/
|
|
50
|
+
private bind;
|
|
32
51
|
/** `scopes` lists the scope nodes around the expression, outermost first. */
|
|
33
52
|
resolve(node: Expression, scopes: readonly Node[]): Resolved;
|
|
34
53
|
/**
|
|
@@ -41,14 +60,43 @@ export declare class Values {
|
|
|
41
60
|
init: Expression;
|
|
42
61
|
scopes: readonly Node[];
|
|
43
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;
|
|
44
65
|
/** The module a name is imported from where it is used; null when it isn't an import. */
|
|
45
66
|
importSource(name: string, scopes: readonly Node[]): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* Whether a name refers to a parameter or loop variable bound to a value where it is used: an argument of the helper
|
|
69
|
+
* call being walked, or a value of the case or pass of an expansion.
|
|
70
|
+
*/
|
|
71
|
+
isBound(name: string, scopes: readonly Node[]): boolean;
|
|
46
72
|
/** Whether a name refers to a function or class declared in the file. */
|
|
47
73
|
isFunction(name: string, scopes: readonly Node[]): boolean;
|
|
48
74
|
private propertyKey;
|
|
49
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The calls titles are often built with, all of them pure: `Object.keys()`, `Object.values()`, `Object.entries()`,
|
|
77
|
+
* `JSON.stringify()` with one argument, `String()`, and a string's `toUpperCase()`, `toLowerCase()` and `trim()` and
|
|
78
|
+
* an array's `join()`.
|
|
79
|
+
*/
|
|
80
|
+
private call;
|
|
81
|
+
/** Whether a name refers to a global, as no scope around the use declares it. */
|
|
82
|
+
private isGlobal;
|
|
83
|
+
/** `data.name` where `data` is `import * as data`: what the module exports as `name`; null for any other member. */
|
|
84
|
+
private namespaceMember;
|
|
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;
|
|
94
|
+
/** What a name refers to where it is used; `scopes` lists the scope nodes around the use, outermost first. */
|
|
95
|
+
resolveName(name: string, scopes: readonly Node[]): Resolved;
|
|
50
96
|
/** A parameter of a function read at one of its calls: the argument given there. */
|
|
51
97
|
private resolveParameter;
|
|
98
|
+
/** Names the module changes while it loads, as in `WATCHED.push('MSFT')`, whose declared value isn't the one used. */
|
|
99
|
+
private changedAtLoad;
|
|
52
100
|
private resolveConstant;
|
|
53
101
|
private enumMembers;
|
|
54
102
|
private lookup;
|