test-inventory 0.5.0 → 0.6.1
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 +103 -0
- package/dist/bin.js +1 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +23 -2
- package/dist/config-dependencies.d.ts +62 -0
- package/dist/config-dependencies.js +231 -0
- package/dist/config-runs.d.ts +47 -5
- package/dist/config-runs.js +205 -42
- package/dist/config.js +65 -3
- package/dist/dependency-hash.d.ts +250 -0
- package/dist/dependency-hash.js +1533 -0
- package/dist/diagnostics.js +1 -0
- package/dist/extract.d.ts +12 -0
- package/dist/extract.js +19 -6
- package/dist/filter.d.ts +6 -0
- package/dist/filter.js +11 -1
- package/dist/frameworks/common.d.ts +5 -0
- package/dist/frameworks/common.js +11 -0
- package/dist/frameworks/playwright.js +10 -2
- package/dist/frameworks/vitest.d.ts +1 -1
- package/dist/frameworks/vitest.js +36 -6
- package/dist/hash.d.ts +1 -1
- package/dist/model.d.ts +14 -2
- package/dist/parse.d.ts +6 -0
- package/dist/parse.js +204 -0
- package/dist/records.d.ts +6 -1
- package/dist/records.js +10 -2
- package/dist/resolve.js +28 -8
- package/dist/scan-file.d.ts +8 -0
- package/dist/scan-file.js +11 -2
- package/dist/scan.js +73 -2
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
- package/schema/v1.json +21 -1
package/dist/records.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type Extraction } from './extract.ts';
|
|
1
|
+
import { type Extraction, type PendingTest } from './extract.ts';
|
|
2
|
+
import type { Dependencies } from './dependency-hash.ts';
|
|
2
3
|
import type { RunSettings } from './model.ts';
|
|
3
4
|
import type { ParsedFile } from './parse.ts';
|
|
4
5
|
import type { Diagnostic, FileRecord, Framework, HookCounts, MetaValue, SuiteRecord, TestRecord } from './types.ts';
|
|
@@ -60,6 +61,8 @@ export interface FileConfig {
|
|
|
60
61
|
};
|
|
61
62
|
/** Vitest's tags; null when the scan can't work them out, or the projects that run the file define different ones. */
|
|
62
63
|
tags: VitestTags | null;
|
|
64
|
+
/** The tags Playwright's config `tag` gives every test, before the test's own. */
|
|
65
|
+
configTags: string[];
|
|
63
66
|
}
|
|
64
67
|
interface RecordOptions {
|
|
65
68
|
relativeFilePath: string;
|
|
@@ -69,6 +72,8 @@ interface RecordOptions {
|
|
|
69
72
|
/** A Vitest 5 benchmark file, such as `*.bench.ts`. */
|
|
70
73
|
isBenchmarkFile: boolean;
|
|
71
74
|
config: FileConfig | null;
|
|
75
|
+
/** What a test runs, for its `dependencyHash`. */
|
|
76
|
+
dependencies: (test: PendingTest) => Dependencies;
|
|
72
77
|
}
|
|
73
78
|
/** The record of a scanned file, from its records and the hooks at its top level. */
|
|
74
79
|
export declare function fileRecord(relativeFilePath: string, hooks: HookCounts, records: Pick<FileRecords, 'suites' | 'tests'>): FileRecord;
|
package/dist/records.js
CHANGED
|
@@ -314,7 +314,7 @@ class RecordBuilder {
|
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
316
|
/** What the config's Vitest tags set on a test with `tags`, merged as Vitest merges them; null when none applies. */
|
|
317
|
-
tagOptions(tags) {
|
|
317
|
+
tagOptions(tags, ownStateOptions) {
|
|
318
318
|
const definitions = this.options.config?.tags?.definitions ?? [];
|
|
319
319
|
const applied = tags.flatMap((name) => definitions.filter((definition) => definition.name === name));
|
|
320
320
|
if (applied.length === 0)
|
|
@@ -330,10 +330,14 @@ class RecordBuilder {
|
|
|
330
330
|
if (meta)
|
|
331
331
|
merged.meta = { ...merged.meta, ...meta };
|
|
332
332
|
}
|
|
333
|
+
// Vitest merges the test's own options over its tags', so `skip: false` runs a test whose tag skips it.
|
|
334
|
+
for (const option of ownStateOptions)
|
|
335
|
+
merged[option] = undefined;
|
|
333
336
|
return merged;
|
|
334
337
|
}
|
|
335
338
|
test(pending, suites) {
|
|
336
339
|
const { declaration } = pending;
|
|
340
|
+
const dependencies = this.options.dependencies(pending);
|
|
337
341
|
// A test from an imported helper is placed at the call in this file that registers it.
|
|
338
342
|
const located = pending.site ?? pending.call;
|
|
339
343
|
const scopes = outward(pending.scope);
|
|
@@ -342,11 +346,12 @@ class RecordBuilder {
|
|
|
342
346
|
const id = this.id([this.describeTitles(describes), title.withoutTags]);
|
|
343
347
|
const fileScope = scopes[scopes.length - 1];
|
|
344
348
|
const tags = unique([
|
|
349
|
+
...(this.options.config?.configTags ?? []),
|
|
345
350
|
...fileScope.details.tags,
|
|
346
351
|
...describes.flatMap((scope) => scope.details.tags),
|
|
347
352
|
...declaration.details.tags,
|
|
348
353
|
]);
|
|
349
|
-
const fromTags = this.tagOptions(tags);
|
|
354
|
+
const fromTags = this.tagOptions(tags, declaration.ownStateOptions ?? []);
|
|
350
355
|
// In Vitest 5 the options of a test's tags win over those the describes pass down; before, the describes' win.
|
|
351
356
|
const tagsFirst = this.options.config?.tags?.beatsDescribes ?? true;
|
|
352
357
|
const tagged = (own, tag, around) => tagsFirst ? [...own, ...tag, ...around] : [...own, ...around, ...tag];
|
|
@@ -396,6 +401,9 @@ class RecordBuilder {
|
|
|
396
401
|
const record = {
|
|
397
402
|
id,
|
|
398
403
|
bodyHash: declaration.hashed ? hashNodes([declaration.hashed]) : null,
|
|
404
|
+
dependencyHash: dependencies.hash,
|
|
405
|
+
dependencies: dependencies.files,
|
|
406
|
+
hasUnfollowedDependencies: !dependencies.isComplete,
|
|
399
407
|
suiteId: describes.length > 0 ? this.suiteIds.get(describes[describes.length - 1]) : null,
|
|
400
408
|
suitePath,
|
|
401
409
|
relativeFilePath: this.options.relativeFilePath,
|
package/dist/resolve.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isUtf8 } from 'node:buffer';
|
|
1
2
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { globSync } from 'tinyglobby';
|
|
@@ -8,7 +9,7 @@ function isFile(file) {
|
|
|
8
9
|
}
|
|
9
10
|
/** The file a path refers to, found the way TypeScript and Node do: as is, with an extension, or a folder's index. */
|
|
10
11
|
function findFile(base) {
|
|
11
|
-
const stripped = base.replace(/\.[cm]?js$/, '');
|
|
12
|
+
const stripped = base.replace(/\.(?:[cm]?js|jsx)$/, '');
|
|
12
13
|
const candidates = [
|
|
13
14
|
base,
|
|
14
15
|
...EXTENSIONS.map((extension) => stripped + extension),
|
|
@@ -42,6 +43,16 @@ function readText(file) {
|
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
/** A file's text, with its bytes too when it isn't UTF-8 text, as an image isn't; null when it can't be read. */
|
|
47
|
+
function readContent(file) {
|
|
48
|
+
try {
|
|
49
|
+
const bytes = readFileSync(file);
|
|
50
|
+
return isUtf8(bytes) ? { code: bytes.toString('utf8') } : { code: bytes.toString('utf8'), bytes };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
function readJson(file) {
|
|
46
57
|
const text = isFile(file) ? readText(file) : null;
|
|
47
58
|
return text === null ? null : record(parseJsonc(text));
|
|
@@ -362,17 +373,24 @@ export function moduleResolver(root) {
|
|
|
362
373
|
}
|
|
363
374
|
else {
|
|
364
375
|
const options = optionsFor(directory);
|
|
365
|
-
const
|
|
376
|
+
const targets = options ? aliasFiles(specifier, options) : null;
|
|
377
|
+
const aliased = targets?.find((file) => findFile(file) !== null);
|
|
366
378
|
if (aliased) {
|
|
367
|
-
candidates = aliased;
|
|
379
|
+
candidates = [aliased];
|
|
368
380
|
}
|
|
369
381
|
else {
|
|
370
|
-
//
|
|
382
|
+
// TypeScript goes on to the usual lookup when no file of a matching alias exists. `baseUrl` turns any name
|
|
383
|
+
// into a path, so a name not found there may still be a workspace package.
|
|
371
384
|
const fromBaseUrl = options?.baseUrl ? findFile(path.resolve(options.baseUrl, specifier)) : null;
|
|
372
385
|
const workspace = fromBaseUrl ? null : workspaceSource(specifier, directory, packagesOf);
|
|
373
386
|
if (workspace && 'reason' in workspace)
|
|
374
387
|
return workspace;
|
|
375
388
|
const file = fromBaseUrl ?? workspace?.file;
|
|
389
|
+
// No package name starts with `@/` or `~/`, so these are aliases, as Vite's `resolve.alias` sets them; any
|
|
390
|
+
// other name with no file may still be a package or a built-in module, as TypeScript takes it to be.
|
|
391
|
+
if (!file && /^[@~]\//.test(specifier)) {
|
|
392
|
+
return { reason: targets ? 'could not be read' : 'is a path alias that no tsconfig paths define' };
|
|
393
|
+
}
|
|
376
394
|
if (!file)
|
|
377
395
|
return { reason: 'is not a local file, a path alias or a workspace package' };
|
|
378
396
|
candidates = [file];
|
|
@@ -382,10 +400,12 @@ export function moduleResolver(root) {
|
|
|
382
400
|
if (!found)
|
|
383
401
|
return { reason: 'could not be read' };
|
|
384
402
|
if (!modules.has(found)) {
|
|
385
|
-
const
|
|
386
|
-
modules.set(found,
|
|
387
|
-
|
|
388
|
-
|
|
403
|
+
const content = readContent(found);
|
|
404
|
+
modules.set(found, content && {
|
|
405
|
+
...content,
|
|
406
|
+
relativeFilePath: path.relative(root, found).replaceAll('\\', '/'),
|
|
407
|
+
readModule: reader(found),
|
|
408
|
+
});
|
|
389
409
|
}
|
|
390
410
|
return modules.get(found) ?? { reason: 'could not be read' };
|
|
391
411
|
};
|
package/dist/scan-file.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { RunsWith } from './config-dependencies.ts';
|
|
2
|
+
import { DependencyGraph, type SnapshotFiles } from './dependency-hash.ts';
|
|
1
3
|
import type { ModuleReader } from './model.ts';
|
|
2
4
|
import { type CommentsMode, type FileConfig, type FileRecords } from './records.ts';
|
|
3
5
|
import type { Framework } from './types.ts';
|
|
@@ -13,6 +15,12 @@ export interface FileSettings {
|
|
|
13
15
|
isBenchmarkFile?: boolean | null;
|
|
14
16
|
/** What the config the file uses sets for its tests; null without a config. */
|
|
15
17
|
config?: FileConfig | null;
|
|
18
|
+
/** Shared by the files of one scan, so that code many tests reach is read once. */
|
|
19
|
+
graph?: DependencyGraph;
|
|
20
|
+
/** What the file's config runs with each of its tests: the config, setup files and setup projects. */
|
|
21
|
+
runsWith?: RunsWith;
|
|
22
|
+
/** Reads the snapshot files tests compare against; none when one file's code is scanned. */
|
|
23
|
+
snapshotFiles?: SnapshotFiles;
|
|
16
24
|
}
|
|
17
25
|
export interface FileResult extends FileRecords {
|
|
18
26
|
usesGlobals: boolean;
|
package/dist/scan-file.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DependencyGraph, fileDependencies } from "./dependency-hash.js";
|
|
1
2
|
import { fileDiagnostic, toDiagnostic } from "./diagnostics.js";
|
|
2
3
|
import { extract, noHooks } from "./extract.js";
|
|
3
4
|
import { parseModule } from "./frameworks/bindings.js";
|
|
@@ -39,20 +40,28 @@ export function scanFile(code, relativeFilePath, settings) {
|
|
|
39
40
|
const helper = parseModule(module);
|
|
40
41
|
return helper.error ? null : frame(helper, module.relativeFilePath, settings, module.readModule);
|
|
41
42
|
});
|
|
43
|
+
const isTypeTest = settings.framework === 'vitest' && TYPE_TEST.test(relativeFilePath);
|
|
44
|
+
const unfollowed = [];
|
|
45
|
+
const dependencies = fileDependencies(settings.graph ?? new DependencyGraph(), main, extraction, isTypeTest, (place) => unfollowed.push(place), settings.runsWith, settings.snapshotFiles);
|
|
42
46
|
const records = buildRecords(extraction, parsed, {
|
|
43
47
|
relativeFilePath,
|
|
44
48
|
framework: settings.framework,
|
|
45
49
|
comments: settings.comments,
|
|
46
|
-
isTypeTest
|
|
50
|
+
isTypeTest,
|
|
47
51
|
// Vitest 5's default `benchmark.include`, when no config says.
|
|
48
52
|
isBenchmarkFile: settings.framework === 'vitest' &&
|
|
49
53
|
atLeast(settings.version, 5, 0) &&
|
|
50
54
|
(settings.isBenchmarkFile ?? BENCHMARK.test(relativeFilePath)),
|
|
51
55
|
config: settings.config ?? null,
|
|
56
|
+
dependencies,
|
|
52
57
|
});
|
|
53
58
|
return {
|
|
54
59
|
...records,
|
|
55
|
-
diagnostics: [
|
|
60
|
+
diagnostics: [
|
|
61
|
+
...diagnostics,
|
|
62
|
+
...records.diagnostics,
|
|
63
|
+
...unfollowed.map((place) => toDiagnostic(place.problem, place.relativeFilePath, place.source)),
|
|
64
|
+
],
|
|
56
65
|
usesGlobals: extraction.usesGlobals,
|
|
57
66
|
};
|
|
58
67
|
}
|
package/dist/scan.js
CHANGED
|
@@ -1,13 +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";
|
|
4
6
|
import { ConfigRuns, fileConfig } from "./config-runs.js";
|
|
5
7
|
import { Configs } from "./config.js";
|
|
8
|
+
import { DependencyGraph } from "./dependency-hash.js";
|
|
6
9
|
import { fileDiagnostic } from "./diagnostics.js";
|
|
7
10
|
import { findFiles, normalizePattern, readFailure } from "./files.js";
|
|
8
11
|
import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
|
|
9
12
|
import { createInventory } from "./inventory.js";
|
|
10
|
-
import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, } from "./options.js";
|
|
13
|
+
import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
|
|
11
14
|
import { moduleResolver } from "./resolve.js";
|
|
12
15
|
import { noRecords, scanFile } from "./scan-file.js";
|
|
13
16
|
import { globalsCheck } from "./vitest-config.js";
|
|
@@ -50,6 +53,43 @@ function folderHint(root, patterns, framework) {
|
|
|
50
53
|
}
|
|
51
54
|
/** The JavaScript and TypeScript files the runners read, which the configs choose the test files from. */
|
|
52
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
|
+
}
|
|
53
93
|
/** Why a scan found no test files: what the patterns matched, or whether there is a config to find them from. */
|
|
54
94
|
function noFiles(root, patterns, framework, placements) {
|
|
55
95
|
if (patterns) {
|
|
@@ -94,10 +134,37 @@ export async function scan(options) {
|
|
|
94
134
|
const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
|
|
95
135
|
projectDiagnostics.push(...found.diagnostics);
|
|
96
136
|
const readerFor = moduleResolver(root);
|
|
137
|
+
const graph = new DependencyGraph();
|
|
97
138
|
const configs = new Configs(root, framework, readerFor, rules);
|
|
98
139
|
const runs = new ConfigRuns(configs, framework, rules, patterns === null);
|
|
99
140
|
const placements = new Map(found.files.map((file) => [file.absolutePath, runs.placementOf(file.absolutePath)]));
|
|
100
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) };
|
|
101
168
|
const notRun = [];
|
|
102
169
|
const results = [];
|
|
103
170
|
const globalsFiles = [];
|
|
@@ -121,6 +188,7 @@ export async function scan(options) {
|
|
|
121
188
|
});
|
|
122
189
|
continue;
|
|
123
190
|
}
|
|
191
|
+
const runsWith = configDependencies.of(placement);
|
|
124
192
|
const result = scanFile(code, relativeFilePath, {
|
|
125
193
|
framework,
|
|
126
194
|
version: rules,
|
|
@@ -129,6 +197,9 @@ export async function scan(options) {
|
|
|
129
197
|
root,
|
|
130
198
|
isBenchmarkFile: placement.runs === null ? null : placement.runs === 'benchmark',
|
|
131
199
|
config: fileConfig(placement, framework, rules, absolutePath),
|
|
200
|
+
graph,
|
|
201
|
+
runsWith,
|
|
202
|
+
snapshotFiles: { ...snapshotFiles, moved: runsWith.moved },
|
|
132
203
|
});
|
|
133
204
|
if (result.usesGlobals)
|
|
134
205
|
globalsFiles.push({ absolutePath, relativeFilePath });
|
package/dist/types.d.ts
CHANGED
|
@@ -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';
|
|
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';
|
|
42
42
|
export type MetaValue = string | number | boolean | null | MetaValue[] | {
|
|
43
43
|
[key: string]: MetaValue;
|
|
44
44
|
};
|
|
@@ -250,6 +250,16 @@ export interface TestRecord {
|
|
|
250
250
|
id: Hash;
|
|
251
251
|
/** Hash of the test body, ignoring whitespace and comments; null when the test has no body. */
|
|
252
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;
|
|
253
263
|
/** The nearest describe; null outside any describe. */
|
|
254
264
|
suiteId: Hash | null;
|
|
255
265
|
/** Titles of the describes around the test, outermost first. */
|
package/package.json
CHANGED
package/schema/v1.json
CHANGED
|
@@ -754,6 +754,22 @@
|
|
|
754
754
|
],
|
|
755
755
|
"description": "Hash of the test body, ignoring whitespace and comments; null when the test has no body."
|
|
756
756
|
},
|
|
757
|
+
"dependencyHash": {
|
|
758
|
+
"type": "string",
|
|
759
|
+
"pattern": "^[0-9a-f]{16}$",
|
|
760
|
+
"description": "Hash of everything the test runs in the project: its call, the hooks, options and code of the file and of every describe around it, and the functions, classes, constants and modules these use, followed through imports. Ignores whitespace and comments. Installed packages are not in it."
|
|
761
|
+
},
|
|
762
|
+
"dependencies": {
|
|
763
|
+
"type": "array",
|
|
764
|
+
"items": {
|
|
765
|
+
"type": "string"
|
|
766
|
+
},
|
|
767
|
+
"description": "The files that went into `dependencyHash`, the test's own included, sorted, with `/` separators."
|
|
768
|
+
},
|
|
769
|
+
"hasUnfollowedDependencies": {
|
|
770
|
+
"type": "boolean",
|
|
771
|
+
"description": "Something the test reaches couldn't be followed, so a change there doesn't change `dependencyHash`."
|
|
772
|
+
},
|
|
757
773
|
"suiteId": {
|
|
758
774
|
"anyOf": [
|
|
759
775
|
{
|
|
@@ -1160,6 +1176,9 @@
|
|
|
1160
1176
|
"required": [
|
|
1161
1177
|
"id",
|
|
1162
1178
|
"bodyHash",
|
|
1179
|
+
"dependencyHash",
|
|
1180
|
+
"dependencies",
|
|
1181
|
+
"hasUnfollowedDependencies",
|
|
1163
1182
|
"suiteId",
|
|
1164
1183
|
"suitePath",
|
|
1165
1184
|
"relativeFilePath",
|
|
@@ -1426,7 +1445,8 @@
|
|
|
1426
1445
|
"bench-outside-benchmark-file",
|
|
1427
1446
|
"config-value-unknown",
|
|
1428
1447
|
"file-not-run",
|
|
1429
|
-
"forbidden-only"
|
|
1448
|
+
"forbidden-only",
|
|
1449
|
+
"dependency-not-followed"
|
|
1430
1450
|
]
|
|
1431
1451
|
},
|
|
1432
1452
|
{
|