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 CHANGED
@@ -1,5 +1,108 @@
1
1
  # test-inventory
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - An import that matches a tsconfig `paths` pattern but none of its files, as an installed package does with
8
+ `"paths": { "*": ["./src/types/*"] }`, now goes on to the usual lookup, as TypeScript does. Such imports were reported
9
+ as unreadable, so every test that used a package or a built-in module such as `node:fs` got
10
+ `hasUnfollowedDependencies`. An `@/` or `~/` alias whose files are missing is still reported.
11
+
12
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - A file that only passes names on, as a barrel `index.ts` with `export * from './fees'` or
13
+ `export { OrderTicket } from './order-ticket'` does, is now in the `dependencies` of the tests that import through it,
14
+ so `--related` keeps them when the file changes. Which module a re-export points at is now part of `dependencyHash`, as
15
+ an import's already was, so the hash of a test that imports through a re-export changes once.
16
+
17
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - An imported file that isn't text, such as `import logo from './logo.png'`, is now taken by its bytes in
18
+ `dependencyHash`. It was read as text, where every byte that isn't UTF-8 reads the same, so two different images could
19
+ give the same hash.
20
+
21
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - A `<script>` block in a Vue or Svelte component whose `type` isn't JavaScript, such as
22
+ `<script type="application/ld+json">` in `<svelte:head>`, is no longer read as the component's code. It made the
23
+ component unreadable, so the modules its real script imports were missing from `dependencies`.
24
+
25
+ - [#68](https://github.com/Huldoser/test-inventory/pull/68) [`fa9b97a`](https://github.com/Huldoser/test-inventory/commit/fa9b97affd5d8598dcb91f11c7fc2c5343ec5b87) - `dependencyHash` now follows what a Vue or Svelte single-file component imports, also the components and helpers only
26
+ its template uses, so a change to a child component changes the hash of the tests that mount its parent. A component
27
+ whose script doesn't parse is still taken by its text, and its tests get `hasUnfollowedDependencies`.
28
+
29
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - A file with brackets nested more than 500 deep, such as a generated fixture, now gets a `parse-error` instead of
30
+ crashing the scan. oxc-parser overflows the native stack on such a file, about 5,000 deep, and the process dies with a
31
+ segmentation fault that no error handling can catch, so no inventory was written.
32
+
33
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - `expectCount` now counts `expect.element()`, the assertion of Vitest browser mode, so a browser test that checks the
34
+ page only with `await expect.element(locator).toBeVisible()` no longer looks like a test without assertions.
35
+
36
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - An import written with a `.jsx` extension now finds the `.tsx` file it refers to, as `.js` already found `.ts`, so
37
+ `import { OrderHeader } from './OrderHeader.jsx'` is followed instead of marking the test with
38
+ `hasUnfollowedDependencies`.
39
+
40
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - From Playwright 1.57, the config's `tag` is now read: every test gets it first in its `tags`, and a project's `grep` or
41
+ `grepInvert` matches it, so a project with `grepInvert: /@nightly/` in a config with `tag: '@nightly'` no longer runs
42
+ the tests. When the scan can't work out the `tag`, as for `process.env.CI_ENVIRONMENT_NAME`, it gets a
43
+ `config-value-unknown` warning, and the tests of a project with a `grep` get `projects: null`.
44
+
45
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - From Playwright 1.64, a project with `default: false` is no longer listed in a test's `projects`, as Playwright runs it
46
+ only with `--project`, unless a project that runs lists it in `dependencies` or `teardown`. A `default` the scan can't
47
+ work out gets a `config-value-unknown` warning, and the tests it may run get `projects: null`.
48
+
49
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - Playwright's `defineConfig(base, config)` is now merged as Playwright merges it: a project of `config` merges into the
50
+ project of `base` with the same name instead of replacing all of `base`'s projects, and `expect`, `use` and `build`
51
+ merge key by key. A setup project from a base config is no longer lost, and an `expect.toHaveScreenshot.pathTemplate`
52
+ from the base still marks the tests that compare screenshots with `hasUnfollowedDependencies`.
53
+
54
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - A Vitest `retry` object in a test's or describe's options before Vitest 4.1 now gets an `unresolved-option` warning
55
+ and `retries: null`. Vitest 4.0 and 3.2 never run such a test; the scan used to ignore the object and give the
56
+ config's retry count instead.
57
+
58
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - A Vitest test's own `skip`, `only`, `todo` or `fails` option now replaces the one its tags set, as Vitest does, so
59
+ `test('title', { tags: ['flaky'], skip: false }, fn)` is active instead of skipped, and a conditional `skip` replaces
60
+ the tag's unconditional one.
61
+
62
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - An import that starts with `@/` or `~/`, as a Vite `resolve.alias` allows, now gives the test
63
+ `hasUnfollowedDependencies` and a `dependency-not-followed` warning when no tsconfig `paths` define it. It was taken for
64
+ an installed package, so the aliased file was missing from `dependencies` without a sign, and `--related` left the test
65
+ out when that file changed.
66
+
67
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - Vitest 5 tests follow the option rules of Vitest 5.0.5: the last of `.skip`, `skipIf()` and `runIf()` on a test
68
+ decides its skip, so `test.skip.skipIf(false)` is active, and a `skipIf()` or `runIf()` replaces the skip of a tag or of
69
+ the options a describe passes down. An option set to `undefined`, as in `test.skip('title', { skip: undefined }, fn)`,
70
+ no longer undoes the chain or a tag.
71
+
72
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - In Vitest 5, the projects of a config that another config lists in `projects` are named inside that config's name, as
73
+ Vitest names them: the `unit` project of `apps/web/vitest.config.ts`, named `web`, is `web (unit)`.
74
+
75
+ - [#69](https://github.com/Huldoser/test-inventory/pull/69) [`41ce74d`](https://github.com/Huldoser/test-inventory/commit/41ce74d6dc5fb413ffcfa789442754a445f5572c) - A Vitest project without a `test.name` now has the name Vitest gives it, in `projects` and in `configs`: the `name` in
76
+ the package.json of its folder, else the folder's name, so `projects: ['packages/*']` names its projects
77
+ `@trading/pricing` or `ledger` instead of `""`. An inline project without a name is named after its place among the
78
+ inline projects, as `0`, and in Vitest 5 no longer takes the name of the config it extends.
79
+
80
+ ## 0.6.0
81
+
82
+ ### Minor Changes
83
+
84
+ - [#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
85
+ with its options, the hooks, `test.use()` and options of its file and of every describe around it, the fixtures it,
86
+ those hooks, conditions and `test.use()` values ask for, with the fixtures those ask for, `auto` ones and overrides of
87
+ Playwright's own, through every `extend()` and `mergeTests()`, the file's other top-level code, the config and the
88
+ configs it builds on, setup files, global setup and teardown, the Playwright setup projects the test runs after, and the
89
+ snapshot files and screenshots it compares against, and the functions, classes, constants and modules all of these use,
90
+ followed by scope and through imports, re-exports, barrels, `require()`, `import()` and Vitest's `vi.mock()` with a
91
+ fixed path, with the code each module runs when it loads. A helper that declares the test is in it with the arguments of
92
+ the call that declares it, so two calls give two hashes. Formatting and comments don't change it, nor does moving the
93
+ test. Installed packages are left out, and types too, except for type tests.
94
+
95
+ `dependencies` lists the files that went into the hash. When the test reaches something the scan can't follow, a local
96
+ 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
97
+ declaration file describes, or a setup file the config names in a way the scan can't work out,
98
+ `hasUnfollowedDependencies` is `true` and the new `dependency-not-followed` warning says where, once per place.
99
+
100
+ - [#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
101
+ whose `dependencies` include it, and those that reach code the scan can't follow, which a change may affect unseen.
102
+ `--related -` reads the files from standard input, one per line, so `git diff --name-only origin/main... | npx
103
+ test-inventory --framework playwright --related -` lists the tests a pull request can affect. Paths are taken from the
104
+ current folder, as git writes them, also when `--root` is a folder in the repository.
105
+
3
106
  ## 0.5.0
4
107
 
5
108
  ### 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,8 +11,18 @@ 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;
24
+ /** Whether the runner runs the project without `--project`, as Playwright's `default` decides; null when unknown. */
25
+ runsByDefault: boolean | null;
16
26
  defaults: ProjectDefaults;
17
27
  }
18
28
  /**
@@ -35,6 +45,8 @@ export interface Placement {
35
45
  runs: Run | false | null;
36
46
  /** The projects of the config that run the file, which a Playwright `grep` may narrow per test; null when unknown. */
37
47
  projects: ProjectAbout[] | null;
48
+ /** The tags Playwright's config `tag` gives every test; null when the scan can't work them out. */
49
+ tags: string[] | null;
38
50
  }
