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.
@@ -0,0 +1,1533 @@
1
+ import path from 'node:path';
2
+ import { visitorKeys, } from 'oxc-parser';
3
+ import { childNodes, isFunctionNode, propertyKey, unwrap } from "./ast.js";
4
+ import { exportName, parseModule } from "./frameworks/bindings.js";
5
+ import { destructuredNames, problem, takesWhole } from "./frameworks/common.js";
6
+ import { shortHash, hashNodes } from "./hash.js";
7
+ import { parseFile } from "./parse.js";
8
+ import { snippet } from "./source.js";
9
+ import { bindingNames, isScopeNode, Values } from "./values.js";
10
+ /** Statements that only declare something, unless code in them runs while they are evaluated. */
11
+ const DECLARATIONS = new Set([
12
+ 'ImportDeclaration',
13
+ 'FunctionDeclaration',
14
+ 'ClassDeclaration',
15
+ 'VariableDeclaration',
16
+ 'TSTypeAliasDeclaration',
17
+ 'TSInterfaceDeclaration',
18
+ 'TSEnumDeclaration',
19
+ 'TSModuleDeclaration',
20
+ 'TSDeclareFunction',
21
+ 'TSImportEqualsDeclaration',
22
+ 'ExportAllDeclaration',
23
+ 'ExportNamedDeclaration',
24
+ 'ExportDefaultDeclaration',
25
+ 'EmptyStatement',
26
+ ]);
27
+ /** Expressions that run code other than the declaration's own: calls, constructors, decorators, static blocks. */
28
+ const RUNS = new Set([
29
+ 'CallExpression',
30
+ 'NewExpression',
31
+ 'TaggedTemplateExpression',
32
+ 'AwaitExpression',
33
+ 'ImportExpression',
34
+ 'Decorator',
35
+ 'StaticBlock',
36
+ ]);
37
+ /** The nodes an import binds a name with. */
38
+ const IMPORT_BINDINGS = new Set([
39
+ 'ImportSpecifier',
40
+ 'ImportDefaultSpecifier',
41
+ 'ImportNamespaceSpecifier',
42
+ 'TSImportEqualsDeclaration',
43
+ ]);
44
+ /** Keys that hold types, which don't run. */
45
+ const TYPE_KEYS = new Set([
46
+ 'typeAnnotation',
47
+ 'returnType',
48
+ 'typeParameters',
49
+ 'typeArguments',
50
+ 'superTypeArguments',
51
+ 'implements',
52
+ ]);
53
+ const TYPES = new Set(['TSInterfaceDeclaration', 'TSTypeAliasDeclaration']);
54
+ /** Vitest calls that load or mock a module by its path. */
55
+ const VI_MODULE_CALLS = new Set(['mock', 'doMock', 'unmock', 'doUnmock', 'importActual', 'importMock']);
56
+ /** Reasons the resolver gives for an installed package, whose code the hash leaves out on purpose. */
57
+ const PACKAGE_REASONS = new Set([
58
+ 'is an installed package, which is not followed',
59
+ 'is not a local file, a path alias or a workspace package',
60
+ ]);
61
+ /** Modules whose `test` object fixtures are defined on. */
62
+ const FRAMEWORK = /^(?:@playwright\/test|playwright\/test|@playwright\/experimental-ct-[\w-]+|vitest)$/;
63
+ /** Calls on a test object whose functions run with the test's fixtures: tests and hooks. */
64
+ const FIXTURE_CALLS = new Set([
65
+ ...['test', 'it', 'only', 'todo', 'concurrent', 'sequential'],
66
+ ...['beforeEach', 'afterEach', 'beforeAll', 'afterAll', 'aroundEach', 'aroundAll'],
67
+ ]);
68
+ /** Conditions, which take a value or a function of the fixtures, as in `test.skip(({ account }) => ...)`. */
69
+ const CONDITION_CALLS = new Set(['skip', 'fixme', 'fail', 'slow']);
70
+ /** Vitest's globals, which a file with `globals: true` calls without importing them. */
71
+ const GLOBAL_TESTS = new Set([
72
+ ...['test', 'it', 'describe', 'suite', 'bench'],
73
+ ...['beforeEach', 'afterEach', 'beforeAll', 'afterAll', 'aroundEach', 'aroundAll'],
74
+ ]);
75
+ /** Matchers that compare against a file beside the test: Vitest's snapshots and file snapshots, Playwright's too. */
76
+ const SNAPSHOT_MATCHERS = new Set([
77
+ 'toMatchSnapshot',
78
+ 'matchSnapshot',
79
+ 'toMatchScreenshot',
80
+ 'toThrowErrorMatchingSnapshot',
81
+ 'toMatchFileSnapshot',
82
+ 'toHaveScreenshot',
83
+ 'toMatchAriaSnapshot',
84
+ ]);
85
+ /**
86
+ * The name or path a snapshot matcher is given: a string, a list of path parts as Playwright takes, or an options
87
+ * object's `name`; null for anything else, as a name worked out at run time.
88
+ */
89
+ function snapshotName(argument) {
90
+ if (argument.type === 'Literal' && typeof argument.value === 'string')
91
+ return { name: argument.value, isParts: false };
92
+ if (argument.type === 'ArrayExpression') {
93
+ const parts = argument.elements.map((element) => (element ? snapshotName(element) : null));
94
+ return parts.every((part) => part !== null)
95
+ ? { name: parts.map((part) => part.name).join('/'), isParts: true }
96
+ : null;
97
+ }
98
+ if (argument.type === 'ObjectExpression') {
99
+ const property = argument.properties.find((item) => item.type === 'Property' && propertyKey(item) === 'name');
100
+ return property?.type === 'Property' ? snapshotName(property.value) : null;
101
+ }
102
+ return null;
103
+ }
104
+ /** How a matcher is called from another, as a custom matcher calls Vitest's own. */
105
+ const CALLS = new Set(['call', 'apply']);
106
+ /** The characters Playwright 1.64 turns into `-` in a snapshot name before its extension. */
107
+ const UNSAFE_NAME = /[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g;
108
+ /** Calls that set fixtures for a file or a describe: Playwright's `test.use()`, Vitest's `override()` and `scoped()`. */
109
+ const USE_CALLS = new Set(['use', 'override', 'scoped']);
110
+ /** The exports of a framework module that are test objects, or combine them. */
111
+ const TEST_EXPORTS = new Set(['test', 'it', 'mergeTests']);
112
+ /** How many constants and imports deep a fixture's value is followed. */
113
+ const MAX_VALUE_DEPTH = 16;
114
+ /** Playwright's own fixtures and options (1.64), which a fixtures file can override for every test. */
115
+ const BUILT_INS = new Set([
116
+ ...['browser', 'browserName', 'channel', 'connectOptions', 'context', 'defaultBrowserType', 'headless'],
117
+ ...['launchOptions', 'mount', 'page', 'playwright', 'request', 'reuseContext', 'router', 'screenshot', 'trace'],
118
+ 'video',
119
+ ...['acceptDownloads', 'actionTimeout', 'baseURL', 'bypassCSP', 'clientCertificates', 'colorScheme'],
120
+ ...['contextOptions', 'contrast', 'deviceScaleFactor', 'extraHTTPHeaders', 'forcedColors', 'geolocation'],
121
+ ...['hasTouch', 'httpCredentials', 'ignoreHTTPSErrors', 'isMobile', 'javaScriptEnabled', 'locale'],
122
+ ...['navigationTimeout', 'offline', 'permissions', 'proxy', 'reducedMotion', 'screen', 'serviceWorkers'],
123
+ ...['storageState', 'testIdAttribute', 'timezoneId', 'userAgent', 'viewport'],
124
+ ]);
125
+ /**
126
+ * Whether an expression is a value that can't be a function, such as a literal, an object or an array: a fixture with
127
+ * such a value is never called, so it asks for no other fixture.
128
+ */
129
+ function isPlainValue(node) {
130
+ return PLAIN_VALUES.has(node.type);
131
+ }
132
+ const PLAIN_VALUES = new Set([
133
+ 'Literal',
134
+ 'TemplateLiteral',
135
+ 'ArrayExpression',
136
+ 'ObjectExpression',
137
+ 'UnaryExpression',
138
+ 'BinaryExpression',
139
+ ]);
140
+ /** Whether a declarator at the top of a module, as an exported one is, is a `const`. */
141
+ function isConst(module, declarator) {
142
+ return module.parsed.program.body.some((statement) => {
143
+ const declaration = declared(statement);
144
+ return (declaration.type === 'VariableDeclaration' &&
145
+ declaration.kind === 'const' &&
146
+ declaration.declarations.includes(declarator));
147
+ });
148
+ }
149
+ /** A property written as `name: value` or `name() {}`, whose name is known. */
150
+ function isNamedProperty(property) {
151
+ return property.type === 'Property' && !property.computed && propertyKey(property) !== null;
152
+ }
153
+ /** `auto: true` in a fixture's options. */
154
+ function isAutoOption(property) {
155
+ return (property.type === 'Property' &&
156
+ propertyKey(property) === 'auto' &&
157
+ property.value.type === 'Literal' &&
158
+ property.value.value === true);
159
+ }
160
+ const CODE_FILE = /\.[cm]?[jt]sx?$/;
161
+ /** A Vue or Svelte single-file component: markup with its code in `<script>` blocks. */
162
+ const COMPONENT_FILE = /\.(?:vue|svelte)$/;
163
+ /**
164
+ * A comment, which may hold a `<script>` that isn't one, or a script block with its attributes and code. An attribute
165
+ * may hold `>`, as Svelte's `generics="T extends Record<string, number>"` does. Both end where HTML ends them: a
166
+ * comment also at `--!>`, a script at `</script` with anything before its `>`.
167
+ */
168
+ const COMPONENT_BLOCK = /<!--[\s\S]*?--!?>|<script\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/script(?:[\s/][^>]*)?>/gi;
169
+ /**
170
+ * A file's bytes with line endings as `\n`, as git may check out a text file with either; one git takes for binary, with
171
+ * a NUL byte in its first 8,000, stays as it is.
172
+ */
173
+ function withLineFeeds(bytes) {
174
+ if (bytes.subarray(0, 8000).includes(0))
175
+ return bytes;
176
+ // Latin-1 maps each byte to one character and back, so only the line endings change.
177
+ return Buffer.from(Buffer.from(bytes).toString('latin1').replace(/\r\n?/g, '\n'), 'latin1');
178
+ }
179
+ /** Whether a script block holds code: one with a `type` other than a JavaScript one, as JSON-LD, holds data. */
180
+ function isScriptCode(attributes) {
181
+ const type = /(?:^|\s)type\s*=\s*["']?([^"'\s>]*)/i.exec(attributes)?.[1] ?? '';
182
+ return /^(?:module|(?:text|application)\/(?:java|ecma)script)?$/i.test(type);
183
+ }
184
+ /**
185
+ * A component's script blocks as one module, the rest of the file blanked so that offsets stay those of the file, and
186
+ * a file name for the language they are in: `lang="ts"` is TypeScript, no `lang` JavaScript.
187
+ */
188
+ function componentScript(code) {
189
+ // A byte order mark stays, for the parser to leave out as it does from any file.
190
+ const blank = (text) => text.replace(/[^\r\n\uFEFF]/g, ' ');
191
+ let script = '';
192
+ let end = 0;
193
+ let lang = 'js';
194
+ for (const match of code.matchAll(COMPONENT_BLOCK)) {
195
+ const [whole, attributes, body] = match;
196
+ if (whole.startsWith('<!--') || !isScriptCode(attributes))
197
+ continue;
198
+ const start = match.index + '<script'.length + attributes.length + 1;
199
+ script += blank(code.slice(end, start)) + body;
200
+ end = start + body.length;
201
+ lang = /(?:^|\s)lang\s*=\s*["']?(\w+)/.exec(attributes)?.[1] ?? lang;
202
+ }
203
+ return { fileName: `component.${lang}`, code: script + blank(code.slice(end)) };
204
+ }
205
+ const DECLARATION_FILE = /\.d\.[cm]?ts$/;
206
+ /**
207
+ * What an import being looked up stands for while the lookup leads back to it, through re-exports: nothing found
208
+ * there, so that an `export *` goes on to the next module, as the module system does.
209
+ */
210
+ const IN_PROGRESS = [];
211
+ const COMPUTED = "The module this loads is worked out at run time, so dependencyHash doesn't cover it.";
212
+ function isLocalSpecifier(specifier) {
213
+ return /^\.\.?(?:\/|$)/.test(specifier) || specifier.startsWith('#');
214
+ }
215
+ function contains(outer, inner) {
216
+ return outer.start <= inner.start && inner.end <= outer.end;
217
+ }
218
+ /** The path to a node from `from`, searched node by node, for a node outside its parent's span. */
219
+ function pathTo(from, target) {
220
+ if (from === target)
221
+ return [];
222
+ for (const child of childNodes(from)) {
223
+ const path = pathTo(child, target);
224
+ if (path)
225
+ return [child, ...path];
226
+ }
227
+ return null;
228
+ }
229
+ /**
230
+ * The scopes around a node, outermost first, found by going down from the program to it. Spans lead the way, except
231
+ * where a node is outside its parent's span, as a decorator written before `export` is.
232
+ */
233
+ function scopesAt(program, target) {
234
+ const scopes = [];
235
+ let node = program;
236
+ while (node !== target) {
237
+ if (isScopeNode(node))
238
+ scopes.push(node);
239
+ const next = childNodes(node).find((child) => contains(child, target));
240
+ if (!next) {
241
+ const rest = pathTo(node, target);
242
+ scopes.push(...rest.slice(0, -1).filter(isScopeNode));
243
+ break;
244
+ }
245
+ node = next;
246
+ }
247
+ return scopes;
248
+ }
249
+ /** Where a statement's own declaration is, for a statement that exports one. */
250
+ function declared(statement) {
251
+ return statement.type === 'ExportNamedDeclaration' && statement.declaration ? statement.declaration : statement;
252
+ }
253
+ /** Whether evaluating a statement runs code, leaving out the bodies of the functions it declares, which run later. */
254
+ function runsOnLoad(node) {
255
+ if (RUNS.has(node.type))
256
+ return true;
257
+ // An instance field's value is worked out when the class is built, not when it is defined.
258
+ if ((node.type === 'PropertyDefinition' || node.type === 'AccessorProperty') && !node.static) {
259
+ return node.decorators.length > 0 || (node.computed && runsOnLoad(node.key));
260
+ }
261
+ if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression')
262
+ return false;
263
+ if (node.type === 'ArrowFunctionExpression')
264
+ return false;
265
+ return childNodes(node).some(runsOnLoad);
266
+ }
267
+ /**
268
+ * Whether a top-level or describe statement runs when the file or describe does. An import that names nothing is there
269
+ * for what its module does when loaded, so it is code of the file, also for a package.
270
+ */
271
+ /** An import that loads its module when the file runs: not a type-only one. */
272
+ function isRuntimeImport(statement) {
273
+ return ((statement.type === 'ImportDeclaration' || statement.type === 'TSImportEqualsDeclaration') &&
274
+ statement.importKind !== 'type');
275
+ }
276
+ function isLoadCode(statement) {
277
+ // `import '@testing-library/jest-dom'` runs a module for what it does; a named import is followed through its names.
278
+ if (statement.type === 'ImportDeclaration')
279
+ return statement.importKind !== 'type' && statement.specifiers.length === 0;
280
+ return !DECLARATIONS.has(statement.type) || runsOnLoad(statement);
281
+ }
282
+ /**
283
+ * Follows what each test runs, across the files of one scan: its call, the code around it, and every declaration
284
+ * that code uses, in its own file and through imports. Each piece is read once per scan, however many tests reach it.
285
+ */
286
+ export class DependencyGraph {
287
+ modules = new WeakMap();
288
+ units = [new WeakMap(), new WeakMap()];
289
+ imports = new WeakMap();
290
+ snapshotFiles = new Map();
291
+ calledTests = new WeakMap();
292
+ testObjects = [
293
+ new WeakMap(),
294
+ new WeakMap(),
295
+ ];
296
+ /** Places already reported, so that one the scan reaches from many files is reported once. */
297
+ reported = new Set();
298
+ /** The module of a file the scan reads code from, given as its parsed file. */
299
+ module(parsed, relativeFilePath, values, readModule) {
300
+ let module = this.modules.get(parsed);
301
+ if (!module) {
302
+ module = this.newModule(relativeFilePath, parsed, null, values, readModule);
303
+ this.modules.set(parsed, module);
304
+ }
305
+ return module;
306
+ }
307
+ /**
308
+ * Code a test or a describe runs, written in `module`. `cut` lists the nodes left out, such as the tests in a
309
+ * describe.
310
+ */
311
+ unit(module, nodes, cut, types) {
312
+ const program = module.parsed.program;
313
+ return this.newUnit(module, nodes.map((node) => ({ node, scopes: scopesAt(program, node) })), new Map(cut.map((span) => [span.start, span.end])), types);
314
+ }
315
+ /** The code a file runs when it is loaded, leaving out the tests and describes at `cut`. */
316
+ loadUnit(module, types, cut = [], allImports = false) {
317
+ const index = Number(types);
318
+ const cached = module.load[index];
319
+ if (cached)
320
+ return cached;
321
+ const program = module.parsed.program;
322
+ const unit = this.newUnit(module, [], new Map(cut.map((span) => [span.start, span.end])), types);
323
+ // Read once the walk reaches it, as telling a test object apart may need this module's load code in turn.
324
+ // A test file's every import is its own code; another module's are followed through the names a test uses.
325
+ const isCode = (statement) => isLoadCode(statement) || (allImports && isRuntimeImport(statement));
326
+ unit.prepare = () => {
327
+ for (const statement of program.body.filter(isCode)) {
328
+ const declaration = declared(statement);
329
+ if (declaration.type !== 'VariableDeclaration') {
330
+ unit.roots.push({ node: statement, scopes: [program] });
331
+ continue;
332
+ }
333
+ // A test object's fixtures run only for the tests that ask for them.
334
+ for (const declarator of declaration.declarations) {
335
+ const testObject = this.testObjectUnit(module, declarator, types);
336
+ if (testObject)
337
+ unit.with.push(testObject);
338
+ else if (runsOnLoad(declarator))
339
+ unit.roots.push({ node: declarator, scopes: [program] });
340
+ }
341
+ }
342
+ };
343
+ module.load[index] = unit;
344
+ return unit;
345
+ }
346
+ /** The units a set of roots reaches, leaving out those `skip` reached, with their hashes by file. */
347
+ reach(roots, skip = []) {
348
+ const units = new Set();
349
+ const pending = [...roots];
350
+ for (let unit = pending.pop(); unit; unit = pending.pop()) {
351
+ if (units.has(unit) || skip.some((reached) => reached.units.has(unit)))
352
+ continue;
353
+ units.add(unit);
354
+ unit.prepare?.();
355
+ unit.prepare = null;
356
+ unit.edges ??= this.edgesOf(unit);
357
+ pending.push(...unit.with, ...unit.edges);
358
+ }
359
+ const byFile = new Map();
360
+ const places = [];
361
+ const fixtures = [];
362
+ let asks = [];
363
+ const snapshots = [];
364
+ for (const unit of units) {
365
+ snapshots.push(...unit.snapshots);
366
+ if (unit.place)
367
+ places.push(unit.place);
368
+ if (unit.fixtures)
369
+ fixtures.push(unit.fixtures);
370
+ if (unit.asks === null)
371
+ asks = null;
372
+ else if (asks !== null && unit.asks.length > 0)
373
+ asks.push(...unit.asks);
374
+ const hash = this.hashOf(unit);
375
+ if (hash === null)
376
+ continue;
377
+ const hashes = byFile.get(unit.module.relativeFilePath);
378
+ if (hashes)
379
+ hashes.push(hash);
380
+ else
381
+ byFile.set(unit.module.relativeFilePath, [hash]);
382
+ }
383
+ // Sorted by hash, so that moving code around in a file doesn't change it; each part keeps its own order.
384
+ for (const hashes of byFile.values())
385
+ hashes.sort();
386
+ return { units, byFile, places, fixtures, asks, snapshots };
387
+ }
388
+ /**
389
+ * Everything a test's roots reach on top of `base`, the code its file runs when loaded, which all its tests share,
390
+ * with the fixtures it needs of each test object reached: the hash, the files, and whether all of it could be
391
+ * followed.
392
+ */
393
+ dependencies(roots, bases, needs, snapshots, report) {
394
+ const reached = [...bases, this.reach(roots, bases)];
395
+ // A fixture can ask for one an object it extends defines, or use another test object, so names are shared by
396
+ // every test object reached, and each name and object is read once.
397
+ const names = new Set();
398
+ const queue = [];
399
+ const objects = [];
400
+ const taken = new Set();
401
+ let fresh = [];
402
+ let all = false;
403
+ const need = (name) => {
404
+ if (names.has(name))
405
+ return;
406
+ names.add(name);
407
+ queue.push(name);
408
+ };
409
+ const takeAll = () => {
410
+ all = true;
411
+ for (const item of objects)
412
+ for (const name of item.byName.keys())
413
+ need(name);
414
+ };
415
+ const resolve = (item, name) => {
416
+ for (const fixture of item.byName.get(name) ?? []) {
417
+ if (taken.has(fixture.unit))
418
+ continue;
419
+ taken.add(fixture.unit);
420
+ fresh.push(fixture.unit);
421
+ if (fixture.uses === null) {
422
+ if (!all)
423
+ takeAll();
424
+ }
425
+ else {
426
+ for (const used of fixture.uses)
427
+ need(used);
428
+ }
429
+ }
430
+ };
431
+ const found = (reach) => {
432
+ if (reach.asks === null && !all)
433
+ takeAll();
434
+ for (const name of reach.asks ?? [])
435
+ need(name);
436
+ // Each walk skips what earlier ones reached, so an object turns up once.
437
+ for (const item of reach.fixtures) {
438
+ objects.push(item);
439
+ for (const name of all ? item.byName.keys() : item.auto)
440
+ need(name);
441
+ for (const name of names)
442
+ resolve(item, name);
443
+ }
444
+ };
445
+ for (const name of needs.names)
446
+ need(name);
447
+ for (const reach of reached)
448
+ found(reach);
449
+ if (needs.all)
450
+ takeAll();
451
+ while (queue.length > 0 || fresh.length > 0) {
452
+ for (let name = queue.pop(); name !== undefined; name = queue.pop()) {
453
+ for (const item of objects)
454
+ resolve(item, name);
455
+ }
456
+ if (fresh.length > 0) {
457
+ const reach = this.reach(fresh, reached);
458
+ fresh = [];
459
+ reached.push(reach);
460
+ found(reach);
461
+ }
462
+ }
463
+ // The snapshot files the matchers it reaches compare against, which have no code to reach in turn.
464
+ const uses = reached.flatMap((item) => item.snapshots);
465
+ if (uses.length > 0)
466
+ reached.push(this.reach(snapshots(uses), reached));
467
+ const places = reached.flatMap((item) => item.places);
468
+ const start = (place) => place.problem.node.start;
469
+ // Compared by code unit, as `files` is, so that the order doesn't depend on the locale.
470
+ const path = (a, b) => a.relativeFilePath < b.relativeFilePath ? -1 : a.relativeFilePath > b.relativeFilePath ? 1 : 0;
471
+ places.sort((a, b) => path(a, b) || start(a) - start(b));
472
+ for (const place of places) {
473
+ const key = `${place.relativeFilePath}\0${start(place)}\0${place.problem.message}`;
474
+ if (place.isQuiet || this.reported.has(key))
475
+ continue;
476
+ this.reported.add(key);
477
+ report(place);
478
+ }
479
+ const files = [...new Set(reached.flatMap((item) => [...item.byFile.keys()]))].sort();
480
+ const parts = files.map((file) => {
481
+ return [file, reached.flatMap((item) => item.byFile.get(file) ?? []).sort()];
482
+ });
483
+ return { hash: shortHash(JSON.stringify(parts)), files, isComplete: places.length === 0 };
484
+ }
485
+ newModule(relativeFilePath, parsed, text, values, readModule) {
486
+ return {
487
+ relativeFilePath,
488
+ parsed,
489
+ text,
490
+ values,
491
+ readModule,
492
+ load: [null, null],
493
+ whole: [null, null],
494
+ imports: new Map(),
495
+ reads: new Map(),
496
+ };
497
+ }
498
+ newUnit(module, roots, cut, types) {
499
+ return {
500
+ module,
501
+ roots,
502
+ cut,
503
+ types,
504
+ with: [],
505
+ edges: null,
506
+ place: null,
507
+ fixtures: null,
508
+ prepare: null,
509
+ asks: [],
510
+ snapshots: [],
511
+ hash: undefined,
512
+ };
513
+ }
514
+ hashOf(unit) {
515
+ if (unit.hash === undefined) {
516
+ if (unit.module.text !== null) {
517
+ // Counted once, through the unit of all of it, however many of a component's units a test reaches.
518
+ unit.hash = unit.module.whole.includes(unit) ? shortHash(unit.module.text) : null;
519
+ }
520
+ else {
521
+ const cut = [...unit.cut].map(([start, end]) => ({ start, end }));
522
+ unit.hash = hashNodes(unit.roots.map((root) => root.node), cut);
523
+ }
524
+ }
525
+ return unit.hash;
526
+ }
527
+ /** The module a file imports, read and parsed once per scan. */
528
+ moduleOf(source) {
529
+ let module = this.modules.get(source);
530
+ if (!module) {
531
+ if (CODE_FILE.test(source.relativeFilePath)) {
532
+ const parsed = parseModule(source);
533
+ module =
534
+ this.modules.get(parsed) ??
535
+ this.newModule(source.relativeFilePath, parsed, null, new Values(), source.readModule);
536
+ this.modules.set(parsed, module);
537
+ }
538
+ else {
539
+ const text = source.bytes ? withLineFeeds(source.bytes) : source.code.replace(/\r\n?/g, '\n');
540
+ // A component's markup and styles shape what it renders, so it is taken by its text, and its scripts are
541
+ // followed for the modules they import.
542
+ const script = COMPONENT_FILE.test(source.relativeFilePath) ? componentScript(source.code) : null;
543
+ const parsed = script && parseFile(script.fileName, script.code);
544
+ module = this.newModule(source.relativeFilePath, parsed, text, new Values(), parsed && source.readModule);
545
+ }
546
+ this.modules.set(source, module);
547
+ }
548
+ return module;
549
+ }
550
+ textUnit(module) {
551
+ const cached = module.whole[0];
552
+ if (cached)
553
+ return cached;
554
+ const unit = this.newUnit(module, [], new Map(), false);
555
+ unit.edges = new Set();
556
+ module.whole = [unit, unit];
557
+ module.load = [unit, unit];
558
+ return unit;
559
+ }
560
+ wholeUnit(module, types) {
561
+ const index = Number(types);
562
+ const cached = module.whole[index];
563
+ if (cached)
564
+ return cached;
565
+ const program = module.parsed.program;
566
+ const statements = program.body.filter((statement) => statement.type !== 'ImportDeclaration');
567
+ const unit = this.newUnit(module, statements.map((node) => ({ node, scopes: [program] })), new Map(), types);
568
+ unit.with.push(this.loadUnit(module, types));
569
+ module.whole[index] = unit;
570
+ return unit;
571
+ }
572
+ /** The unit of a declaration: a declarator, function, class, enum, namespace or `import x = require()`. */
573
+ declarationUnit(module, declaration, types, roots) {
574
+ const cache = this.units[Number(types)];
575
+ // A function's parameters alone are a unit of their own, kept apart from the whole function.
576
+ const key = roots ?? declaration;
577
+ let unit = cache.get(key);
578
+ if (!unit) {
579
+ const program = module.parsed.program;
580
+ const scopes = scopesAt(program, declaration);
581
+ const nodes = roots ?? [declaration];
582
+ const inner = roots ? [...scopes, declaration] : scopes;
583
+ unit = this.newUnit(module, nodes.map((node) => ({ node, scopes: inner })), new Map(), types);
584
+ // Code in a module runs only once the module is loaded, which runs its top-level code.
585
+ unit.with.push(this.loadUnit(module, types));
586
+ cache.set(key, unit);
587
+ }
588
+ return unit;
589
+ }
590
+ /** Whether a name is bound to a test object, once per declaration, as a file may call a helper thousands of times. */
591
+ isTestObject(module, name, declaration, scopes, types) {
592
+ let found = this.calledTests.get(declaration);
593
+ if (found === undefined) {
594
+ found = this.frameworkOf(module, name, scopes, types) !== null;
595
+ this.calledTests.set(declaration, found);
596
+ }
597
+ return found;
598
+ }
599
+ /**
600
+ * A file a test compares against, by its bytes: a snapshot or a screenshot, read once per scan however many tests
601
+ * compare against it; null when there is none yet.
602
+ */
603
+ snapshotUnit(relativeFilePath, read) {
604
+ let unit = this.snapshotFiles.get(relativeFilePath);
605
+ if (unit === undefined) {
606
+ const bytes = read();
607
+ if (bytes === null || bytes === 'unreadable')
608
+ unit = bytes;
609
+ else {
610
+ unit = this.newUnit(this.newModule(relativeFilePath, null, null, new Values(), null), [], new Map(), false);
611
+ unit.edges = new Set();
612
+ unit.hash = shortHash(bytes);
613
+ }
614
+ this.snapshotFiles.set(relativeFilePath, unit);
615
+ }
616
+ return unit;
617
+ }
618
+ /** A place in a scanned file that marks its tests without a warning, as for what one file's code can't show. */
619
+ quietUnit(module, message) {
620
+ return this.placeUnit(module, { start: 0, end: 0 }, message, true);
621
+ }
622
+ /** An import of a name a test uses: the module it names and how, as `import { round } from 'lodash'`. */
623
+ importUnit(module, statement, specifier) {
624
+ let unit = this.imports.get(specifier);
625
+ if (!unit) {
626
+ const roots = [...new Set([statement.source, specifier])].map((node) => ({ node, scopes: [] }));
627
+ unit = this.newUnit(module, roots, new Map(), false);
628
+ unit.edges = new Set();
629
+ this.imports.set(specifier, unit);
630
+ }
631
+ return unit;
632
+ }
633
+ /** A place in a module the scan can't follow, as a unit that marks the tests that reach it. */
634
+ placeUnit(module, at, message, isQuiet = false) {
635
+ const { source } = module.parsed;
636
+ const unit = this.newUnit(module, [], new Map(), false);
637
+ unit.edges = new Set();
638
+ const text = snippet(source.slice(at.start, at.end)) || null;
639
+ unit.place = {
640
+ relativeFilePath: module.relativeFilePath,
641
+ source,
642
+ problem: problem('dependency-not-followed', at, message, text),
643
+ isQuiet,
644
+ };
645
+ return unit;
646
+ }
647
+ /**
648
+ * A module the config runs each test with, taken whole: the config itself, a setup file, or a file of a setup
649
+ * project.
650
+ */
651
+ runUnit(source, isConfig) {
652
+ const module = this.moduleOf(source);
653
+ if (module.parsed === null)
654
+ return this.textUnit(module);
655
+ const { error } = module.parsed;
656
+ if (error) {
657
+ // A config that doesn't parse is reported when the scan reads it.
658
+ const message = "The file could not be parsed, so dependencyHash doesn't cover it.";
659
+ return this.placeUnit(module, error, message, isConfig);
660
+ }
661
+ return this.wholeUnit(module, false);
662
+ }
663
+ /** Something in a config the scan can't follow, as a unit that marks the tests whose config it is. */
664
+ gapUnit(gap) {
665
+ if ('module' in gap) {
666
+ const module = this.moduleOf(gap.module);
667
+ // The path a setup key names, where the config writes it.
668
+ const at = gap.text === null ? null : literalIn(module.parsed.program, gap.text);
669
+ return this.placeUnit(module, at ?? { start: 0, end: 0 }, gap.message, gap.isQuiet);
670
+ }
671
+ const module = this.newModule(gap.relativeFilePath, null, null, new Values(), null);
672
+ const unit = this.newUnit(module, [], new Map(), false);
673
+ unit.edges = new Set();
674
+ const text = snippet(gap.source.slice(gap.span.start, gap.span.end));
675
+ unit.place = {
676
+ relativeFilePath: gap.relativeFilePath,
677
+ source: gap.source,
678
+ problem: problem('dependency-not-followed', gap.span, gap.message, text),
679
+ isQuiet: gap.isQuiet,
680
+ };
681
+ return unit;
682
+ }
683
+ /**
684
+ * What a module's import of `name` from `specifier` stands for, `at` being the statement in `from` that imports it:
685
+ * the declaration it exports, `*` for all of it, or `''` for the code it runs when loaded. Installed packages give
686
+ * nothing; a local module that can't be read gives a place.
687
+ */
688
+ imported(from, specifier, name, types, at) {
689
+ const key = `${Number(types)}\0${specifier}\0${name}\0${at.start}`;
690
+ let units = from.imports.get(key);
691
+ if (!units) {
692
+ from.imports.set(key, IN_PROGRESS);
693
+ units = this.readImport(from, specifier, name, types, at);
694
+ // Within a cycle, the answer depends on where the search started, so it isn't kept.
695
+ if (units === IN_PROGRESS)
696
+ from.imports.delete(key);
697
+ else
698
+ from.imports.set(key, units);
699
+ }
700
+ return units;
701
+ }
702
+ /** The module a specifier names, from where `from` is; null when no module is read, as for one file's code. */
703
+ read(from, specifier) {
704
+ if (!from.readModule)
705
+ return null;
706
+ let read = from.reads.get(specifier);
707
+ if (!read) {
708
+ // A query, as in Vite's `./script.ts?raw`, says how to load the file, not which file.
709
+ read = from.readModule(specifier.replace(/\?.*$/s, ''));
710
+ from.reads.set(specifier, read);
711
+ }
712
+ return read;
713
+ }
714
+ readImport(from, specifier, name, types, at) {
715
+ const read = this.read(from, specifier);
716
+ if (read === null) {
717
+ return isLocalSpecifier(specifier) ? [this.placeUnit(from, at, `'${specifier}' is not read.`, true)] : [];
718
+ }
719
+ if ('reason' in read) {
720
+ if (PACKAGE_REASONS.has(read.reason))
721
+ return [];
722
+ return [this.placeUnit(from, at, `'${specifier}' ${read.reason}, so dependencyHash doesn't cover it.`)];
723
+ }
724
+ const module = this.moduleOf(read);
725
+ if (module.parsed === null)
726
+ return [this.textUnit(module)];
727
+ if (DECLARATION_FILE.test(module.relativeFilePath) && !types) {
728
+ // An import only types use is dropped when the code is compiled; a value the test uses runs code from elsewhere.
729
+ if (name === '')
730
+ return [];
731
+ return [
732
+ this.placeUnit(from, at, `'${specifier}' leads to a declaration file, so dependencyHash doesn't cover the code that runs.`),
733
+ ];
734
+ }
735
+ if (module.parsed.error) {
736
+ const place = this.placeUnit(from, at, `'${specifier}' could not be parsed, so dependencyHash doesn't cover it.`);
737
+ return module.text === null ? [place] : [this.textUnit(module), place];
738
+ }
739
+ // A component's markup may use anything its scripts declare or import, its types included.
740
+ if (module.text !== null)
741
+ return [this.wholeUnit(module, false)];
742
+ return name === '' ? [this.loadUnit(module, types)] : this.exported(module, name, types, new Set());
743
+ }
744
+ /**
745
+ * The units of what a module exports under a name: its own export first, then one an `export *` passes on, as the
746
+ * module system picks them. `*` is the whole module. A name the scan can't find, as with CommonJS exports, also
747
+ * takes the whole module, so that nothing it may run is left out.
748
+ */
749
+ exported(module, name, types, searched) {
750
+ if (name === '*' || searched.has(module))
751
+ return [this.wholeUnit(module, types)];
752
+ searched.add(module);
753
+ const program = module.parsed.program;
754
+ const local = (localName) => {
755
+ const declaration = module.values.declarationOf(localName, [program]) ?? otherDeclaration(localName, [program], types);
756
+ // A global the module re-exports, such as `structuredClone`, has no code in the project.
757
+ if (declaration === null)
758
+ return [];
759
+ return this.toDeclaration(module, declaration, localName, null, types);
760
+ };
761
+ for (const statement of program.body) {
762
+ if (statement.type === 'ExportDefaultDeclaration' && name === 'default') {
763
+ const declaration = statement.declaration;
764
+ return declaration.type === 'Identifier'
765
+ ? local(declaration.name)
766
+ : [this.declarationUnit(module, declaration, types)];
767
+ }
768
+ if (statement.type === 'ExportAllDeclaration' && statement.exported && exportName(statement.exported) === name) {
769
+ return this.passedOn(module, statement, statement.source, statement.exported, '*', types);
770
+ }
771
+ if (statement.type !== 'ExportNamedDeclaration')
772
+ continue;
773
+ if (statement.exportKind === 'type' && !types)
774
+ continue;
775
+ const declaration = statement.declaration;
776
+ if (declaration) {
777
+ if (declaration.type === 'VariableDeclaration') {
778
+ const declarator = declaration.declarations.find((item) => bindingNames(item.id).includes(name));
779
+ if (declarator)
780
+ return this.toDeclaration(module, declarator, name, null, types);
781
+ }
782
+ else if ('id' in declaration && declaration.id?.type === 'Identifier' && declaration.id.name === name) {
783
+ // `export import Fees = require('./fees')` stands for the other module.
784
+ return this.toDeclaration(module, declaration, name, null, types);
785
+ }
786
+ continue;
787
+ }
788
+ const specifier = statement.specifiers.find((item) => exportName(item.exported) === name);
789
+ if (!specifier)
790
+ continue;
791
+ const localName = exportName(specifier.local);
792
+ return statement.source
793
+ ? this.passedOn(module, statement, statement.source, specifier, localName, types)
794
+ : local(localName);
795
+ }
796
+ // `export *` passes on every name but `default`, and only when the module doesn't export the name itself.
797
+ if (name !== 'default') {
798
+ for (const statement of program.body) {
799
+ if (statement.type !== 'ExportAllDeclaration' || statement.exported)
800
+ continue;
801
+ const found = this.reexported(module, statement.source.value, name, types, searched);
802
+ if (found)
803
+ return [...found, this.importUnit(module, statement, statement.source)];
804
+ }
805
+ }
806
+ return [this.wholeUnit(module, types)];
807
+ }
808
+ /**
809
+ * A name a module passes on from another, as `export { quote } from './nasdaq'` does, with the re-export itself: which
810
+ * module the name comes from is code too, so the barrel is among the files a change to which affects the test.
811
+ */
812
+ passedOn(module, statement, source, specifier, name, types) {
813
+ const units = this.imported(module, source.value, name, types, statement);
814
+ return units === IN_PROGRESS ? units : [...units, this.importUnit(module, { source }, specifier)];
815
+ }
816
+ /** What `export * from source` gives for a name; null when the module behind it doesn't export it. */
817
+ reexported(from, source, name, types, searched) {
818
+ const read = this.read(from, source);
819
+ if (!read || 'reason' in read)
820
+ return null;
821
+ const module = this.moduleOf(read);
822
+ if (module.parsed === null || module.parsed.error)
823
+ return null;
824
+ const found = this.exported(module, name, types, searched);
825
+ // A module that doesn't export the name falls back to the whole of it, which doesn't count as found here.
826
+ const isWhole = found.length === 1 && found[0] === module.whole[Number(types)];
827
+ return isWhole || found === IN_PROGRESS ? null : found;
828
+ }
829
+ /**
830
+ * The units a name stands for where it is declared: the declaration, the export of the module it's imported from, or
831
+ * for a parameter only the parameters, as the call that passes their values is followed on its own.
832
+ */
833
+ toDeclaration(module, declaration, name, member, types) {
834
+ switch (declaration.type) {
835
+ case 'ImportSpecifier':
836
+ case 'ImportDefaultSpecifier':
837
+ case 'ImportNamespaceSpecifier': {
838
+ const statement = this.importOf(module, declaration);
839
+ const imported = declaration.type === 'ImportSpecifier'
840
+ ? exportName(declaration.imported)
841
+ : declaration.type === 'ImportDefaultSpecifier'
842
+ ? 'default'
843
+ : (member ?? '*');
844
+ // Which module a name comes from is code too, also a package that isn't followed.
845
+ return [
846
+ ...this.imported(module, statement.source.value, imported, types, statement),
847
+ this.importUnit(module, statement, declaration),
848
+ ];
849
+ }
850
+ case 'TSImportEqualsDeclaration': {
851
+ const reference = declaration.moduleReference;
852
+ if (reference.type !== 'TSExternalModuleReference')
853
+ return [this.declarationUnit(module, declaration, types)];
854
+ return this.imported(module, reference.expression.value, member ?? '*', types, declaration);
855
+ }
856
+ case 'FunctionDeclaration':
857
+ case 'FunctionExpression':
858
+ case 'ArrowFunctionExpression':
859
+ if (declaration.params.some((parameter) => bindingNames(parameter).includes(name))) {
860
+ return [this.declarationUnit(module, declaration, types, declaration.params)];
861
+ }
862
+ break;
863
+ case 'VariableDeclarator': {
864
+ const testObject = this.testObjectUnit(module, declaration, types);
865
+ if (testObject)
866
+ return [testObject];
867
+ break;
868
+ }
869
+ }
870
+ return [this.declarationUnit(module, declaration, types)];
871
+ }
872
+ /**
873
+ * A test object built with `extend()` on the framework's own: its code without the fixtures its `extend()` calls
874
+ * define, which each test takes as it needs them. Null for any other value.
875
+ */
876
+ testObjectUnit(module, declarator, types) {
877
+ const cache = this.testObjects[Number(types)];
878
+ if (!cache.has(declarator)) {
879
+ // A test object built on itself, through imports, isn't one.
880
+ cache.set(declarator, null);
881
+ cache.set(declarator, this.readTestObject(module, declarator, types));
882
+ }
883
+ return cache.get(declarator) ?? null;
884
+ }
885
+ readTestObject(module, declarator, types) {
886
+ const scopes = scopesAt(module.parsed.program, declarator);
887
+ const calls = [];
888
+ let node = declarator.init && unwrap(declarator.init);
889
+ while (node?.type === 'CallExpression' && node.callee.type === 'MemberExpression' && !node.callee.computed) {
890
+ if (node.callee.property.name !== 'extend')
891
+ break;
892
+ calls.push(node);
893
+ node = unwrap(node.callee.object);
894
+ }
895
+ const framework = node && calls.length > 0 ? this.frameworkOf(module, node, scopes, types) : null;
896
+ if (!framework)
897
+ return null;
898
+ const byName = new Map();
899
+ const auto = [];
900
+ const cut = [];
901
+ const define = (name, unit, value, options, where = { module, node: declarator, scopes }) => {
902
+ const uses = this.fixtureUses(where.module, value, where.scopes, types);
903
+ byName.set(name, [...(byName.get(name) ?? []), { unit, uses }]);
904
+ const read = options && this.valueOf(where.module, options, where.scopes, types).node;
905
+ // Playwright's own fixtures and options, such as `page` or `storageState`, shape what every test runs.
906
+ const isBuiltIn = framework === 'playwright' && BUILT_INS.has(name);
907
+ if (isBuiltIn || (read?.type === 'ObjectExpression' && read.properties.some(isAutoOption))) {
908
+ auto.push(name);
909
+ }
910
+ };
911
+ for (const call of calls) {
912
+ const first = call.arguments.at(0);
913
+ if (first === undefined)
914
+ continue;
915
+ if (first.type === 'Literal' && typeof first.value === 'string') {
916
+ // Vitest's `test.extend('name', options?, fn)`.
917
+ cut.push(...call.arguments);
918
+ const unit = this.declarationUnit(module, call, types, call.arguments);
919
+ define(first.value, unit, call.arguments.at(-1), call.arguments.length > 2 ? call.arguments[1] : undefined);
920
+ }
921
+ else if (first.type === 'ObjectExpression' && first.properties.every(isNamedProperty)) {
922
+ cut.push(first);
923
+ for (const property of first.properties) {
924
+ // `[fn, { auto: true }]`, also when a constant holds the pair or the options.
925
+ const value = this.valueOf(module, property.value, scopes, types);
926
+ const [fn, options] = value.node.type === 'ArrayExpression' ? value.node.elements : [value.node];
927
+ const unit = this.declarationUnit(module, property, types);
928
+ define(propertyKey(property), unit, fn ?? undefined, options ?? undefined, value);
929
+ }
930
+ }
931
+ }
932
+ if (cut.length === 0)
933
+ return null;
934
+ const unit = this.newUnit(module, [{ node: declarator, scopes }], new Map(cut.map((span) => [span.start, span.end])), types);
935
+ unit.with.push(this.loadUnit(module, types));
936
+ unit.fixtures = { byName, auto, framework };
937
+ return unit;
938
+ }
939
+ /**
940
+ * The framework of a test object: its own `test`, `mergeTests()` of test objects, or one built on them with
941
+ * `extend()`, followed through imports; null for an expression that isn't one.
942
+ */
943
+ frameworkOf(module, expression, scopes, types, seen = new Set()) {
944
+ const node = unwrap(expression);
945
+ if (node.type === 'CallExpression') {
946
+ const { callee } = node;
947
+ if (callee.type === 'MemberExpression' && !callee.computed && callee.property.name === 'extend') {
948
+ return this.frameworkOf(module, callee.object, scopes, types, seen);
949
+ }
950
+ // `mergeTests(a, b)`.
951
+ return this.frameworkOf(module, callee, scopes, types, seen);
952
+ }
953
+ if (node.type !== 'Identifier')
954
+ return null;
955
+ const declaration = module.values.declarationOf(node.name, scopes);
956
+ // Values built from each other, as `const poll = () => retry()` and `const retry = () => poll()`, end here.
957
+ if (declaration === null || seen.has(declaration))
958
+ return null;
959
+ seen.add(declaration);
960
+ if (declaration.type === 'ImportSpecifier') {
961
+ const source = this.importOf(module, declaration).source.value;
962
+ // `expect` has an extend() too, for matchers.
963
+ if (FRAMEWORK.test(source)) {
964
+ return TEST_EXPORTS.has(exportName(declaration.imported))
965
+ ? source === 'vitest'
966
+ ? 'vitest'
967
+ : 'playwright'
968
+ : null;
969
+ }
970
+ }
971
+ for (const unit of this.toDeclaration(module, declaration, node.name, null, types)) {
972
+ if (unit.fixtures)
973
+ return unit.fixtures.framework;
974
+ const root = unit.roots.at(0)?.node;
975
+ if (root?.type !== 'VariableDeclarator' || root.init === null)
976
+ continue;
977
+ const at = root === declaration ? scopes : scopesAt(unit.module.parsed.program, root);
978
+ const framework = this.frameworkOf(unit.module, root.init, at, types, seen);
979
+ if (framework)
980
+ return framework;
981
+ }
982
+ return null;
983
+ }
984
+ /**
985
+ * Takes what the values given to `test.use()`, `override()` or `scoped()` ask for: functions and `[fn, options]`
986
+ * pairs, as `extend()` takes them. Playwright's options and a spread of its `devices` are values, not fixtures.
987
+ */
988
+ useValues(values, take) {
989
+ for (const property of values.properties) {
990
+ if (property.type === 'SpreadElement') {
991
+ const argument = unwrap(property.argument);
992
+ const isDevice = argument.type === 'MemberExpression' &&
993
+ argument.object.type === 'Identifier' &&
994
+ argument.object.name === 'devices';
995
+ if (!isDevice)
996
+ take(property);
997
+ continue;
998
+ }
999
+ const value = unwrap(property.value);
1000
+ const fn = value.type === 'ArrayExpression' ? value.elements.at(0) : value;
1001
+ const isOption = BUILT_INS.has(propertyKey(property));
1002
+ if (fn && (isFunctionNode(fn) || !isOption))
1003
+ take(fn);
1004
+ }
1005
+ }
1006
+ /**
1007
+ * Whether a call is on a test object, so that its functions run with fixtures: the framework's own, a global one,
1008
+ * one built with `extend()`, or a parameter, as a helper that takes the test object has.
1009
+ */
1010
+ isTestReference(module, callee, scopes, types) {
1011
+ let root = callee;
1012
+ while (root.type === 'MemberExpression' || root.type === 'CallExpression') {
1013
+ root = root.type === 'MemberExpression' ? root.object : root.callee;
1014
+ }
1015
+ if (root.type !== 'Identifier')
1016
+ return false;
1017
+ const declaration = module.values.declarationOf(root.name, scopes);
1018
+ if (declaration === null)
1019
+ return GLOBAL_TESTS.has(root.name);
1020
+ if (declaration.type === 'ImportSpecifier') {
1021
+ if (FRAMEWORK.test(this.importOf(module, declaration).source.value))
1022
+ return true;
1023
+ }
1024
+ // A parameter's binding is the function it belongs to.
1025
+ if (declaration.type === 'FunctionDeclaration' || isFunctionNode(declaration)) {
1026
+ return declaration.params.some((parameter) => bindingNames(parameter).includes(root.name));
1027
+ }
1028
+ return this.frameworkOf(module, root, scopes, types) !== null;
1029
+ }
1030
+ /**
1031
+ * The fixtures a fixture's or hook's function asks for. A value such as a constant asks for none; null when the
1032
+ * function takes them whole, or the value can't be told, as a factory's result or a fixture from a package.
1033
+ */
1034
+ fixtureUses(module, value, scopes, types) {
1035
+ if (!value)
1036
+ return [];
1037
+ const { node } = this.valueOf(module, value, scopes, types);
1038
+ if (node.type === 'FunctionDeclaration' || isFunctionNode(node)) {
1039
+ return takesWhole(node, 0) ? null : destructuredNames(node, 0);
1040
+ }
1041
+ return isPlainValue(node) ? [] : null;
1042
+ }
1043
+ /**
1044
+ * What an expression holds, followed through constants and imports to the function or value they are bound to; the
1045
+ * expression itself when it is something else, with the module and scopes to read it in.
1046
+ */
1047
+ valueOf(module, value, scopes, types, depth = 0) {
1048
+ const node = unwrap(value);
1049
+ if (node.type !== 'Identifier' || depth > MAX_VALUE_DEPTH)
1050
+ return { module, node, scopes };
1051
+ // Only a `const` holds one value; a `let` may be given another later.
1052
+ const constant = module.values.constantOf(node.name, scopes);
1053
+ if (constant)
1054
+ return this.valueOf(module, constant.init, constant.scopes, types, depth + 1);
1055
+ const declaration = module.values.declarationOf(node.name, scopes);
1056
+ if (declaration === null || declaration.type === 'VariableDeclarator')
1057
+ return { module, node, scopes };
1058
+ for (const unit of this.toDeclaration(module, declaration, node.name, null, types)) {
1059
+ const root = unit.roots.at(0)?.node;
1060
+ if (root?.type === 'FunctionDeclaration')
1061
+ return { module: unit.module, node: root, scopes };
1062
+ if (root?.type === 'VariableDeclarator' && root.init && isConst(unit.module, root)) {
1063
+ const at = scopesAt(unit.module.parsed.program, root);
1064
+ return this.valueOf(unit.module, root.init, at, types, depth + 1);
1065
+ }
1066
+ }
1067
+ return { module, node, scopes };
1068
+ }
1069
+ importOf(module, specifier) {
1070
+ const program = module.parsed.program;
1071
+ return program.body.find((statement) => statement.type === 'ImportDeclaration' && contains(statement, specifier));
1072
+ }
1073
+ /** The units a unit uses directly: the declarations its names refer to, and the modules it loads. */
1074
+ edgesOf(unit) {
1075
+ const edges = new Set();
1076
+ const { module, types } = unit;
1077
+ const program = module.parsed.program;
1078
+ const isOwn = (declaration) => unit.roots.some((root) => contains(root.node, declaration));
1079
+ const add = (units) => {
1080
+ for (const target of units)
1081
+ edges.add(target);
1082
+ };
1083
+ const follow = (name, scopes, member) => {
1084
+ const declaration = module.values.declarationOf(name, scopes) ?? otherDeclaration(name, scopes, types);
1085
+ // An import in the unit's own code still names code elsewhere.
1086
+ if (declaration === null || (isOwn(declaration) && !IMPORT_BINDINGS.has(declaration.type)))
1087
+ return;
1088
+ add(this.toDeclaration(module, declaration, name, member, types));
1089
+ };
1090
+ /** A module loaded by the path in `argument`, or a place when the path is worked out at run time. */
1091
+ const loadsByPath = (argument, at) => {
1092
+ // `vi.mock(import('./broker'))`: the walk reads the import() itself.
1093
+ if (argument?.type === 'ImportExpression')
1094
+ return;
1095
+ if (argument?.type === 'Literal' && typeof argument.value === 'string') {
1096
+ add(this.imported(module, argument.value, '*', types, at));
1097
+ }
1098
+ else {
1099
+ edges.add(this.placeUnit(module, at, COMPUTED));
1100
+ }
1101
+ };
1102
+ /** The fixtures a call's functions ask for, when it registers a hook, a condition, a test or `use()` values. */
1103
+ const asks = (call, scopes) => {
1104
+ const { callee } = call;
1105
+ const name = callee.type === 'Identifier'
1106
+ ? callee.name
1107
+ : callee.type === 'MemberExpression' && !callee.computed
1108
+ ? callee.property.name
1109
+ : null;
1110
+ if (name === null)
1111
+ return;
1112
+ // A test object imported under another name, as Playwright's `import { test as setup }` for setup projects;
1113
+ // `mergeTests()` combines test objects, and isn't one.
1114
+ const declaration = callee.type === 'Identifier' ? module.values.declarationOf(callee.name, scopes) : null;
1115
+ const isMerge = declaration?.type === 'ImportSpecifier' && exportName(declaration.imported) === 'mergeTests';
1116
+ const isTest = declaration !== null && !isMerge && this.isTestObject(module, callee, declaration, scopes, types);
1117
+ if (!(isTest || FIXTURE_CALLS.has(name) || CONDITION_CALLS.has(name) || USE_CALLS.has(name)))
1118
+ return;
1119
+ // A fixture's own `use(value)` callback is called by name; `test.use()` is a member.
1120
+ if (USE_CALLS.has(name) && callee.type !== 'MemberExpression')
1121
+ return;
1122
+ // `/^[A-Z]+$/.test(symbol)`, `assert.fail()` or `app.use()` don't run with fixtures.
1123
+ if (!this.isTestReference(module, callee, scopes, types))
1124
+ return;
1125
+ const take = (value) => {
1126
+ const uses = this.fixtureUses(module, value, scopes, types);
1127
+ unit.asks = unit.asks && uses && [...unit.asks, ...uses];
1128
+ };
1129
+ if (USE_CALLS.has(name)) {
1130
+ // `test.use(MARGIN_ACCOUNT)` is followed to the object, as written in place.
1131
+ for (const values of call.arguments.slice(0, 1)) {
1132
+ const { node } = this.valueOf(module, values, scopes, types);
1133
+ if (node.type === 'ObjectExpression')
1134
+ this.useValues(node, take);
1135
+ else
1136
+ take(values);
1137
+ }
1138
+ return;
1139
+ }
1140
+ for (const argument of call.arguments) {
1141
+ const { node } = this.valueOf(module, argument, scopes, types);
1142
+ if (CONDITION_CALLS.has(name)) {
1143
+ // A condition's value, as in `test.skip(isCI)`, isn't a function of the fixtures.
1144
+ if (node.type === 'FunctionDeclaration' || isFunctionNode(node))
1145
+ take(argument);
1146
+ }
1147
+ else if (!isPlainValue(node)) {
1148
+ // A title or options, or else a function, also one a factory, a member or a condition gives.
1149
+ take(argument);
1150
+ }
1151
+ }
1152
+ };
1153
+ /** A snapshot matcher's call: the file it compares against, or nothing for one written inline. */
1154
+ const snapshot = (matcher, call, scopes) => {
1155
+ const first = call.arguments.at(0);
1156
+ const value = first === undefined ? null : this.valueOf(module, first, scopes, types).node;
1157
+ // `toMatchAriaSnapshot('- heading "Portfolio"')` holds the snapshot in the code, also from a constant.
1158
+ const isInline = value?.type === 'TemplateLiteral' || (value?.type === 'Literal' && typeof value.value === 'string');
1159
+ if (matcher === 'toMatchAriaSnapshot' && isInline)
1160
+ return;
1161
+ const named = value === null ? null : snapshotName(value);
1162
+ if (matcher === 'toMatchFileSnapshot' && (named === null || isAbsolute(named.name))) {
1163
+ edges.add(this.placeUnit(module, call, unplacedFile(named === null)));
1164
+ return;
1165
+ }
1166
+ unit.snapshots.push({ matcher, name: named?.name ?? null, isParts: named?.isParts ?? false });
1167
+ };
1168
+ const walk = (node, scopes, key, parent) => {
1169
+ if (unit.cut.get(node.start) === node.end)
1170
+ return;
1171
+ switch (node.type) {
1172
+ case 'Identifier':
1173
+ if (isReference(key, parent))
1174
+ follow(node.name, scopes, null);
1175
+ break;
1176
+ case 'JSXIdentifier':
1177
+ // `<div>` is an element; `<OrderTicket>` a component.
1178
+ if (key === 'name' && parent?.type !== 'JSXAttribute' && /^[A-Z_$]/.test(node.name)) {
1179
+ follow(node.name, scopes, null);
1180
+ }
1181
+ return;
1182
+ case 'MemberExpression':
1183
+ case 'JSXMemberExpression': {
1184
+ // `pages.OrderTicket` on a namespace import needs only that export.
1185
+ const { object, property } = node;
1186
+ const named = node.type === 'JSXMemberExpression' || !node.computed;
1187
+ if (named && (object.type === 'Identifier' || object.type === 'JSXIdentifier')) {
1188
+ // A property that isn't computed is a name.
1189
+ follow(object.name, scopes, property.name);
1190
+ return;
1191
+ }
1192
+ break;
1193
+ }
1194
+ case 'ImportExpression':
1195
+ loadsByPath(node.source, node);
1196
+ break;
1197
+ case 'CallExpression': {
1198
+ const { callee } = node;
1199
+ asks(node, scopes);
1200
+ if (callee.type === 'Identifier' && callee.name === 'require')
1201
+ loadsByPath(node.arguments.at(0), node);
1202
+ if (callee.type === 'MemberExpression' && !callee.computed) {
1203
+ const { object, property } = callee;
1204
+ if (SNAPSHOT_MATCHERS.has(property.name))
1205
+ snapshot(property.name, node, scopes);
1206
+ // `Snapshots.toMatchSnapshot.call(this, received)` in a custom matcher, or one taken out of `Snapshots`.
1207
+ const target = object.type === 'MemberExpression' && !object.computed
1208
+ ? object.property.name
1209
+ : object.type === 'Identifier'
1210
+ ? object.name
1211
+ : '';
1212
+ if (CALLS.has(property.name) && target === 'toMatchFileSnapshot') {
1213
+ edges.add(this.placeUnit(module, node, unplacedFile(true)));
1214
+ }
1215
+ else if (CALLS.has(property.name) && SNAPSHOT_MATCHERS.has(target)) {
1216
+ unit.snapshots.push({ matcher: target, name: null, isParts: false });
1217
+ }
1218
+ if (object.type === 'Identifier' && object.name === 'vi' && VI_MODULE_CALLS.has(property.name)) {
1219
+ loadsByPath(node.arguments.at(0), node);
1220
+ }
1221
+ if (object.type === 'MetaProperty' && property.name.startsWith('glob')) {
1222
+ edges.add(this.placeUnit(module, node, "The modules this loads match a pattern, so dependencyHash doesn't cover them."));
1223
+ }
1224
+ }
1225
+ break;
1226
+ }
1227
+ case 'ImportDeclaration': {
1228
+ if (module.text === null) {
1229
+ if (node.importKind !== 'type' || types)
1230
+ add(this.imported(module, node.source.value, '', types, node));
1231
+ return;
1232
+ }
1233
+ // A component's markup uses what its scripts import, where the scan doesn't look, and Vue builds the props
1234
+ // of `defineProps<Props>()` from the types, so every import counts, types with their types.
1235
+ const isType = node.importKind === 'type';
1236
+ add(this.imported(module, node.source.value, '', isType, node));
1237
+ for (const specifier of node.specifiers) {
1238
+ const isTypeName = isType || (specifier.type === 'ImportSpecifier' && specifier.importKind === 'type');
1239
+ add(this.toDeclaration(module, specifier, specifier.local.name, null, isTypeName));
1240
+ }
1241
+ return;
1242
+ }
1243
+ case 'TSImportEqualsDeclaration':
1244
+ if (node.moduleReference.type !== 'TSExternalModuleReference')
1245
+ break;
1246
+ if (node.importKind !== 'type' || types) {
1247
+ add(this.imported(module, node.moduleReference.expression.value, '', types, node));
1248
+ }
1249
+ return;
1250
+ // Re-exports, in a module taken whole: each name comes from the other module.
1251
+ case 'ExportNamedDeclaration':
1252
+ if (!node.source)
1253
+ break;
1254
+ if (node.exportKind !== 'type' || types) {
1255
+ for (const specifier of node.specifiers) {
1256
+ add(this.imported(module, node.source.value, exportName(specifier.local), types, node));
1257
+ }
1258
+ }
1259
+ return;
1260
+ case 'ExportAllDeclaration':
1261
+ if (node.exportKind !== 'type' || types)
1262
+ add(this.imported(module, node.source.value, '*', types, node));
1263
+ return;
1264
+ }
1265
+ const isScope = isScopeNode(node);
1266
+ if (isScope)
1267
+ scopes.push(node);
1268
+ const fields = node;
1269
+ for (const field of visitorKeys[node.type]) {
1270
+ if (!types && TYPE_KEYS.has(field))
1271
+ continue;
1272
+ const value = fields[field];
1273
+ if (Array.isArray(value)) {
1274
+ for (const item of value)
1275
+ if (item)
1276
+ walk(item, scopes, field, node);
1277
+ }
1278
+ else if (value) {
1279
+ walk(value, scopes, field, node);
1280
+ }
1281
+ }
1282
+ if (isScope)
1283
+ scopes.pop();
1284
+ };
1285
+ for (const root of unit.roots)
1286
+ walk(root.node, [...root.scopes], null, null);
1287
+ // A module's imports run when it is loaded, whatever of them it uses.
1288
+ if (unit === module.load[Number(types)]) {
1289
+ for (const statement of program.body) {
1290
+ if (statement.type === 'ImportDeclaration' || statement.type === 'TSImportEqualsDeclaration') {
1291
+ walk(statement, [program], null, null);
1292
+ }
1293
+ if ((statement.type === 'ExportAllDeclaration' || statement.type === 'ExportNamedDeclaration') &&
1294
+ statement.source &&
1295
+ (statement.exportKind !== 'type' || types)) {
1296
+ add(this.imported(module, statement.source.value, '', types, statement));
1297
+ }
1298
+ }
1299
+ }
1300
+ return edges;
1301
+ }
1302
+ }
1303
+ /** Whether an identifier refers to a binding, rather than naming a property, a label or a member. */
1304
+ function isReference(key, parent) {
1305
+ switch (parent?.type) {
1306
+ case 'MemberExpression':
1307
+ return key === 'object' || parent.computed;
1308
+ case 'Property':
1309
+ case 'MethodDefinition':
1310
+ case 'PropertyDefinition':
1311
+ case 'AccessorProperty':
1312
+ case 'TSPropertySignature':
1313
+ case 'TSMethodSignature':
1314
+ return key !== 'key' || parent.computed;
1315
+ case 'LabeledStatement':
1316
+ case 'BreakStatement':
1317
+ case 'ContinueStatement':
1318
+ return false;
1319
+ case 'TSEnumMember':
1320
+ case 'TSModuleDeclaration':
1321
+ case 'TSTypeParameter':
1322
+ case 'TSImportEqualsDeclaration':
1323
+ return key !== 'id' && key !== 'name';
1324
+ case 'TSQualifiedName':
1325
+ return key === 'left';
1326
+ case 'ExportSpecifier':
1327
+ return key === 'local';
1328
+ case 'MetaProperty':
1329
+ return false;
1330
+ default:
1331
+ return true;
1332
+ }
1333
+ }
1334
+ /**
1335
+ * A namespace or `import x = require()` a name refers to, or for a type test also an interface or type alias, looked
1336
+ * up in the scopes around it. Imports, classes and enums are bindings `Values` already finds.
1337
+ */
1338
+ function otherDeclaration(name, scopes, types) {
1339
+ for (let index = scopes.length - 1; index >= 0; index--) {
1340
+ const scope = scopes[index];
1341
+ const body = scope.type === 'Program' || scope.type === 'BlockStatement' || scope.type === 'TSModuleBlock' ? scope.body : [];
1342
+ for (const statement of body) {
1343
+ const node = declared(statement);
1344
+ const isNamed = 'id' in node && node.id?.type === 'Identifier' && node.id.name === name;
1345
+ const isValue = node.type === 'TSModuleDeclaration' || node.type === 'TSImportEqualsDeclaration';
1346
+ if (isNamed && (isValue || (types && TYPES.has(node.type))))
1347
+ return node;
1348
+ }
1349
+ }
1350
+ return null;
1351
+ }
1352
+ /** The first string literal with a value in a tree, in source order; null when there is none. */
1353
+ function literalIn(node, value) {
1354
+ if (node.type === 'Literal' && node.value === value)
1355
+ return node;
1356
+ for (const child of childNodes(node)) {
1357
+ const found = literalIn(child, value);
1358
+ if (found)
1359
+ return found;
1360
+ }
1361
+ return null;
1362
+ }
1363
+ /**
1364
+ * The snapshot files the matchers of a test compare against, from the project root, at the runners' default places:
1365
+ * Vitest's `__snapshots__/<file>.snap`, paths given to `toMatchFileSnapshot()`, and browser screenshots in
1366
+ * `__screenshots__/<file>/`, beside the test file; Playwright's `<file>-snapshots/` folder. Of a folder, the files that
1367
+ * start with a name given, or all of them when a matcher gives none; `outside` when a path leaves the project.
1368
+ */
1369
+ function snapshotPaths(testFile, uses, framework) {
1370
+ const folder = path.posix.dirname(testFile);
1371
+ const name = path.posix.basename(testFile);
1372
+ const found = new Set();
1373
+ const folders = new Map();
1374
+ /** A name in a folder; path parts, as Playwright takes, may lead to another folder. */
1375
+ const named = (target, stem) => {
1376
+ const file = stem === null ? null : path.posix.join(target, stem);
1377
+ const where = file === null ? target : path.posix.dirname(file);
1378
+ folders.set(where, [...(folders.get(where) ?? []), file === null ? null : path.posix.basename(file)]);
1379
+ };
1380
+ for (const use of uses) {
1381
+ if (framework === 'playwright')
1382
+ named(path.posix.join(folder, `${name}-snapshots`), playwrightStem(use));
1383
+ else if (use.matcher === 'toMatchScreenshot') {
1384
+ named(path.posix.join(folder, '__screenshots__', name), use.name === null ? null : vitestStem(use.name));
1385
+ }
1386
+ else if (use.matcher === 'toMatchFileSnapshot')
1387
+ found.add(path.posix.join(folder, use.name));
1388
+ else
1389
+ found.add(path.posix.join(folder, '__snapshots__', `${name}.snap`));
1390
+ }
1391
+ const isInside = (file) => !file.startsWith('../');
1392
+ return {
1393
+ files: [...found].filter(isInside),
1394
+ folders: new Map([...folders].filter(([where]) => isInside(where))),
1395
+ isOutside: ![...found, ...folders.keys()].every(isInside),
1396
+ };
1397
+ }
1398
+ /**
1399
+ * The start of the files Playwright writes for a name: without its extension, as the project and platform come
1400
+ * before it, and a string name sanitized as Playwright 1.64 does, where path parts are kept as written.
1401
+ */
1402
+ function playwrightStem(use) {
1403
+ const stem = use.name?.replace(/(\.aria\.yml|\.[^./]*)$/, '') ?? null;
1404
+ return stem !== null && !use.isParts ? stem.replace(UNSAFE_NAME, '-') : stem;
1405
+ }
1406
+ /** The start of the files Vitest 5 writes for a screenshot name, sanitized as its `sanitizeArg` does. */
1407
+ function vitestStem(name) {
1408
+ return name
1409
+ .replace(/\.png$/, '')
1410
+ .replace(/^\.\//, '')
1411
+ .replaceAll('..', '')
1412
+ .replace(/\s+/g, '-')
1413
+ .replace(/[^\w-]/g, '')
1414
+ .replace(/-{2,}/g, '-');
1415
+ }
1416
+ /** Whether a path is absolute, on any platform, as Vitest takes it as it is. */
1417
+ function isAbsolute(file) {
1418
+ return path.posix.isAbsolute(file) || path.win32.isAbsolute(file);
1419
+ }
1420
+ /** Why a file snapshot isn't followed: a path worked out at run time, or an absolute one. */
1421
+ function unplacedFile(isComputed) {
1422
+ const why = isComputed ? 'is worked out at run time' : "is an absolute path, which the scan doesn't place";
1423
+ return `The file this compares against ${why}, so dependencyHash doesn't cover it.`;
1424
+ }
1425
+ /** Whether a file in a snapshots folder is one a name gives, as Playwright and Vitest add a suffix to the name. */
1426
+ function hasStem(file, stem) {
1427
+ return stem === null || file === stem || file.startsWith(`${stem}-`) || file.startsWith(`${stem}.`);
1428
+ }
1429
+ /** The statements of a describe's callback, or its expression when it has no block body. */
1430
+ function bodyNodes(fn) {
1431
+ const body = fn.body;
1432
+ return body.type === 'BlockStatement' ? body.body : [body];
1433
+ }
1434
+ /**
1435
+ * What each test of a scanned file runs: its own call, the helper call that registers it, every describe around it
1436
+ * with its hooks and options, the file's own top-level code, and what all of these reach. `report` gets each place the
1437
+ * scan can't follow, once per scan.
1438
+ */
1439
+ export function fileDependencies(graph, main, extraction, types, report, runsWith = { config: null, modules: [], gaps: [], moved: null }, snapshotFiles = null) {
1440
+ const moduleOf = (frame) => graph.module(frame.parsed, frame.relativeFilePath, frame.values, frame.readModule);
1441
+ const mainModule = moduleOf(main);
1442
+ // The file's tests and describes, and the helper calls that declare them, are each test's own, not code every test
1443
+ // in the file or a describe runs.
1444
+ const items = [...extraction.tests, ...extraction.suites];
1445
+ const registered = items.flatMap((item) => (item.registeredBy ? [item.registeredBy] : []));
1446
+ const load = graph.loadUnit(mainModule, types, [...items.flatMap((item) => (item.frame === main ? [item.call] : [])), ...registered], true);
1447
+ const registering = (call) => (call ? [graph.unit(mainModule, [call], [], types)] : []);
1448
+ const suiteUnits = new Map();
1449
+ const suiteUnit = (suite) => {
1450
+ let units = suiteUnits.get(suite);
1451
+ if (!units) {
1452
+ // A describe without a callback has no tests in it.
1453
+ const fn = suite.declaration.fn;
1454
+ units = [
1455
+ graph.unit(moduleOf(suite.frame), [suite.call, ...bodyNodes(fn).filter(isLoadCode)],
1456
+ // Offsets in another file could match these by chance.
1457
+ [fn, ...suite.cuts, ...(suite.frame === main ? registered : [])], types),
1458
+ ...registering(suite.registeredBy),
1459
+ ];
1460
+ suiteUnits.set(suite, units);
1461
+ }
1462
+ return units;
1463
+ };
1464
+ // What the file runs when it loads is the same for each of its tests, and often most of what they run.
1465
+ const config = [
1466
+ ...(runsWith.config ? [graph.runUnit(runsWith.config, true)] : []),
1467
+ ...runsWith.modules.map((module) => graph.runUnit(module, false)),
1468
+ ...runsWith.gaps.map((gap) => graph.gapUnit(gap)),
1469
+ ];
1470
+ const base = graph.reach([load, ...config]);
1471
+ // The describes around a test and the helper call that registers it are shared by the tests there, often thousands
1472
+ // for one helper call, so what they reach is walked once for all of them.
1473
+ const contexts = new Map();
1474
+ const contextOf = (scope, registeredBy) => {
1475
+ const byCall = contexts.get(scope) ?? new Map();
1476
+ contexts.set(scope, byCall);
1477
+ let context = byCall.get(registeredBy);
1478
+ if (!context) {
1479
+ const roots = registering(registeredBy);
1480
+ for (let around = scope; around; around = around.parent) {
1481
+ if (around.suite)
1482
+ roots.push(...suiteUnit(around.suite));
1483
+ }
1484
+ context = graph.reach(roots, [base]);
1485
+ byCall.set(registeredBy, context);
1486
+ }
1487
+ return context;
1488
+ };
1489
+ const snapshots = (uses) => {
1490
+ if (!snapshotFiles)
1491
+ return [graph.quietUnit(mainModule, 'Snapshot files are not read when one file is scanned.')];
1492
+ if (snapshotFiles.moved)
1493
+ return [graph.gapUnit(snapshotFiles.moved)];
1494
+ const paths = snapshotPaths(main.relativeFilePath, uses, snapshotFiles.framework);
1495
+ const units = [];
1496
+ const unreadable = () => graph.quietUnit(mainModule, "A snapshot file can't be read, so dependencyHash doesn't cover it.");
1497
+ if (paths.isOutside)
1498
+ units.push(graph.quietUnit(mainModule, "A snapshot path leaves the project, so dependencyHash doesn't cover it."));
1499
+ const files = [...paths.files];
1500
+ for (const [folder, stems] of paths.folders) {
1501
+ const listed = snapshotFiles.list(folder);
1502
+ if (listed === 'unreadable')
1503
+ units.push(unreadable());
1504
+ else
1505
+ files.push(...listed.filter((file) => stems.some((stem) => hasStem(file, stem))).map((file) => `${folder}/${file}`));
1506
+ }
1507
+ for (const file of files) {
1508
+ // A snapshot not written yet is written by the test's first run.
1509
+ const unit = graph.snapshotUnit(file, () => snapshotFiles.read(file));
1510
+ if (unit === 'unreadable')
1511
+ units.push(unreadable());
1512
+ else if (unit)
1513
+ units.push(unit);
1514
+ }
1515
+ return units;
1516
+ };
1517
+ // A test declared in a helper is one call for every test the helper's calls register.
1518
+ const calls = new Map();
1519
+ return (test) => {
1520
+ let own = calls.get(test.call);
1521
+ if (!own) {
1522
+ own = graph.unit(moduleOf(test.frame), [test.call], [], types);
1523
+ calls.set(test.call, own);
1524
+ }
1525
+ const roots = [own];
1526
+ const needs = { names: [...test.declaration.fixtures], all: test.declaration.takesAllFixtures };
1527
+ for (let scope = test.scope; scope; scope = scope.parent) {
1528
+ needs.names.push(...scope.setupFixtures);
1529
+ needs.all ||= scope.takesAllFixtures;
1530
+ }
1531
+ return graph.dependencies(roots, [base, contextOf(test.scope, test.registeredBy)], needs, snapshots, report);
1532
+ };
1533
+ }