test-inventory 0.5.0 → 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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # test-inventory
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#61](https://github.com/Huldoser/test-inventory/pull/61) [`926352f`](https://github.com/Huldoser/test-inventory/commit/926352fd67caf8d26e17e346b9e916400e28a4e3) - Each test now has a `dependencyHash` of everything it runs in the project, beside the unchanged `bodyHash`: its own call
8
+ with its options, the hooks, `test.use()` and options of its file and of every describe around it, the fixtures it,
9
+ those hooks, conditions and `test.use()` values ask for, with the fixtures those ask for, `auto` ones and overrides of
10
+ Playwright's own, through every `extend()` and `mergeTests()`, the file's other top-level code, the config and the
11
+ configs it builds on, setup files, global setup and teardown, the Playwright setup projects the test runs after, and the
12
+ snapshot files and screenshots it compares against, and the functions, classes, constants and modules all of these use,
13
+ followed by scope and through imports, re-exports, barrels, `require()`, `import()` and Vitest's `vi.mock()` with a
14
+ fixed path, with the code each module runs when it loads. A helper that declares the test is in it with the arguments of
15
+ the call that declares it, so two calls give two hashes. Formatting and comments don't change it, nor does moving the
16
+ test. Installed packages are left out, and types too, except for type tests.
17
+
18
+ `dependencies` lists the files that went into the hash. When the test reaches something the scan can't follow, a local
19
+ file that is missing or doesn't parse, an import with a path worked out at run time or a pattern, a value only a
20
+ declaration file describes, or a setup file the config names in a way the scan can't work out,
21
+ `hasUnfollowedDependencies` is `true` and the new `dependency-not-followed` warning says where, once per place.
22
+
23
+ - [#65](https://github.com/Huldoser/test-inventory/pull/65) [`31a9bee`](https://github.com/Huldoser/test-inventory/commit/31a9beef6f2912a03c4ae70ccd797b9b40cfe3e8) - `--related <file>` and the `related` filter of `filterInventory()` keep the tests a change to a file can affect: those
24
+ whose `dependencies` include it, and those that reach code the scan can't follow, which a change may affect unseen.
25
+ `--related -` reads the files from standard input, one per line, so `git diff --name-only origin/main... | npx
26
+ test-inventory --framework playwright --related -` lists the tests a pull request can affect. Paths are taken from the
27
+ current folder, as git writes them, also when `--root` is a folder in the repository.
28
+
3
29
  ## 0.5.0
4
30
 
5
31
  ### Minor Changes
package/dist/bin.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { run } from "./cli.js";
3
3
  process.exitCode = await run(process.argv.slice(2), {
4
4
  stdout: process.stdout,
5
+ stdin: process.stdin,
5
6
  stderr: process.stderr,
6
7
  cwd: process.cwd(),
7
8
  });
package/dist/cli.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import type { Writable } from 'node:stream';
1
+ import type { Readable, Writable } from 'node:stream';
2
2
  export interface Io {
3
3
  stdout: Writable;
4
4
  stderr: Writable;
5
5
  cwd: string;
6
+ /** Read for `--related -`. */
7
+ stdin?: Readable;
6
8
  }
7
- export declare const HELP = "Usage: test-inventory [glob...] --framework <playwright|vitest> [options]\n\nLists the tests in the matching files as JSON, read from the source code without running them. Without globs, the\ntest files are those the Playwright or Vitest configs in the root and the folders below it run.\n\nOptions:\n --framework <name> playwright or vitest (required)\n --root <dir> project root; paths in the output are relative to it (default: current directory)\n --ignore <glob> leave out matching files; can be repeated (node_modules is always left out)\n --framework-version <x.y.z> apply the rules of this framework version instead of the one in the project\n --comments <mode> comments above tests: all, non-active or none (default: non-active)\n --state <state> keep tests in this state, such as skip or fixme; can be repeated\n --tag <tag> keep tests with this tag, such as @smoke; can be repeated\n --grep <regex> keep tests whose full name matches, ignoring case\n --only keep tests that use .only, or are inside a describe that does\n --without-reason keep tests that don't run and give no reason\n --fields <keys> write only these keys of each test, separated by commas; id is always kept\n --format <format> json or jsonl, one record per line (default: json)\n -o, --output <file> write to a file instead of standard output\n --pretty indent the JSON\n -h, --help show this help\n -v, --version show the version\n\nExit codes: 0 when the scan finishes, even with diagnostics; 2 for invalid arguments, an output that cannot be\nwritten, or a crash. 1 is reserved for a future release.\n\nExamples:\n test-inventory \"tests/**/*.spec.ts\" --framework playwright --output inventory.json\n test-inventory \"tests/**/*.spec.ts\" --framework playwright --state skip --fields fullName,reason\n";
9
+ export declare const HELP = "Usage: test-inventory [glob...] --framework <playwright|vitest> [options]\n\nLists the tests in the matching files as JSON, read from the source code without running them. Without globs, the\ntest files are those the Playwright or Vitest configs in the root and the folders below it run.\n\nOptions:\n --framework <name> playwright or vitest (required)\n --root <dir> project root; paths in the output are relative to it (default: current directory)\n --ignore <glob> leave out matching files; can be repeated (node_modules is always left out)\n --framework-version <x.y.z> apply the rules of this framework version instead of the one in the project\n --comments <mode> comments above tests: all, non-active or none (default: non-active)\n --state <state> keep tests in this state, such as skip or fixme; can be repeated\n --tag <tag> keep tests with this tag, such as @smoke; can be repeated\n --grep <regex> keep tests whose full name matches, ignoring case\n --only keep tests that use .only, or are inside a describe that does\n --without-reason keep tests that don't run and give no reason\n --related <file> keep tests a change to this file can affect; can be repeated; - reads files, one per\n line, from standard input, as git diff --name-only writes them\n --fields <keys> write only these keys of each test, separated by commas; id is always kept\n --format <format> json or jsonl, one record per line (default: json)\n -o, --output <file> write to a file instead of standard output\n --pretty indent the JSON\n -h, --help show this help\n -v, --version show the version\n\nExit codes: 0 when the scan finishes, even with diagnostics; 2 for invalid arguments, an output that cannot be\nwritten, or a crash. 1 is reserved for a future release.\n\nExamples:\n test-inventory \"tests/**/*.spec.ts\" --framework playwright --output inventory.json\n test-inventory \"tests/**/*.spec.ts\" --framework playwright --state skip --fields fullName,reason\n";
8
10
  /** Runs the command line with the given arguments and streams. Resolves to the exit code. */
9
11
  export declare function run(argv: string[], io: Io): Promise<number>;
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { parseArgs } from 'node:util';
5
5
  import { readFailure } from "./files.js";
6
6
  import { checkFields, filterInventory, matcher, pickTestFields } from "./filter.js";
7
7
  import { writeInventory, writeInventoryLines } from "./json.js";
8
- import { OptionsError } from "./options.js";
8
+ import { OptionsError, toPosixPath } from "./options.js";
9
9
  import { scan } from "./scan.js";
10
10
  import { toolVersion } from "./version.js";
11
11
  export const HELP = `Usage: test-inventory [glob...] --framework <playwright|vitest> [options]
@@ -24,6 +24,8 @@ Options:
24
24
  --grep <regex> keep tests whose full name matches, ignoring case
25
25
  --only keep tests that use .only, or are inside a describe that does
26
26
  --without-reason keep tests that don't run and give no reason
27
+ --related <file> keep tests a change to this file can affect; can be repeated; - reads files, one per
28
+ line, from standard input, as git diff --name-only writes them
27
29
  --fields <keys> write only these keys of each test, separated by commas; id is always kept
28
30
  --format <format> json or jsonl, one record per line (default: json)
29
31
  -o, --output <file> write to a file instead of standard output
@@ -49,6 +51,7 @@ const FLAGS = {
49
51
  'filter.states': '--state',
50
52
  'filter.tags': '--tag',
51
53
  'filter.grep': '--grep',
54
+ 'filter.related': '--related',
52
55
  fields: '--fields',
53
56
  };
54
57
  const FORMATS = ['json', 'jsonl'];
@@ -83,6 +86,13 @@ async function writeOutput(io, file, write) {
83
86
  throw error;
84
87
  }
85
88
  }
89
+ /** All of a stream's text, as `git diff --name-only` writes to it. */
90
+ async function text(stream) {
91
+ const chunks = [];
92
+ for await (const chunk of stream)
93
+ chunks.push(Buffer.from(chunk));
94
+ return Buffer.concat(chunks).toString('utf8');
95
+ }
86
96
  /** ESLint's exit codes. 1 is reserved for a future release. */
87
97
  const FINISHED = 0;
88
98
  const FAILED = 2;
@@ -109,6 +119,7 @@ export async function run(argv, io) {
109
119
  grep: { type: 'string' },
110
120
  only: { type: 'boolean' },
111
121
  'without-reason': { type: 'boolean' },
122
+ related: { type: 'string', multiple: true },
112
123
  fields: { type: 'string' },
113
124
  format: { type: 'string' },
114
125
  output: { type: 'string', short: 'o' },
@@ -144,6 +155,16 @@ export async function run(argv, io) {
144
155
  only: values.only,
145
156
  withoutReason: values['without-reason'],
146
157
  };
158
+ const root = path.resolve(io.cwd, values.root ?? '.');
159
+ if (values.related) {
160
+ const read = values.related.includes('-') && io.stdin ? await text(io.stdin) : '';
161
+ const named = [
162
+ ...values.related.filter((file) => file !== '-'),
163
+ ...read.split(/\r?\n/).filter((line) => line.trim()),
164
+ ];
165
+ // Paths from the current folder, as git writes them there, to the root the output's paths start from.
166
+ filter.related = named.map((file) => toPosixPath(path.relative(root, path.resolve(io.cwd, file.trim()))));
167
+ }
147
168
  const fields = values.fields?.split(',').map((field) => field.trim());
148
169
  let inventory;
149
170
  let result;
@@ -155,7 +176,7 @@ export async function run(argv, io) {
155
176
  inventory = await scan({
156
177
  patterns: positionals.length > 0 ? positionals : undefined,
157
178
  framework: values.framework,
158
- root: path.resolve(io.cwd, values.root ?? '.'),
179
+ root,
159
180
  ignore: values.ignore ?? [],
160
181
  frameworkVersion: values['framework-version'],
161
182
  comments: values.comments,
@@ -0,0 +1,62 @@
1
+ import type { ConfigRuns, Placement } from './config-runs.ts';
2
+ import type { ModuleReader, ModuleSource, Span } from './model.ts';
3
+ import { SourceText } from './source.ts';
4
+ import type { Framework } from './types.ts';
5
+ /**
6
+ * Something in a config the scan can't follow: where it is written, or the config and the path it names, which the
7
+ * warning points at.
8
+ */
9
+ export type ConfigGap = {
10
+ message: string;
11
+ /** Not reported, as the config reader reports it already: the tests are only marked. */
12
+ isQuiet: boolean;
13
+ } & ({
14
+ relativeFilePath: string;
15
+ source: SourceText;
16
+ span: Span;
17
+ } | {
18
+ module: ModuleSource;
19
+ text: string | null;
20
+ });
21
+ /** What the config a file uses runs with each of its tests: modules to take whole, and what can't be followed. */
22
+ export interface RunsWith {
23
+ /** The config the file uses, which builds on others through imports. */
24
+ config: ModuleSource | null;
25
+ /** Its setup files, the configs that list its project, and the files of setup projects. */
26
+ modules: ModuleSource[];
27
+ gaps: ConfigGap[];
28
+ /** Where the config puts snapshots elsewhere than the runner's default, which the scan doesn't follow. */
29
+ moved: ConfigGap | null;
30
+ }
31
+ /**
32
+ * Finds what each file's config makes its tests run besides the file: the config itself, which brings in the configs it
33
+ * builds on through imports, the setup files and global setup and teardown of each project that runs the file, the
34
+ * config that lists a Vitest project, whose global setup runs too, and for Playwright the files of the setup projects
35
+ * the file's projects depend on. Read once per config and set of projects.
36
+ */
37
+ export declare class ConfigDependencies {
38
+ private readonly framework;
39
+ private readonly runs;
40
+ private readonly readerFor;
41
+ /** The files setup projects may run. */
42
+ private readonly candidates;
43
+ private readonly found;
44
+ private readonly ids;
45
+ constructor(framework: Framework, runs: ConfigRuns, readerFor: (fromFile: string) => ModuleReader, candidates: readonly string[]);
46
+ of(placement: Placement): RunsWith;
47
+ private idOf;
48
+ private read;
49
+ /** Whether Playwright's `snapshotDir` is the testDir of every project that runs the file, its default. */
50
+ private isDefault;
51
+ /** The first setting of the config or a project running the file that puts snapshots elsewhere; null for none. */
52
+ private moved;
53
+ /** The files of the setup projects the file's projects depend on, and the ones those depend on. */
54
+ private setupProjects;
55
+ /**
56
+ * A setup file a config names, found from `root` as the runners find it; null for a name that isn't a file there,
57
+ * which is a package, whose code is left out as installed code is.
58
+ */
59
+ private setupFile;
60
+ /** A file the scan reads code from, by its absolute path, as the modules a test imports are read. */
61
+ private module;
62
+ }
@@ -0,0 +1,231 @@
1
+ import path from 'node:path';
2
+ import { settingAt } from "./config.js";
3
+ import { SourceText } from "./source.js";
4
+ const NOTHING = { config: null, modules: [], gaps: [], moved: null };
5
+ /** The keys that put snapshots elsewhere than the runner's default, by framework. */
6
+ const SNAPSHOT_KEYS = {
7
+ playwright: [
8
+ ['snapshotPathTemplate'],
9
+ ['snapshotDir'],
10
+ ['expect', 'toHaveScreenshot', 'pathTemplate'],
11
+ ['expect', 'toMatchAriaSnapshot', 'pathTemplate'],
12
+ ],
13
+ vitest: [
14
+ ['test', 'resolveSnapshotPath'],
15
+ ['test', 'snapshotEnvironment'],
16
+ ['test', 'browser', 'expect', 'toMatchScreenshot', 'resolveScreenshotPath'],
17
+ ['test', 'browser', 'expect', 'toMatchScreenshot', 'screenshotDirectory'],
18
+ ],
19
+ };
20
+ /** The keys of a config that name setup files, by framework; Vitest's are a project's own. */
21
+ const SETUP_KEYS = {
22
+ playwright: [['globalSetup'], ['globalTeardown']],
23
+ vitest: [
24
+ ['test', 'setupFiles'],
25
+ ['test', 'globalSetup'],
26
+ ],
27
+ };
28
+ const UNKNOWN_PROJECTS = "The projects that run the file can't be worked out, so dependencyHash doesn't cover their setup.";
29
+ /**
30
+ * Finds what each file's config makes its tests run besides the file: the config itself, which brings in the configs it
31
+ * builds on through imports, the setup files and global setup and teardown of each project that runs the file, the
32
+ * config that lists a Vitest project, whose global setup runs too, and for Playwright the files of the setup projects
33
+ * the file's projects depend on. Read once per config and set of projects.
34
+ */
35
+ export class ConfigDependencies {
36
+ framework;
37
+ runs;
38
+ readerFor;
39
+ /** The files setup projects may run. */
40
+ candidates;
41
+ found = new Map();
42
+ ids = new Map();
43
+ constructor(framework, runs, readerFor, candidates) {
44
+ this.framework = framework;
45
+ this.runs = runs;
46
+ this.readerFor = readerFor;
47
+ this.candidates = candidates;
48
+ }
49
+ of(placement) {
50
+ const { config } = placement;
51
+ if (config === null)
52
+ return NOTHING;
53
+ // Projects without a name are told apart by identity, as two may have different setup.
54
+ const ids = (placement.projects ?? []).map((project) => this.idOf(project));
55
+ const key = `${config.file}\0${placement.projects === null}\0${ids.join('\0')}`;
56
+ let found = this.found.get(key);
57
+ if (!found) {
58
+ found = this.read(config, placement);
59
+ this.found.set(key, found);
60
+ }
61
+ return found;
62
+ }
63
+ idOf(project) {
64
+ let id = this.ids.get(project);
65
+ if (id === undefined) {
66
+ id = this.ids.size;
67
+ this.ids.set(project, id);
68
+ }
69
+ return id;
70
+ }
71
+ read(config, placement) {
72
+ const own = this.module(config.file);
73
+ if (!own) {
74
+ // The config reader reports a config it can't read.
75
+ const message = "The config can't be read, so dependencyHash doesn't cover it.";
76
+ const span = { start: 0, end: 0 };
77
+ const gap = { relativeFilePath: config.record.relativeFilePath, source: new SourceText(''), span, message };
78
+ return { config: null, modules: [], gaps: [{ ...gap, isQuiet: true }], moved: null };
79
+ }
80
+ const modules = new Set();
81
+ const gaps = [];
82
+ const places = new Set();
83
+ /** The setup files a key of `settings` names, found from `root`, as written in the config `module`. */
84
+ const setup = (module, settings, root, keys) => {
85
+ const key = keys.join('.');
86
+ const found = written(settings, keys);
87
+ for (const item of found ? listOf(found.setting) : []) {
88
+ if (item.kind === 'value' && typeof item.value === 'string') {
89
+ const read = this.setupFile(root, item.value);
90
+ if (read && !('reason' in read))
91
+ modules.add(read);
92
+ else if (read) {
93
+ const message = `${key} names '${item.value}', which ${read.reason}, so dependencyHash doesn't cover it.`;
94
+ gaps.push({ module, text: item.value, message, isQuiet: false });
95
+ }
96
+ }
97
+ else if (item.kind === 'unknown') {
98
+ const message = `${key} can't be worked out, so dependencyHash doesn't cover the files it names.`;
99
+ // A Vitest 3 workspace file, which the scan doesn't read, has no place in the config.
100
+ if (!item.where)
101
+ gaps.push({ module, text: null, message, isQuiet: true });
102
+ // One gap per place, as both setup keys may come from the same spread.
103
+ else if (!places.has(item.where.node)) {
104
+ places.add(item.where.node);
105
+ const { frame, node } = item.where;
106
+ const at = { relativeFilePath: frame.relativeFilePath, source: frame.parsed.source, span: node };
107
+ gaps.push({ ...at, message, isQuiet: found.isInherited });
108
+ }
109
+ }
110
+ }
111
+ };
112
+ const { projects } = placement;
113
+ if (projects === null)
114
+ gaps.push({ module: own, text: null, message: UNKNOWN_PROJECTS, isQuiet: true });
115
+ const folder = path.dirname(config.file);
116
+ if (this.framework === 'playwright') {
117
+ for (const keys of SETUP_KEYS.playwright)
118
+ setup(own, config.settings, folder, keys);
119
+ this.setupProjects(config, own, projects ?? [], modules, gaps);
120
+ }
121
+ else {
122
+ // A file no project runs still runs with the config's own setup when the runner is given it.
123
+ const running = projects?.length
124
+ ? projects
125
+ : [{ settings: placement.settings, root: folder, container: null }];
126
+ // Vitest runs the global setup of the config it starts with for every project, extending it or not.
127
+ setup(own, config.settings, folder, ['test', 'globalSetup']);
128
+ for (const project of running) {
129
+ // A file several projects run may be run by one from another config, which the file runs with too; the config
130
+ // reader has read that config, so it can be read.
131
+ const written = 'config' in project ? this.module(project.config.file) : own;
132
+ modules.add(written);
133
+ for (const keys of SETUP_KEYS.vitest)
134
+ setup(written, project.settings, project.root, keys);
135
+ const { container } = project;
136
+ const outer = container && this.module(container.file);
137
+ if (container && outer) {
138
+ modules.add(outer);
139
+ setup(outer, container.settings, path.dirname(container.file), ['test', 'globalSetup']);
140
+ }
141
+ }
142
+ }
143
+ return { config: own, modules: [...modules], gaps, moved: this.moved(own, config, placement) };
144
+ }
145
+ /** Whether Playwright's `snapshotDir` is the testDir of every project that runs the file, its default. */
146
+ isDefault(config, placement, keys, found) {
147
+ if (keys[0] !== 'snapshotDir' || found.kind !== 'value' || typeof found.value !== 'string')
148
+ return false;
149
+ const folder = path.resolve(path.dirname(config.file), found.value);
150
+ const projects = placement.projects ?? [];
151
+ return projects.length > 0 && projects.every((project) => project.testDir === folder);
152
+ }
153
+ /** The first setting of the config or a project running the file that puts snapshots elsewhere; null for none. */
154
+ moved(own, config, placement) {
155
+ const settings = [config.settings, ...(placement.projects ?? []).map((project) => project.settings)];
156
+ for (const keys of SNAPSHOT_KEYS[this.framework]) {
157
+ for (const setting of settings) {
158
+ const found = settingAt(setting, keys);
159
+ if (found === null || this.isDefault(config, placement, keys, found))
160
+ continue;
161
+ const message = `${keys.join('.')} puts snapshots where the scan doesn't look, so dependencyHash doesn't cover them.`;
162
+ if (found.kind === 'unknown' && found.where) {
163
+ const { frame, node } = found.where;
164
+ return {
165
+ relativeFilePath: frame.relativeFilePath,
166
+ source: frame.parsed.source,
167
+ span: node,
168
+ message,
169
+ isQuiet: false,
170
+ };
171
+ }
172
+ const text = found.kind === 'value' && typeof found.value === 'string' ? found.value : null;
173
+ return { module: own, text, message, isQuiet: false };
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+ /** The files of the setup projects the file's projects depend on, and the ones those depend on. */
179
+ setupProjects(config, own, projects, modules, gaps) {
180
+ const names = new Set();
181
+ const pending = projects.flatMap((project) => project.dependencies);
182
+ for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
183
+ if (names.has(name))
184
+ continue;
185
+ names.add(name);
186
+ const project = this.runs.projectNamed(config, name);
187
+ if (!project) {
188
+ // The config reader reports a project it can't work out.
189
+ const message = `The files of the setup project '${name}' can't be worked out, so dependencyHash doesn't cover them.`;
190
+ gaps.push({ module: own, text: null, message, isQuiet: true });
191
+ continue;
192
+ }
193
+ pending.push(...project.about.dependencies);
194
+ const files = this.candidates.filter((file) => project.runs(file)).map((file) => this.module(file));
195
+ for (const file of files.filter((item) => item !== null))
196
+ modules.add(file);
197
+ }
198
+ }
199
+ /**
200
+ * A setup file a config names, found from `root` as the runners find it; null for a name that isn't a file there,
201
+ * which is a package, whose code is left out as installed code is.
202
+ */
203
+ setupFile(root, file) {
204
+ // `path.join(__dirname, 'setup.ts')` gives an absolute path.
205
+ const relative = path.isAbsolute(file) ? path.relative(root, file).replaceAll('\\', '/') : file;
206
+ const isPath = /^\.\.?[\\/]/.test(relative);
207
+ const read = this.readerFor(path.join(root, 'config'))(isPath ? relative : `./${relative}`);
208
+ return 'reason' in read && !isPath ? null : read;
209
+ }
210
+ /** A file the scan reads code from, by its absolute path, as the modules a test imports are read. */
211
+ module(file) {
212
+ const read = this.readerFor(file)(`./${path.basename(file)}`);
213
+ return 'reason' in read ? null : read;
214
+ }
215
+ }
216
+ /**
217
+ * The setting under a path of keys, and whether it comes from an object around it that the scan can't work out
218
+ * rather than from the key itself; null when the config doesn't set it.
219
+ */
220
+ function written(setting, keys) {
221
+ const found = settingAt(setting, keys);
222
+ if (found?.kind !== 'unknown')
223
+ return found && { setting: found, isInherited: false };
224
+ // An unknown setting is set by the object around the key when the key itself isn't written there.
225
+ const around = settingAt(setting, keys.slice(0, -1));
226
+ const isWritten = around.kind === 'object' && around.keys.get(keys.at(-1)) === found;
227
+ return { setting: found, isInherited: !isWritten };
228
+ }
229
+ function listOf(setting) {
230
+ return setting.kind === 'list' ? setting.items : [setting];
231
+ }
@@ -11,6 +11,14 @@ export interface ProjectAbout {
11
11
  name: string;
12
12
  testDir: string;
13
13
  dependencies: string[];
14
+ /** The project's settings, which its setup files are read from. */
15
+ settings: Setting;
16
+ /** The folder its setup files are found from: Vitest's root, or the config's folder. */
17
+ root: string;
18
+ /** For a Vitest project from a config another config lists, the outermost one, whose global setup runs too. */
19
+ container: ConfigFile | null;
20
+ /** The config the project is written in. */
21
+ config: ConfigFile;
14
22
  grep: ((title: string) => boolean) | null;
15
23
  grepRoot: string;
16
24
  defaults: ProjectDefaults;
@@ -57,6 +65,14 @@ export declare class ConfigRuns {
57
65
  placementOf(file: string): Placement;
58
66
  /** The projects of a config the output lists; null when the scan can't work them out. */
59
67
  projectsIn(config: ConfigFile): ProjectRecord[] | null;
68
+ /**
69
+ * A config's project, named as the runner names it, with the files it runs; null when there is none, or the scan
70
+ * can't tell which files it runs.
71
+ */
72
+ projectNamed(config: ConfigFile, name: string): {
73
+ about: ProjectAbout;
74
+ runs: (file: string) => unknown;
75
+ } | null;
60
76
  private projectsOf;
61
77
  /** Vitest's `retry`: before 4.1, which reads only a count, an object is one the scan can't work out. */
62
78
  private vitestRetry;
@@ -151,6 +151,14 @@ export class ConfigRuns {
151
151
  dependencies: about.dependencies,
152
152
  }));
153
153
  }