39
51
  /** Finds the configs a file is under and decides which one runs it, as the runner started at the root would. */
40
52
  export declare class ConfigRuns {
@@ -46,7 +58,7 @@ export declare class ConfigRuns {
46
58
  private readonly projects;
47
59
  private readonly ignoreRules;
48
60
  private readonly folderStates;
49
- /** The Vitest configs whose projects are being found, to stop at one whose projects name itself. */
61
+ /** The Vitest configs whose projects are being found, with their names, to stop at one whose projects name itself. */
50
62
  private readonly expanding;
51
63
  constructor(configs: Configs, framework: Framework, version: [number, number] | null, findsFiles: boolean);
52
64
  /**
@@ -55,14 +67,36 @@ export declare class ConfigRuns {
55
67
  * nearest config.
56
68
  */
57
69
  placementOf(file: string): Placement;
70
+ private place;
71
+ /**
72
+ * Playwright's config `tag`, from 1.57, which it gives every test before its own tags; null when the scan can't work
73
+ * it out. That is reported where the config writes `tag`; a config whose settings come from code the scan can't
74
+ * follow is reported there already, as its projects can't be worked out either.
75
+ */
76
+ private configTags;
58
77
  /** The projects of a config the output lists; null when the scan can't work them out. */
59
78
  projectsIn(config: ConfigFile): ProjectRecord[] | null;
79
+ /**
80
+ * A config's project, named as the runner names it, with the files it runs; null when there is none, or the scan
81
+ * can't tell which files it runs.
82
+ */
83
+ projectNamed(config: ConfigFile, name: string): {
84
+ about: ProjectAbout;
85
+ runs: (file: string) => unknown;
86
+ } | null;
60
87
  private projectsOf;
61
88
  /** Vitest's `retry`: before 4.1, which reads only a count, an object is one the scan can't work out. */
62
89
  private vitestRetry;
63
90
  /** A project whose files can't be worked out, because of `setting` under `key`: it may run any file. */
64
91
  private unknownProject;
65
92
  private playwrightProjects;
93
+ /**
94
+ * Whether Playwright runs each project without `--project`: from 1.64, not with `default: false`, unless a project it
95
+ * runs lists it in `dependencies` or `teardown`, directly or through others. Null when the scan can't tell.
96
+ */
97
+ private runsByDefault;
98
+ /** The name of a project a Vitest config lists, with a warning when its `test.name` can't be worked out. */
99
+ private listedName;
66
100
  /** A Playwright project: its own settings, else the config's, else Playwright's defaults. */
67
101
  private playwrightProject;
68
102
  /**
@@ -73,17 +107,25 @@ export declare class ConfigRuns {
73
107
  /** A folder's status and the rules inside it, worked out once per project folder however many files it holds. */
74
108
  private folderState;
75
109
  private rulesIn;
110
+ /**
111
+ * The projects of a Vitest config, the one the runner starts with or, in Vitest 5, a config another lists, whose name
112
+ * then goes around the names of its projects.
113
+ */
76
114
  private vitestProjects;
77
- /** A project written in `test.projects`, which inherits the root config as each version of Vitest decides. */
115
+ /**
116
+ * A project written in `test.projects`, which inherits the root config as each version of Vitest decides, named
117
+ * after `index` when it has no name of its own.
118
+ */
78
119
  private vitestEntry;
79
120
  /**
80
121
  * The projects of a config file or folder `test.projects` names: the config's own, also the projects it lists in
81
- * Vitest 5, or a project with Vitest's defaults for a folder without a config.
122
+ * Vitest 5, or a project with Vitest's defaults for a folder without a config. Without a name, a project is named
123
+ * after the package.json in its folder, else the folder.
82
124
  */
83
125
  private vitestPath;
84
126
  /**
85
- * A Vitest project: its files are found from `test.dir`, else `test.root` or `root`, else its folder. With browser
86
- * mode on, each of `browser.instances` is a project of its own, named as Vitest names it.
127
+ * A Vitest project, with the name Vitest gives it: its files are found from `test.dir`, else `test.root` or `root`,
128
+ * else its folder. With browser mode on, each of `browser.instances` is a project of its own, named as Vitest names it.
87
129
  */
88
130
  private vitestProject;
89
131
  /** The files a Vitest project runs and its settings; `ownTags`, when set, in place of the tags in `settings`. */