test-inventory 0.4.1 → 0.6.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 +96 -0
- package/README.md +4 -1
- package/dist/ast.d.ts +3 -3
- package/dist/ast.js +4 -4
- package/dist/bin.js +1 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +27 -7
- package/dist/config-dependencies.d.ts +62 -0
- package/dist/config-dependencies.js +231 -0
- package/dist/config-runs.d.ts +113 -0
- package/dist/config-runs.js +807 -0
- package/dist/config.d.ts +90 -0
- package/dist/config.js +652 -0
- package/dist/dependency-hash.d.ts +242 -0
- package/dist/dependency-hash.js +1458 -0
- package/dist/diagnostics.js +6 -1
- package/dist/extract.d.ts +20 -0
- package/dist/extract.js +20 -4
- package/dist/filter.d.ts +6 -0
- package/dist/filter.js +18 -1
- package/dist/frameworks/bindings.d.ts +22 -0
- package/dist/frameworks/bindings.js +19 -9
- package/dist/frameworks/common.d.ts +5 -0
- package/dist/frameworks/common.js +11 -0
- package/dist/frameworks/playwright.d.ts +2 -1
- package/dist/frameworks/playwright.js +11 -2
- package/dist/frameworks/vitest.d.ts +2 -1
- package/dist/frameworks/vitest.js +9 -3
- package/dist/hash.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/inventory.d.ts +2 -2
- package/dist/inventory.js +2 -1
- package/dist/json.js +2 -0
- package/dist/model.d.ts +9 -1
- package/dist/options.d.ts +2 -2
- package/dist/records.d.ts +58 -2
- package/dist/records.js +196 -28
- package/dist/scan-file.d.ts +13 -1
- package/dist/scan-file.js +16 -4
- package/dist/scan-source.js +1 -1
- package/dist/scan.d.ts +2 -1
- package/dist/scan.js +131 -18
- package/dist/types.d.ts +83 -7
- package/dist/values.d.ts +5 -0
- package/dist/values.js +5 -0
- package/dist/vitest-config.d.ts +6 -4
- package/dist/vitest-config.js +32 -28
- package/dist/vitest-defaults.d.ts +6 -0
- package/dist/vitest-defaults.js +16 -0
- package/package.json +10 -8
- package/schema/v1.json +200 -6
package/dist/scan.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { existsSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { glob } from 'tinyglobby';
|
|
5
|
+
import { ConfigDependencies } from "./config-dependencies.js";
|
|
6
|
+
import { ConfigRuns, fileConfig } from "./config-runs.js";
|
|
7
|
+
import { Configs } from "./config.js";
|
|
8
|
+
import { DependencyGraph } from "./dependency-hash.js";
|
|
4
9
|
import { fileDiagnostic } from "./diagnostics.js";
|
|
5
10
|
import { findFiles, normalizePattern, readFailure } from "./files.js";
|
|
6
11
|
import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
|
|
7
12
|
import { createInventory } from "./inventory.js";
|
|
8
|
-
import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, } from "./options.js";
|
|
13
|
+
import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
|
|
9
14
|
import { moduleResolver } from "./resolve.js";
|
|
10
15
|
import { noRecords, scanFile } from "./scan-file.js";
|
|
11
16
|
import { globalsCheck } from "./vitest-config.js";
|
|
@@ -46,14 +51,66 @@ function folderHint(root, patterns, framework) {
|
|
|
46
51
|
const example = `${folder.slice(0, end)}/**/*.${framework === 'playwright' ? 'spec' : 'test'}.ts`;
|
|
47
52
|
return ` "${folder}" is a folder; to scan the test files in it, use a pattern such as "${example}".`;
|
|
48
53
|
}
|
|
54
|
+
/** The JavaScript and TypeScript files the runners read, which the configs choose the test files from. */
|
|
55
|
+
const SCRIPT_FILES = '**/*.{js,jsx,ts,tsx,mjs,mjsx,mts,mtsx,cjs,cjsx,cts,ctsx}';
|
|
56
|
+
/** Whether an error says a file or folder isn't there, as a snapshot isn't before a test first writes it. */
|
|
57
|
+
function isMissing(error) {
|
|
58
|
+
return error.code === 'ENOENT';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The files in a folder of the project and the folders in it, linked files too, as paths in the folder with `/`;
|
|
62
|
+
* none for no folder.
|
|
63
|
+
*/
|
|
64
|
+
function filesIn(root, folder) {
|
|
65
|
+
const base = path.join(root, folder);
|
|
66
|
+
try {
|
|
67
|
+
return readdirSync(base, { recursive: true, withFileTypes: true })
|
|
68
|
+
.filter((entry) => entry.isFile() || (entry.isSymbolicLink() && isFile(path.join(entry.parentPath, entry.name))))
|
|
69
|
+
.map((entry) => toPosixPath(path.relative(base, path.join(entry.parentPath, entry.name))))
|
|
70
|
+
.sort();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return isMissing(error) ? [] : 'unreadable';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function isFile(file) {
|
|
77
|
+
try {
|
|
78
|
+
return statSync(file).isFile();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** A file's bytes, from the project root; null when there is none. */
|
|
85
|
+
function bytesOf(root, file) {
|
|
86
|
+
try {
|
|
87
|
+
return readFileSync(path.join(root, file));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return isMissing(error) ? null : 'unreadable';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Why a scan found no test files: what the patterns matched, or whether there is a config to find them from. */
|
|
94
|
+
function noFiles(root, patterns, framework, placements) {
|
|
95
|
+
if (patterns) {
|
|
96
|
+
const list = patterns.map((pattern) => `"${pattern}"`).join(', ');
|
|
97
|
+
return `No test files match ${list} in the root.${folderHint(root, patterns, framework)}`;
|
|
98
|
+
}
|
|
99
|
+
if ([...placements.values()].some((placement) => placement.config)) {
|
|
100
|
+
return 'No test files were found from the configs in the root.';
|
|
101
|
+
}
|
|
102
|
+
const name = framework === 'playwright' ? 'Playwright' : 'Vitest';
|
|
103
|
+
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
|
+
}
|
|
49
105
|
/**
|
|
50
|
-
* Scans the test files that match `patterns` under `root`, one file at a
|
|
106
|
+
* Scans the test files that match `patterns` under `root`, or without patterns those the configs run, one file at a
|
|
107
|
+
* time.
|
|
51
108
|
*
|
|
52
109
|
* Throws a `TypeError` for invalid options. Problems with files and code are reported as diagnostics.
|
|
53
110
|
*/
|
|
54
111
|
export async function scan(options) {
|
|
55
112
|
const checked = checkObject(options, 'options');
|
|
56
|
-
const patterns = checkStrings(checked.patterns, 'options.patterns', true);
|
|
113
|
+
const patterns = checked.patterns === undefined ? null : checkStrings(checked.patterns, 'options.patterns', true);
|
|
57
114
|
const framework = checkFramework(checked.framework, 'options.framework');
|
|
58
115
|
const ignore = checkStrings(checked.ignore, 'options.ignore', false);
|
|
59
116
|
const comments = checkComments(checked.comments, 'options.comments');
|
|
@@ -73,45 +130,101 @@ export async function scan(options) {
|
|
|
73
130
|
: majorMinor(frameworkVersion);
|
|
74
131
|
const source = override === null ? (detected?.source ?? 'package.json') : '.';
|
|
75
132
|
const projectDiagnostics = versionDiagnostics(framework, frameworkVersion, source, range);
|
|
76
|
-
|
|
133
|
+
// Without patterns, the configs choose among all JavaScript and TypeScript files.
|
|
134
|
+
const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
|
|
77
135
|
projectDiagnostics.push(...found.diagnostics);
|
|
78
|
-
if (found.files.length === 0) {
|
|
79
|
-
const list = patterns.map((pattern) => `"${pattern}"`).join(', ');
|
|
80
|
-
const message = `No test files match ${list} in the root.${folderHint(root, patterns, framework)}`;
|
|
81
|
-
projectDiagnostics.push(fileDiagnostic('no-files-matched', '.', message));
|
|
82
|
-
}
|
|
83
136
|
const readerFor = moduleResolver(root);
|
|
137
|
+
const graph = new DependencyGraph();
|
|
138
|
+
const configs = new Configs(root, framework, readerFor, rules);
|
|
139
|
+
const runs = new ConfigRuns(configs, framework, rules, patterns === null);
|
|
140
|
+
const placements = new Map(found.files.map((file) => [file.absolutePath, runs.placementOf(file.absolutePath)]));
|
|
141
|
+
const files = patterns ? found.files : found.files.filter((file) => placements.get(file.absolutePath)?.runs);
|
|
142
|
+
// A setup project's files may be outside the patterns, or ignored for the scan, so they are looked for in its testDir.
|
|
143
|
+
const setupDirs = new Set();
|
|
144
|
+
for (const { config, projects } of placements.values()) {
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
const names = (projects ?? []).flatMap((project) => project.dependencies);
|
|
147
|
+
for (let name = names.pop(); config && name !== undefined; name = names.pop()) {
|
|
148
|
+
const project = seen.has(name) ? null : runs.projectNamed(config, name);
|
|
149
|
+
seen.add(name);
|
|
150
|
+
if (!project)
|
|
151
|
+
continue;
|
|
152
|
+
setupDirs.add(project.about.testDir);
|
|
153
|
+
names.push(...project.about.dependencies);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Searched from the folder itself, as a folder named like `(auth)` would be read as a glob in a pattern.
|
|
157
|
+
const setupFiles = await Promise.all([...setupDirs].map((folder) => glob(SCRIPT_FILES, { cwd: folder, absolute: true, ignore: ['**/node_modules/**'] })));
|
|
158
|
+
const candidates = setupFiles.flat().map((file) => path.resolve(file));
|
|
159
|
+
const configDependencies = new ConfigDependencies(framework, runs, readerFor, candidates);
|
|
160
|
+
// Many tests of a file compare against one folder, so each is listed once.
|
|
161
|
+
const listed = new Map();
|
|
162
|
+
const list = (folder) => {
|
|
163
|
+
if (!listed.has(folder))
|
|
164
|
+
listed.set(folder, filesIn(root, folder));
|
|
165
|
+
return listed.get(folder);
|
|
166
|
+
};
|
|
167
|
+
const snapshotFiles = { framework, list, read: (file) => bytesOf(root, file) };
|
|
168
|
+
const notRun = [];
|
|
84
169
|
const results = [];
|
|
85
170
|
const globalsFiles = [];
|
|
86
|
-
for (const { absolutePath, relativeFilePath } of
|
|
171
|
+
for (const { absolutePath, relativeFilePath } of files) {
|
|
172
|
+
const placement = placements.get(absolutePath);
|
|
173
|
+
if (placement.config)
|
|
174
|
+
configs.use(placement.config);
|
|
175
|
+
const config = placement.config?.record.relativeFilePath ?? null;
|
|
87
176
|
let code;
|
|
88
177
|
try {
|
|
89
178
|
code = await readFile(absolutePath, 'utf8');
|
|
90
179
|
}
|
|
91
180
|
catch (error) {
|
|
181
|
+
const records = noRecords(relativeFilePath);
|
|
92
182
|
results.push({
|
|
93
|
-
...
|
|
183
|
+
...records,
|
|
184
|
+
file: { ...records.file, config },
|
|
94
185
|
diagnostics: [
|
|
95
186
|
fileDiagnostic('read-error', relativeFilePath, `The file could not be read: ${readFailure(error)}.`),
|
|
96
187
|
],
|
|
97
188
|
});
|
|
98
189
|
continue;
|
|
99
190
|
}
|
|
191
|
+
const runsWith = configDependencies.of(placement);
|
|
100
192
|
const result = scanFile(code, relativeFilePath, {
|
|
101
193
|
framework,
|
|
102
194
|
version: rules,
|
|
103
195
|
comments,
|
|
104
196
|
readModule: readerFor(absolutePath),
|
|
105
197
|
root,
|
|
198
|
+
isBenchmarkFile: placement.runs === null ? null : placement.runs === 'benchmark',
|
|
199
|
+
config: fileConfig(placement, framework, rules, absolutePath),
|
|
200
|
+
graph,
|
|
201
|
+
runsWith,
|
|
202
|
+
snapshotFiles: { ...snapshotFiles, moved: runsWith.moved },
|
|
106
203
|
});
|
|
107
204
|
if (result.usesGlobals)
|
|
108
205
|
globalsFiles.push({ absolutePath, relativeFilePath });
|
|
109
|
-
|
|
206
|
+
// A file without tests that no config runs, such as a source file a broad pattern matches, misses nothing.
|
|
207
|
+
if (placement.runs === false && result.tests.length > 0) {
|
|
208
|
+
const message = `The file has tests, but no config in its folder or above runs it, so the runner doesn't collect them; the nearest config is ${config}.`;
|
|
209
|
+
notRun.push(fileDiagnostic('file-not-run', relativeFilePath, message));
|
|
210
|
+
}
|
|
211
|
+
results.push({ ...result, file: { ...result.file, config } });
|
|
212
|
+
}
|
|
213
|
+
const missingGlobals = globalsCheck(root, configs, (file) => placements.get(file));
|
|
214
|
+
let globals = null;
|
|
215
|
+
for (const { absolutePath, relativeFilePath } of globalsFiles) {
|
|
216
|
+
const message = missingGlobals(absolutePath);
|
|
217
|
+
if (message === null)
|
|
218
|
+
continue;
|
|
219
|
+
globals = fileDiagnostic('globals-not-enabled', relativeFilePath, message);
|
|
220
|
+
break;
|
|
110
221
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
projectDiagnostics.push(fileDiagnostic('
|
|
222
|
+
projectDiagnostics.push(...configs.diagnostics, ...notRun, ...(globals ? [globals] : []));
|
|
223
|
+
// After the config warnings, which may say why no file was found.
|
|
224
|
+
if (files.length === 0) {
|
|
225
|
+
projectDiagnostics.push(fileDiagnostic('no-files-matched', '.', noFiles(root, patterns, framework, placements)));
|
|
115
226
|
}
|
|
116
|
-
|
|
227
|
+
const fileCount = files.length;
|
|
228
|
+
const records = configs.records((config) => runs.projectsIn(config));
|
|
229
|
+
return createInventory(framework, frameworkVersion, fileCount, results, projectDiagnostics, records);
|
|
117
230
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -20,18 +20,25 @@ export type Framework = 'playwright' | 'vitest';
|
|
|
20
20
|
*/
|
|
21
21
|
export type TestState = 'active' | 'skip' | 'fixme' | 'todo' | 'expectedToFail' | 'notLoaded';
|
|
22
22
|
/**
|
|
23
|
-
* Where a test's state is set: on the test, on a describe around it,
|
|
23
|
+
* Where a test's state is set: on the test, on a describe around it, at file level, or by a Vitest tag the config
|
|
24
|
+
* defines.
|
|
24
25
|
*
|
|
25
26
|
* @inline
|
|
26
27
|
*/
|
|
27
|
-
export type StateSource = 'test' | 'suite' | 'file';
|
|
28
|
+
export type StateSource = 'test' | 'suite' | 'file' | 'config';
|
|
29
|
+
/**
|
|
30
|
+
* Where a test's run setting comes from: the test, a describe around it, the file, or the config its file uses.
|
|
31
|
+
*
|
|
32
|
+
* @inline
|
|
33
|
+
*/
|
|
34
|
+
export type SettingSource = 'test' | 'suite' | 'file' | 'config';
|
|
28
35
|
/**
|
|
29
36
|
* `error`: tests may be missing from the output. `warning`: a value is missing or approximate.
|
|
30
37
|
*
|
|
31
38
|
* @inline
|
|
32
39
|
*/
|
|
33
40
|
export type DiagnosticLevel = 'error' | 'warning';
|
|
34
|
-
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' | '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';
|
|
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';
|
|
35
42
|
export type MetaValue = string | number | boolean | null | MetaValue[] | {
|
|
36
43
|
[key: string]: MetaValue;
|
|
37
44
|
};
|
|
@@ -45,6 +52,8 @@ export interface Inventory {
|
|
|
45
52
|
/** Read from the project, or given with `--framework-version`; null when unknown. */
|
|
46
53
|
frameworkVersion: string | null;
|
|
47
54
|
summary: Summary;
|
|
55
|
+
/** The configs the scanned files use, sorted by path. */
|
|
56
|
+
configs: ConfigRecord[];
|
|
48
57
|
/** One per scanned file, in the order the files were scanned. */
|
|
49
58
|
files: FileRecord[];
|
|
50
59
|
suites: SuiteRecord[];
|
|
@@ -53,12 +62,14 @@ export interface Inventory {
|
|
|
53
62
|
}
|
|
54
63
|
/**
|
|
55
64
|
* One line of the JSON Lines output (`--format jsonl`): the inventory with each record on its own line, marked by
|
|
56
|
-
* `type`. The `inventory` line comes first, then the files, suites, tests and diagnostics in the order of the
|
|
57
|
-
* output, and the `summary` line last.
|
|
65
|
+
* `type`. The `inventory` line comes first, then the configs, files, suites, tests and diagnostics in the order of the
|
|
66
|
+
* JSON output, and the `summary` line last.
|
|
58
67
|
*/
|
|
59
68
|
export type InventoryLine = ({
|
|
60
69
|
type: 'inventory';
|
|
61
70
|
} & Pick<Inventory, 'schemaVersion' | 'tool' | 'framework' | 'frameworkVersion'>) | ({
|
|
71
|
+
type: 'config';
|
|
72
|
+
} & ConfigRecord) | ({
|
|
62
73
|
type: 'file';
|
|
63
74
|
} & FileRecord) | ({
|
|
64
75
|
type: 'suite';
|
|
@@ -103,11 +114,40 @@ export interface Summary {
|
|
|
103
114
|
/** Tests per annotation type. */
|
|
104
115
|
annotationCounts: Record<string, Count>;
|
|
105
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* A Playwright or Vitest config that scanned files use: the nearest one in a file's folder or a folder above it, up to
|
|
119
|
+
* the root, as the runner started in that folder would. Read as code, never run.
|
|
120
|
+
*/
|
|
121
|
+
export interface ConfigRecord {
|
|
122
|
+
/** Path from the project root, with `/` separators. */
|
|
123
|
+
relativeFilePath: string;
|
|
124
|
+
/** Files the config takes settings from through imports, such as a shared base config, in the order read. */
|
|
125
|
+
extends: string[];
|
|
126
|
+
/**
|
|
127
|
+
* The projects the config runs tests in: Playwright's `projects`, or one for a config without them; Vitest's, with
|
|
128
|
+
* each browser instance as one. Null when the scan can't work them out.
|
|
129
|
+
*/
|
|
130
|
+
projects: ProjectRecord[] | null;
|
|
131
|
+
}
|
|
132
|
+
/** @inline */
|
|
133
|
+
export interface ProjectRecord {
|
|
134
|
+
/** As the runner names it: `''` for a project without a name. */
|
|
135
|
+
name: string;
|
|
136
|
+
/** The folder the project's test files are found in, from the project root: Playwright's `testDir`, Vitest's `dir`. */
|
|
137
|
+
testDir: string;
|
|
138
|
+
/** Playwright's projects that run before this one; `[]` for Vitest. */
|
|
139
|
+
dependencies: string[];
|
|
140
|
+
}
|
|
106
141
|
/** A scanned file, also one that has no tests or could not be read. */
|
|
107
142
|
export interface FileRecord {
|
|
108
143
|
/** Path from the project root, with `/` separators. */
|
|
109
144
|
relativeFilePath: string;
|
|
110
145
|
fileName: string;
|
|
146
|
+
/**
|
|
147
|
+
* The config the file uses, from the project root: the outermost config at or above it that runs it, as the runner
|
|
148
|
+
* started at the root would, else the nearest. Null when there is none.
|
|
149
|
+
*/
|
|
150
|
+
config: string | null;
|
|
111
151
|
/** Hooks at the top level of the file, which run around every test in it. */
|
|
112
152
|
hooks: HookCounts;
|
|
113
153
|
suiteCount: Count;
|
|
@@ -210,6 +250,16 @@ export interface TestRecord {
|
|
|
210
250
|
id: Hash;
|
|
211
251
|
/** Hash of the test body, ignoring whitespace and comments; null when the test has no body. */
|
|
212
252
|
bodyHash: Hash | null;
|
|
253
|
+
/**
|
|
254
|
+
* Hash of everything the test runs in the project: its call, the hooks, options and code of the file and of every
|
|
255
|
+
* describe around it, and the functions, classes, constants and modules these use, followed through imports. Ignores
|
|
256
|
+
* whitespace and comments. Installed packages are not in it.
|
|
257
|
+
*/
|
|
258
|
+
dependencyHash: Hash;
|
|
259
|
+
/** The files that went into `dependencyHash`, the test's own included, sorted, with `/` separators. */
|
|
260
|
+
dependencies: string[];
|
|
261
|
+
/** Something the test reaches couldn't be followed, so a change there doesn't change `dependencyHash`. */
|
|
262
|
+
hasUnfollowedDependencies: boolean;
|
|
213
263
|
/** The nearest describe; null outside any describe. */
|
|
214
264
|
suiteId: Hash | null;
|
|
215
265
|
/** Titles of the describes around the test, outermost first. */
|
|
@@ -275,7 +325,7 @@ export interface TestRecord {
|
|
|
275
325
|
hooks: HookCounts;
|
|
276
326
|
/**
|
|
277
327
|
* In milliseconds: the last `test.setTimeout()` in the body or a `beforeEach`, otherwise the nearest describe or file
|
|
278
|
-
* that sets one. Null when
|
|
328
|
+
* that sets one, otherwise the config. Null when none sets it, so the runner's default applies.
|
|
279
329
|
*/
|
|
280
330
|
timeout: Count | null;
|
|
281
331
|
/** Playwright `test.slow()` applies: the runner triples the timeout. */
|
|
@@ -288,8 +338,29 @@ export interface TestRecord {
|
|
|
288
338
|
retryDelay: Count | null;
|
|
289
339
|
/** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
|
|
290
340
|
retryCondition: string | null;
|
|
291
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* How many times the runner runs the test again after the first run: Vitest's `repeats`, or Playwright's `repeatEach`
|
|
343
|
+
* from the config less one.
|
|
344
|
+
*/
|
|
292
345
|
repeats: Count | null;
|
|
346
|
+
/**
|
|
347
|
+
* Where `timeout` comes from; null when nothing sets it. A null `timeout` from the config means the config sets it, but
|
|
348
|
+
* its projects set different values or the value depends on a condition, as in `process.env.CI ? 2 : 0`.
|
|
349
|
+
*/
|
|
350
|
+
timeoutSource: SettingSource | null;
|
|
351
|
+
/** Where `retries`, `retryDelay` and `retryCondition` come from, read as `timeoutSource` is. */
|
|
352
|
+
retriesSource: SettingSource | null;
|
|
353
|
+
/** Where `repeats` comes from, read as `timeoutSource` is. */
|
|
354
|
+
repeatsSource: SettingSource | null;
|
|
355
|
+
/**
|
|
356
|
+
* The config's source text for `timeout` when the config sets it under a condition, as in
|
|
357
|
+
* `process.env.CI ? 60_000 : 30_000`, shortened when long; null otherwise.
|
|
358
|
+
*/
|
|
359
|
+
timeoutConfigText: string | null;
|
|
360
|
+
/** The config's source text for `retries`, read as `timeoutConfigText` is. */
|
|
361
|
+
retriesConfigText: string | null;
|
|
362
|
+
/** The config's source text for `repeats`, Playwright's `repeatEach`, read as `timeoutConfigText` is. */
|
|
363
|
+
repeatsConfigText: string | null;
|
|
293
364
|
/**
|
|
294
365
|
* Names the test function takes from its context, then those of the `beforeEach`, `afterEach` and `aroundEach` hooks
|
|
295
366
|
* around it, once each. Vitest's own context keys, such as `expect` and `task`, are left out.
|
|
@@ -320,6 +391,11 @@ export interface TestRecord {
|
|
|
320
391
|
isInCondition: boolean;
|
|
321
392
|
/** Declared inside a plain function. */
|
|
322
393
|
isInFunction: boolean;
|
|
394
|
+
/**
|
|
395
|
+
* Names of the projects that run the test, of the config its file uses, after each project's files and Playwright's
|
|
396
|
+
* `grep` and `grepInvert`. `[]` when no project runs it; null when there is no config, or the scan can't tell.
|
|
397
|
+
*/
|
|
398
|
+
projects: string[] | null;
|
|
323
399
|
}
|
|
324
400
|
/** @inline */
|
|
325
401
|
export interface Step {
|
package/dist/values.d.ts
CHANGED
|
@@ -36,6 +36,11 @@ export declare class Values {
|
|
|
36
36
|
* the file does not declare it, as with globals.
|
|
37
37
|
*/
|
|
38
38
|
declarationOf(name: string, scopes: readonly Node[]): Node | null;
|
|
39
|
+
/** The value a `const` name is declared with, and the scopes around that declaration; null for any other name. */
|
|
40
|
+
constantOf(name: string, scopes: readonly Node[]): {
|
|
41
|
+
init: Expression;
|
|
42
|
+
scopes: readonly Node[];
|
|
43
|
+
} | null;
|
|
39
44
|
/** The module a name is imported from where it is used; null when it isn't an import. */
|
|
40
45
|
importSource(name: string, scopes: readonly Node[]): string | null;
|
|
41
46
|
/** Whether a name refers to a function or class declared in the file. */
|
package/dist/values.js
CHANGED
|
@@ -208,6 +208,11 @@ export class Values {
|
|
|
208
208
|
declarationOf(name, scopes) {
|
|
209
209
|
return this.lookup(name, scopes)?.binding.declaration ?? null;
|
|
210
210
|
}
|
|
211
|
+
/** The value a `const` name is declared with, and the scopes around that declaration; null for any other name. */
|
|
212
|
+
constantOf(name, scopes) {
|
|
213
|
+
const binding = this.lookup(name, scopes)?.binding;
|
|
214
|
+
return binding?.kind === 'const' ? { init: binding.init, scopes: binding.scopes } : null;
|
|
215
|
+
}
|
|
211
216
|
/** The module a name is imported from where it is used; null when it isn't an import. */
|
|
212
217
|
importSource(name, scopes) {
|
|
213
218
|
const binding = this.lookup(name, scopes)?.binding;
|
package/dist/vitest-config.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { Placement } from './config-runs.ts';
|
|
2
|
+
import { type Configs } from './config.ts';
|
|
1
3
|
/**
|
|
2
|
-
* Returns a check of whether Vitest's globals are turned on for a file
|
|
3
|
-
* above it up to the root
|
|
4
|
-
* never run.
|
|
4
|
+
* Returns a check of whether Vitest's globals are turned on for a file: by the project of the config it uses, or by a
|
|
5
|
+
* tsconfig in its folder or any folder above it up to the root. The check gives the message to report when they are
|
|
6
|
+
* not, and null when they are or may be. Config files are read as code, never run.
|
|
5
7
|
*/
|
|
6
|
-
export declare function globalsCheck(root: string): (file: string) =>
|
|
8
|
+
export declare function globalsCheck(root: string, configs: Configs, placementOf: (file: string) => Placement): (file: string) => string | null;
|
package/dist/vitest-config.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import { parseFile } from "./parse.js";
|
|
5
|
-
const CONFIG_FILES = ['vitest.config', 'vite.config'].flatMap((name) => ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'].map((extension) => name + extension));
|
|
6
|
-
function setsGlobals(node) {
|
|
7
|
-
if (node.type === 'Property' &&
|
|
8
|
-
!node.computed &&
|
|
9
|
-
((node.key.type === 'Identifier' && node.key.name === 'globals') ||
|
|
10
|
-
(node.key.type === 'Literal' && node.key.value === 'globals')) &&
|
|
11
|
-
node.value.type === 'Literal' &&
|
|
12
|
-
node.value.value === true) {
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
return childNodes(node).some(setsGlobals);
|
|
16
|
-
}
|
|
3
|
+
import { settingAt } from "./config.js";
|
|
17
4
|
/** A file's text, or null when it is missing or can't be read. */
|
|
18
5
|
function readText(file) {
|
|
19
6
|
try {
|
|
@@ -23,13 +10,8 @@ function readText(file) {
|
|
|
23
10
|
return null;
|
|
24
11
|
}
|
|
25
12
|
}
|
|
26
|
-
/** Whether a folder has a
|
|
27
|
-
function
|
|
28
|
-
for (const name of CONFIG_FILES) {
|
|
29
|
-
const code = readText(path.join(directory, name));
|
|
30
|
-
if (code !== null && setsGlobals(parseFile(name, code).program))
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
13
|
+
/** Whether a folder has a tsconfig with `vitest/globals` in its types. */
|
|
14
|
+
function typesGlobals(directory) {
|
|
33
15
|
let names;
|
|
34
16
|
try {
|
|
35
17
|
names = readdirSync(directory);
|
|
@@ -39,21 +21,43 @@ function enablesGlobals(directory) {
|
|
|
39
21
|
}
|
|
40
22
|
return names.some((name) => /^tsconfig.*\.json$/.test(name) && (readText(path.join(directory, name)) ?? '').includes('"vitest/globals"'));
|
|
41
23
|
}
|
|
24
|
+
const ASSUMED = "files that use Vitest's global test functions are taken to have them";
|
|
25
|
+
/** Whether a project's settings turn Vitest's globals on; null when the scan can't tell. */
|
|
26
|
+
function turnsOnGlobals(settings, configs) {
|
|
27
|
+
// A project the scan can't work out was reported where its files were asked for.
|
|
28
|
+
if (settings.kind === 'unknown')
|
|
29
|
+
return null;
|
|
30
|
+
const globals = settingAt(settings, ['test', 'globals']);
|
|
31
|
+
if (globals?.kind !== 'unknown')
|
|
32
|
+
return globals?.kind === 'value' && globals.value === true;
|
|
33
|
+
configs.reportUnknown(globals, 'test.globals', ASSUMED);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
42
36
|
/**
|
|
43
|
-
* Returns a check of whether Vitest's globals are turned on for a file
|
|
44
|
-
* above it up to the root
|
|
45
|
-
* never run.
|
|
37
|
+
* Returns a check of whether Vitest's globals are turned on for a file: by the project of the config it uses, or by a
|
|
38
|
+
* tsconfig in its folder or any folder above it up to the root. The check gives the message to report when they are
|
|
39
|
+
* not, and null when they are or may be. Config files are read as code, never run.
|
|
46
40
|
*/
|
|
47
|
-
export function globalsCheck(root) {
|
|
41
|
+
export function globalsCheck(root, configs, placementOf) {
|
|
48
42
|
const known = new Map();
|
|
49
|
-
const
|
|
43
|
+
const typed = (directory) => {
|
|
50
44
|
let result = known.get(directory);
|
|
51
45
|
if (result === undefined) {
|
|
52
46
|
const parent = path.dirname(directory);
|
|
53
|
-
result =
|
|
47
|
+
result = typesGlobals(directory) || (directory !== root && parent !== directory && typed(parent));
|
|
54
48
|
known.set(directory, result);
|
|
55
49
|
}
|
|
56
50
|
return result;
|
|
57
51
|
};
|
|
58
|
-
return (file) =>
|
|
52
|
+
return (file) => {
|
|
53
|
+
if (typed(path.dirname(file)))
|
|
54
|
+
return null;
|
|
55
|
+
const { config, settings } = placementOf(file);
|
|
56
|
+
if (config && settings && turnsOnGlobals(settings, configs) !== false)
|
|
57
|
+
return null;
|
|
58
|
+
const why = config
|
|
59
|
+
? `${config.record.relativeFilePath} doesn't set test.globals to true for it`
|
|
60
|
+
: 'no Vitest or Vite config was found';
|
|
61
|
+
return `The file uses Vitest's global test functions, but ${why} and no tsconfig has vitest/globals in its types.`;
|
|
62
|
+
};
|
|
59
63
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Vitest's default file settings, as `configDefaults` in `vitest/config` holds them. */
|
|
2
|
+
export declare const VITEST_INCLUDE: string[];
|
|
3
|
+
export declare const VITEST_TYPECHECK_INCLUDE: string[];
|
|
4
|
+
export declare const VITEST_BENCHMARK_INCLUDE: string[];
|
|
5
|
+
/** Vitest's default `exclude`: Vitest 4 left out the folders and tool configs that Vitest 3 excludes. */
|
|
6
|
+
export declare function vitestExclude(version: [number, number] | null): string[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Vitest's default file settings, as `configDefaults` in `vitest/config` holds them. */
|
|
2
|
+
export const VITEST_INCLUDE = ['**/*.{test,spec}.?(c|m)[jt]s?(x)'];
|
|
3
|
+
export const VITEST_TYPECHECK_INCLUDE = ['**/*.{test,spec}-d.?(c|m)[jt]s?(x)'];
|
|
4
|
+
export const VITEST_BENCHMARK_INCLUDE = ['**/*.{bench,benchmark}.?(c|m)[jt]s?(x)'];
|
|
5
|
+
/** Vitest's default `exclude`: Vitest 4 left out the folders and tool configs that Vitest 3 excludes. */
|
|
6
|
+
export function vitestExclude(version) {
|
|
7
|
+
if (version === null || version[0] >= 4)
|
|
8
|
+
return ['**/node_modules/**', '**/.git/**'];
|
|
9
|
+
return [
|
|
10
|
+
'**/node_modules/**',
|
|
11
|
+
'**/dist/**',
|
|
12
|
+
'**/cypress/**',
|
|
13
|
+
'**/.{idea,git,cache,output,temp}/**',
|
|
14
|
+
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*',
|
|
15
|
+
];
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-inventory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Test metadata from source code, not from test results.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -79,7 +79,8 @@
|
|
|
79
79
|
"changeset": "changeset"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"oxc-parser": "0.
|
|
82
|
+
"oxc-parser": "0.156.0",
|
|
83
|
+
"picomatch": "^4.0.7",
|
|
83
84
|
"tinyglobby": "^0.2.17"
|
|
84
85
|
},
|
|
85
86
|
"devDependencies": {
|
|
@@ -87,19 +88,20 @@
|
|
|
87
88
|
"@changesets/changelog-github": "^1.0.1",
|
|
88
89
|
"@changesets/cli": "^3.0.3",
|
|
89
90
|
"@types/node": "~22.12.0",
|
|
90
|
-
"@
|
|
91
|
+
"@types/picomatch": "^4.0.3",
|
|
92
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
91
93
|
"ajv": "^8.20.0",
|
|
92
|
-
"eslint": "~10.
|
|
94
|
+
"eslint": "~10.13.0",
|
|
93
95
|
"prettier": "3.9.10",
|
|
94
96
|
"publint": "^0.3.25",
|
|
95
97
|
"ts-json-schema-generator": "2.9.0",
|
|
96
98
|
"typedoc": "^0.28.20",
|
|
97
|
-
"typedoc-plugin-markdown": "^4.
|
|
99
|
+
"typedoc-plugin-markdown": "^4.14.1",
|
|
98
100
|
"typedoc-vitepress-theme": "^1.1.4",
|
|
99
101
|
"typescript": "~6.0.3",
|
|
100
|
-
"typescript-eslint": "~8.
|
|
101
|
-
"vitepress": "2.0.0-alpha.
|
|
102
|
+
"typescript-eslint": "~8.72.1",
|
|
103
|
+
"vitepress": "2.0.0-alpha.21",
|
|
102
104
|
"vitepress-plugin-llms": "^1.14.0",
|
|
103
|
-
"vitest": "5.0.
|
|
105
|
+
"vitest": "5.0.5"
|
|
104
106
|
}
|
|
105
107
|
}
|