154
+ /**
155
+ * A config's project, named as the runner names it, with the files it runs; null when there is none, or the scan
156
+ * can't tell which files it runs.
157
+ */
158
+ projectNamed(config, name) {
159
+ const project = this.projectsOf(config).find((item) => item.runs && item.about.name === name);
160
+ return project ? { about: project.about, runs: project.runs } : null;
161
+ }
154
162
  projectsOf(config) {
155
163
  let projects = this.projects.get(config);
156
164
  if (!projects) {
@@ -169,6 +177,10 @@ export class ConfigRuns {
169
177
  name: '',
170
178
  testDir: path.dirname(config.file),
171
179
  dependencies: [],
180
+ settings: setting,
181
+ root: path.dirname(config.file),
182
+ container: null,
183
+ config,
172
184
  grep: null,
173
185
  grepRoot: '',
174
186
  defaults: NO_DEFAULTS,
@@ -245,6 +257,11 @@ export class ConfigRuns {
245
257
  name: name?.setting.kind === 'value' && typeof name.setting.value === 'string' ? name.setting.value : '',
246
258
  testDir: root,
247
259
  dependencies: strings(dependencies),
260
+ // A project's own keys, as `snapshotPathTemplate`; the config's apply to every project, and are read there.
261
+ settings: project,
262
+ root: folder,
263
+ container: null,
264
+ config,
248
265
  grep: grep || grepInvert
249
266
  ? (title) => (grep === null || isGrepped(title)) && !(grepInvert && isInverted(title))
250
267
  : null,
@@ -397,9 +414,11 @@ export class ConfigRuns {
397
414
  if (!nested)
398
415
  return this.vitestProject(config, emptySettings(), target, null);
399
416
  const isContainer = (this.version === null || this.version[0] >= 5) && !this.expanding.has(nested);
400
- return isContainer && settingAt(nested.settings, ['test', 'projects'])
417
+ const projects = isContainer && settingAt(nested.settings, ['test', 'projects'])
401
418
  ? this.vitestProjects(nested)
402
419
  : this.vitestProject(nested, nested.settings, path.dirname(nested.file), null);
420
+ // The outermost config lists the others, so it is set last.
421
+ return projects.map((project) => ({ ...project, about: { ...project.about, container: config } }));
403
422
  }
404
423
  /**
405
424
  * A Vitest project: its files are found from `test.dir`, else `test.root` or `root`, else its folder. With browser
@@ -487,6 +506,10 @@ export class ConfigRuns {
487
506
  name: label?.kind === 'value' && typeof label.value === 'string' ? label.value : '',
488
507
  testDir: base,
489
508
  dependencies: [],
509
+ settings,
510
+ root,
511
+ container: null,
512
+ config,
490
513
  grep: null,
491
514
  grepRoot: base,
492
515
  defaults: {