test-inventory 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +4 -1
  3. package/dist/ast.d.ts +3 -3
  4. package/dist/ast.js +4 -4
  5. package/dist/bin.js +1 -0
  6. package/dist/cli.d.ts +4 -2
  7. package/dist/cli.js +27 -7
  8. package/dist/config-dependencies.d.ts +62 -0
  9. package/dist/config-dependencies.js +231 -0
  10. package/dist/config-runs.d.ts +113 -0
  11. package/dist/config-runs.js +807 -0
  12. package/dist/config.d.ts +90 -0
  13. package/dist/config.js +652 -0
  14. package/dist/dependency-hash.d.ts +242 -0
  15. package/dist/dependency-hash.js +1458 -0
  16. package/dist/diagnostics.js +6 -1
  17. package/dist/extract.d.ts +20 -0
  18. package/dist/extract.js +20 -4
  19. package/dist/filter.d.ts +6 -0
  20. package/dist/filter.js +18 -1
  21. package/dist/frameworks/bindings.d.ts +22 -0
  22. package/dist/frameworks/bindings.js +19 -9
  23. package/dist/frameworks/common.d.ts +5 -0
  24. package/dist/frameworks/common.js +11 -0
  25. package/dist/frameworks/playwright.d.ts +2 -1
  26. package/dist/frameworks/playwright.js +11 -2
  27. package/dist/frameworks/vitest.d.ts +2 -1
  28. package/dist/frameworks/vitest.js +9 -3
  29. package/dist/hash.d.ts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/inventory.d.ts +2 -2
  32. package/dist/inventory.js +2 -1
  33. package/dist/json.js +2 -0
  34. package/dist/model.d.ts +9 -1
  35. package/dist/options.d.ts +2 -2
  36. package/dist/records.d.ts +58 -2
  37. package/dist/records.js +196 -28
  38. package/dist/scan-file.d.ts +13 -1
  39. package/dist/scan-file.js +16 -4
  40. package/dist/scan-source.js +1 -1
  41. package/dist/scan.d.ts +2 -1
  42. package/dist/scan.js +131 -18
  43. package/dist/types.d.ts +83 -7
  44. package/dist/values.d.ts +5 -0
  45. package/dist/values.js +5 -0
  46. package/dist/vitest-config.d.ts +6 -4
  47. package/dist/vitest-config.js +32 -28
  48. package/dist/vitest-defaults.d.ts +6 -0
  49. package/dist/vitest-defaults.js +16 -0
  50. package/package.json +10 -8
  51. package/schema/v1.json +200 -6
@@ -22,6 +22,11 @@ export const LEVELS = {
22
22
  'removed-api': 'error',
23
23
  'only-in-skipped-describe': 'warning',
24
24
  'config-may-change-state': 'warning',
25
+ 'undefined-tag': 'error',
26
+ 'config-value-unknown': 'warning',
27
+ 'file-not-run': 'warning',
28
+ 'forbidden-only': 'error',
29
+ 'dependency-not-followed': 'warning',
25
30
  'symlink-ignored': 'warning',
26
31
  'invalid-import': 'error',
27
32
  'globals-not-enabled': 'warning',
@@ -44,7 +49,7 @@ export const LEVELS = {
44
49
  */
45
50
  export const REJECTS_FILE = {
46
51
  playwright: new Set(['test-without-body', 'tag-without-at', 'invalid-chain', 'duplicate-title']),
47
- vitest: new Set(['invalid-chain', 'removed-api']),
52
+ vitest: new Set(['invalid-chain', 'removed-api', 'undefined-tag']),
48
53
  };
49
54
  export function toDiagnostic(problem, relativeFilePath, source, testId = null) {
50
55
  const position = problem.node && source ? source.position(problem.node.start) : null;
package/dist/extract.d.ts CHANGED
@@ -18,6 +18,10 @@ export interface Scope {
18
18
  hookRun: RunSettings;
19
19
  /** Fixtures of the hooks in the scope that run around each test. */
20
20
  hookFixtures: string[];
21
+ /** Fixtures of every hook in the scope, `beforeAll` and `afterAll` too, which set up what its tests run. */
22
+ setupFixtures: string[];
23
+ /** A hook in the scope takes its fixtures whole, so it may use any. */
24
+ takesAllFixtures: boolean;
21
25
  suite: PendingSuite | null;
22
26
  }
23
27
  /** Where a test or describe registered through an imported helper is written, in the helper's module. */
@@ -33,10 +37,17 @@ export interface PendingSuite {
33
37
  /** For a describe declared in an imported helper, the call in the scanned file that registers it; null otherwise. */
34
38
  site: Span | null;
35
39
  declaredAt: DeclaredAt | null;
40
+ /** The file the describe is written in. */
41
+ frame: Frame;
42
+ /** The outermost helper call in the scanned file that declares the describe; null when the file declares it. */
43
+ registeredBy: CallExpression | null;
36
44
  /** Where the scanned file registers the describe: its own call, or the outermost helper call that declares it. */
37
45
  order: number;
38
46
  /** Statements of the tests and describes directly inside, left out of the describe's body hash. */
39
47
  cuts: Span[];
48
+ /** Declared under a condition, or in a function: it may not run while the file loads. */
49
+ isInCondition: boolean;
50
+ isInFunction: boolean;
40
51
  }
41
52
  export interface PendingTest {
42
53
  scope: Scope;
@@ -45,6 +56,10 @@ export interface PendingTest {
45
56
  /** For a test declared in an imported helper, the call in the scanned file that registers it; null otherwise. */
46
57
  site: Span | null;
47
58
  declaredAt: DeclaredAt | null;
59
+ /** The file the test is written in. */
60
+ frame: Frame;
61
+ /** The outermost helper call in the scanned file that declares the test; null when the file declares it. */
62
+ registeredBy: CallExpression | null;
48
63
  /** Where the scanned file registers the test: its own call, or the outermost helper call that declares it. */
49
64
  order: number;
50
65
  /** States from the declaration, then from calls in the body such as `test.skip(condition)`. */
@@ -80,6 +95,11 @@ export interface Extraction {
80
95
  /** Hooks at the top level of the file. */
81
96
  hooks: HookCounts;
82
97
  usesGlobals: boolean;
98
+ /** The file's `@module-tag` tags, where each is written. */
99
+ fileTags: {
100
+ name: string;
101
+ span: Span;
102
+ }[];
83
103
  }
84
104
  /**
85
105
  * Applies settings written later in the same place: the last value of each wins, and a test is slow when any
package/dist/extract.js CHANGED
@@ -140,6 +140,8 @@ class Walker {
140
140
  hooks: noHooks(),
141
141
  hookRun: {},
142
142
  hookFixtures: [],
143
+ setupFixtures: [],
144
+ takesAllFixtures: false,
143
145
  suite: null,
144
146
  };
145
147
  }
@@ -158,7 +160,7 @@ class Walker {
158
160
  helper: null,
159
161
  declaredIn: null,
160
162
  quiet: false,
161
- registeredAt: null,
163
+ registeredBy: null,
162
164
  });
163
165
  // Functions defined inside these are deferred in turn; the loop reaches them too, as they are added to the end.
164
166
  for (const entry of this.deferred)
@@ -175,6 +177,10 @@ class Walker {
175
177
  problems: [...this.main.adapter.problems, ...this.problems],
176
178
  hooks: this.fileScope.hooks,
177
179
  usesGlobals: this.main.adapter.usesGlobals,
180
+ fileTags: this.main.adapter.fileTags.map((name, index) => ({
181
+ name,
182
+ span: this.main.adapter.fileTagSpans[index],
183
+ })),
178
184
  };
179
185
  }
180
186
  visit(node, context) {
@@ -414,6 +420,8 @@ class Walker {
414
420
  context.scope.hooks[api.name] += 1;
415
421
  if (EACH_HOOKS.has(api.name))
416
422
  context.scope.hookFixtures.push(...api.fixtures);
423
+ context.scope.setupFixtures.push(...api.fixtures);
424
+ context.scope.takesAllFixtures ||= api.takesAllFixtures;
417
425
  }
418
426
  if (api.fn) {
419
427
  if (!call.arguments.includes(api.fn))
@@ -624,7 +632,7 @@ class Walker {
624
632
  ...context,
625
633
  isInFunction: true,
626
634
  declaredIn: null,
627
- registeredAt: context.registeredAt ?? call.start,
635
+ registeredBy: context.registeredBy ?? call,
628
636
  });
629
637
  });
630
638
  this.callStack.pop();
@@ -922,7 +930,9 @@ class Walker {
922
930
  call,
923
931
  site: this.site,
924
932
  declaredAt: this.declaredAt(call),
925
- order: context.registeredAt ?? call.start,
933
+ frame: this.frame,
934
+ registeredBy: context.registeredBy,
935
+ order: (context.registeredBy ?? call).start,
926
936
  states: [...declaration.states],
927
937
  annotations: [...declaration.details.annotations],
928
938
  isInLoop: context.isInLoop,
@@ -972,6 +982,8 @@ class Walker {
972
982
  hooks: noHooks(),
973
983
  hookRun: {},
974
984
  hookFixtures: [],
985
+ setupFixtures: [],
986
+ takesAllFixtures: false,
975
987
  suite: null,
976
988
  };
977
989
  const suite = {
@@ -980,8 +992,12 @@ class Walker {
980
992
  call,
981
993
  site: this.site,
982
994
  declaredAt: this.declaredAt(call),
983
- order: context.registeredAt ?? call.start,
995
+ frame: this.frame,
996
+ registeredBy: context.registeredBy,
997
+ order: (context.registeredBy ?? call).start,
984
998
  cuts: [],
999
+ isInCondition: context.isInCondition,
1000
+ isInFunction: context.isInFunction,
985
1001
  };
986
1002
  scope.suite = suite;
987
1003
  this.suites.push(suite);
package/dist/filter.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface InventoryFilter {
11
11
  only?: boolean;
12
12
  /** Keeps tests that don't run and give no reason. Tests in a file the runner refuses are left out. */
13
13
  withoutReason?: boolean;
14
+ /**
15
+ * Keeps the tests a change to one of these files can affect: those whose `dependencies` include it, and those that
16
+ * reach code the scan can't follow (`hasUnfollowedDependencies`). Paths are from the project root, as
17
+ * `dependencies` writes them. An empty list keeps only the tests the scan can't follow.
18
+ */
19
+ related?: string[];
14
20
  }
15
21
  /** An inventory whose test records have only `id` and the keys picked with `pickTestFields()`. */
16
22
  export type PickedInventory<K extends keyof TestRecord> = Omit<Inventory, 'tests'> & {
package/dist/filter.js CHANGED
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import { checkObject, OptionsError } from "./options.js";
2
3
  import { summarize } from "./summary.js";
3
4
  const STATES = ['active', 'skip', 'fixme', 'todo', 'expectedToFail', 'notLoaded'];
@@ -7,11 +8,15 @@ const FILTER_KEYS = [
7
8
  'grep',
8
9
  'only',
9
10
  'withoutReason',
11
+ 'related',
10
12
  ];
11
13
  // Typed as a record of every key, so a key added to TestRecord fails to compile until it is listed here.
12
14
  const TEST_KEYS = Object.keys({
13
15
  id: true,
14
16
  bodyHash: true,
17
+ dependencyHash: true,
18
+ dependencies: true,
19
+ hasUnfollowedDependencies: true,
15
20
  suiteId: true,
16
21
  suitePath: true,
17
22
  relativeFilePath: true,
@@ -55,6 +60,12 @@ const TEST_KEYS = Object.keys({
55
60
  retryDelay: true,
56
61
  retryCondition: true,
57
62
  repeats: true,
63
+ timeoutSource: true,
64
+ retriesSource: true,
65
+ repeatsSource: true,
66
+ timeoutConfigText: true,
67
+ retriesConfigText: true,
68
+ repeatsConfigText: true,
58
69
  fixtures: true,
59
70
  expectCount: true,
60
71
  expectedAssertions: true,
@@ -67,6 +78,7 @@ const TEST_KEYS = Object.keys({
67
78
  isInLoop: true,
68
79
  isInCondition: true,
69
80
  isInFunction: true,
81
+ projects: true,
70
82
  });
71
83
  function list(value, option, allowed) {
72
84
  if (value === undefined)
@@ -110,11 +122,16 @@ export function matcher(filter) {
110
122
  const grep = pattern(checked.grep);
111
123
  const only = flag(checked.only, 'filter.only');
112
124
  const withoutReason = flag(checked.withoutReason, 'filter.withoutReason');
125
+ // Written as a shell or Windows may write them, as `./pages/order-ticket.ts` or `pages\\order-ticket.ts`.
126
+ const related = checked.related === undefined
127
+ ? null
128
+ : new Set(list(checked.related, 'filter.related', null).map((file) => path.posix.normalize(file.replaceAll('\\', '/'))));
113
129
  return (test) => (states.length === 0 || states.includes(test.state)) &&
114
130
  (tags.length === 0 || test.tags.some((tag) => tags.includes(tag))) &&
115
131
  (grep === null || grep.test(test.fullName)) &&
116
132
  (!only || test.isOnly) &&
117
- (!withoutReason || (!test.isActive && !test.isNotLoaded && test.reason === null));
133
+ (!withoutReason || (!test.isActive && !test.isNotLoaded && test.reason === null)) &&
134
+ (related === null || test.hasUnfollowedDependencies || test.dependencies.some((file) => related.has(file)));
118
135
  }
119
136
  /**
120
137
  * Keeps the tests that pass every condition of the filter; a list keeps a test that has any of its values. The
@@ -54,10 +54,32 @@ export declare function allDeclarators(program: Program): VariableDeclarator[];
54
54
  export declare function moduleImports(program: Program, isFramework: (source: string) => boolean): Map<string, ModuleImport>;
55
55
  /** The module of `require('module')`, or null for any other expression. */
56
56
  export declare function requiredModule(expression: Expression): string | null;
57
+ /** What `module.exports = value` assigns, the default export of a CommonJS module; null for any other statement. */
58
+ export declare function moduleExportsValue(statement: Node): Expression | null;
57
59
  /** Applies `bind` to each declarator until no more names are bound, so declaration order doesn't matter. */
58
60
  export declare function bindAll(declarators: VariableDeclarator[], bind: (declarator: VariableDeclarator) => boolean): void;
59
61
  /** Whether a module exports `name` as a test object, or as a function that returns one. */
60
62
  export declare function exportsTestObject(program: Program, name: string, objects: TestObjects, exported?: Exported): boolean;
63
+ /** How a module exports `name` itself, in `statement`: declared, or exported by name from a local or another module. */
64
+ export type OwnExport = {
65
+ statement: Node;
66
+ } & ({
67
+ kind: 'default';
68
+ value: Expression;
69
+ } | {
70
+ kind: 'variable' | 'function';
71
+ } | {
72
+ kind: 'specifier';
73
+ local: string;
74
+ source: string | null;
75
+ });
76
+ /**
77
+ * The module's own export of `name`; null when it has none and only `export *` may pass the name on. An own export
78
+ * wins over those, as in JavaScript.
79
+ */
80
+ export declare function ownExport(program: Program, name: string): OwnExport | null;
81
+ /** The module `export * from 'module'` or `module.exports = require('module')` passes every name on from; or null. */
82
+ export declare function starReexport(statement: Node): string | null;
61
83
  /** Whether a module imports, re-exports or requires any of the given sources. */
62
84
  export declare function importsFrom(program: Program, matches: (source: string) => boolean): boolean;
63
85
  /** Whether a name a module imports from another module is a test object there, which follows that module in turn. */
@@ -135,8 +135,8 @@ export function requiredModule(expression) {
135
135
  const argument = node.arguments.at(0);
136
136
  return argument?.type === 'Literal' && typeof argument.value === 'string' ? argument.value : null;
137
137
  }
138
- /** The module of `module.exports = require('module')`, which exports what that module does; null otherwise. */
139
- function commonJsReexport(statement) {
138
+ /** What `module.exports = value` assigns, the default export of a CommonJS module; null for any other statement. */
139
+ export function moduleExportsValue(statement) {
140
140
  if (statement.type !== 'ExpressionStatement')
141
141
  return null;
142
142
  const expression = unwrap(statement.expression);
@@ -147,7 +147,12 @@ function commonJsReexport(statement) {
147
147
  target.object.type === 'Identifier' &&
148
148
  target.object.name === 'module' &&
149
149
  propertyName(target) === 'exports';
150
- return isModuleExports ? requiredModule(expression.right) : null;
150
+ return isModuleExports ? expression.right : null;
151
+ }
152
+ /** The module of `module.exports = require('module')`, which exports what that module does; null otherwise. */
153
+ function commonJsReexport(statement) {
154
+ const value = moduleExportsValue(statement);
155
+ return value ? requiredModule(value) : null;
151
156
  }
152
157
  /** Applies `bind` to each declarator until no more names are bound, so declaration order doesn't matter. */
153
158
  export function bindAll(declarators, bind) {
@@ -191,10 +196,10 @@ export function exportsTestObject(program, name, objects, exported = 'test objec
191
196
  * The module's own export of `name`; null when it has none and only `export *` may pass the name on. An own export
192
197
  * wins over those, as in JavaScript.
193
198
  */
194
- function ownExport(program, name) {
199
+ export function ownExport(program, name) {
195
200
  for (const statement of program.body) {
196
201
  if (statement.type === 'ExportDefaultDeclaration' && name === 'default') {
197
- return { kind: 'default', value: statement.declaration };
202
+ return { statement, kind: 'default', value: statement.declaration };
198
203
  }
199
204
  if (statement.type !== 'ExportNamedDeclaration')
200
205
  continue;
@@ -202,21 +207,26 @@ function ownExport(program, name) {
202
207
  if (declaration?.type === 'VariableDeclaration') {
203
208
  const declared = declaration.declarations.some((item) => item.id.type === 'Identifier' && item.id.name === name);
204
209
  if (declared)
205
- return { kind: 'variable' };
210
+ return { statement, kind: 'variable' };
206
211
  }
207
212
  const named = declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration';
208
213
  if (named && declaration.id?.name === name)
209
- return { kind: 'function' };
214
+ return { statement, kind: 'function' };
210
215
  for (const specifier of statement.specifiers) {
211
216
  if (exportName(specifier.exported) !== name)
212
217
  continue;
213
- return { kind: 'specifier', local: exportName(specifier.local), source: statement.source?.value ?? null };
218
+ return {
219
+ statement,
220
+ kind: 'specifier',
221
+ local: exportName(specifier.local),
222
+ source: statement.source?.value ?? null,
223
+ };
214
224
  }
215
225
  }
216
226
  return null;
217
227
  }
218
228
  /** The module `export * from 'module'` or `module.exports = require('module')` passes every name on from; or null. */
219
- function starReexport(statement) {
229
+ export function starReexport(statement) {
220
230
  if (statement.type === 'ExportAllDeclaration')
221
231
  return statement.exported ? null : statement.source.value;
222
232
  return commonJsReexport(statement);
@@ -116,6 +116,11 @@ export declare function readOptions(file: FileContext, scopes: readonly Node[],
116
116
  export declare function unresolvedOptions(file: FileContext, options: Options, result: string): Problem[];
117
117
  /** The names a function takes from the object pattern of one of its parameters, in source order. */
118
118
  export declare function destructuredNames(fn: FunctionNode | null, index: number | null): string[];
119
+ /**
120
+ * Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
121
+ * pattern, so that any key of it may be used.
122
+ */
123
+ export declare function takesWhole(fn: FunctionNode | null, index: number | null): boolean;
119
124
  /** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
120
125
  export declare function readCount(file: FileContext, name: string, option: OptionValue, problems: Problem[]): number | null;
121
126
  /** A warning for one option whose value could not be worked out. */
@@ -287,6 +287,17 @@ export function destructuredNames(fn, index) {
287
287
  const names = parameter.properties.map((property) => (property.type === 'Property' ? propertyKey(property) : null));
288
288
  return names.filter((name) => name !== null);
289
289
  }
290
+ /**
291
+ * Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
292
+ * pattern, so that any key of it may be used.
293
+ */
294
+ export function takesWhole(fn, index) {
295
+ const parameter = index === null ? undefined : fn?.params[index];
296
+ if (!parameter)
297
+ return false;
298
+ return (parameter.type !== 'ObjectPattern' ||
299
+ parameter.properties.some((property) => property.type === 'RestElement' || property.computed));
300
+ }
290
301
  /** A whole number, such as a timeout in milliseconds; null, with a warning, when it can't be worked out. */
291
302
  export function readCount(file, name, option, problems) {
292
303
  const { node, resolved } = option;
@@ -1,9 +1,10 @@
1
1
  import type { CallExpression, Expression, Node, Program } from 'oxc-parser';
2
- import type { Adapter, ApiCall, CallContext, ModuleReader, Problem } from '../model.ts';
2
+ import type { Adapter, ApiCall, CallContext, ModuleReader, Problem, Span } from '../model.ts';
3
3
  import { type FileContext } from './common.ts';
4
4
  export declare class PlaywrightAdapter implements Adapter {
5
5
  readonly problems: Problem[];
6
6
  readonly fileTags: string[];
7
+ readonly fileTagSpans: Span[];
7
8
  readonly usesGlobals = false;
8
9
  private readonly file;
9
10
  private readonly readModule;
@@ -1,7 +1,7 @@
1
1
  import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
3
  import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
4
- import { atLeast, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
4
+ import { atLeast, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, takesWhole, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
5
5
  const MISMATCHED_SOURCES = new Set(['vitest', '@jest/globals']);
6
6
  // Maps rather than object literals, so names such as `constructor` don't match inherited properties.
7
7
  const DESCRIBES = new Map([
@@ -201,6 +201,7 @@ const PLAYWRIGHT = {
201
201
  export class PlaywrightAdapter {
202
202
  problems;
203
203
  fileTags = [];
204
+ fileTagSpans = [];
204
205
  usesGlobals = false;
205
206
  file;
206
207
  readModule;
@@ -258,7 +259,14 @@ export class PlaywrightAdapter {
258
259
  return this.configure(call, context);
259
260
  if (HOOKS.has(key)) {
260
261
  const fn = hookFunction(this.file, context.scopes, call.arguments);
261
- return { kind: 'hook', name: key, fn, testContext: null, fixtures: destructuredNames(fn, 0) };
262
+ return {
263
+ kind: 'hook',
264
+ name: key,
265
+ fn,
266
+ testContext: null,
267
+ fixtures: destructuredNames(fn, 0),
268
+ takesAllFixtures: takesWhole(fn, 0),
269
+ };
262
270
  }
263
271
  if (RUN_MEMBERS.has(key))
264
272
  return this.runSetting(call, key, context);
@@ -474,6 +482,7 @@ export class PlaywrightAdapter {
474
482
  caseCount: null,
475
483
  testContext: null,
476
484
  fixtures: destructuredNames(hashed, 0),
485
+ takesAllFixtures: takesWhole(hashed, 0),
477
486
  usesBench: false,
478
487
  problems,
479
488
  };
@@ -1,5 +1,5 @@
1
1
  import type { CallExpression, Expression, Node } from 'oxc-parser';
2
- import type { Adapter, ApiCall, CallContext, ModuleReader, Problem } from '../model.ts';
2
+ import type { Adapter, ApiCall, CallContext, ModuleReader, Problem, Span } from '../model.ts';
3
3
  import type { ParsedFile } from '../parse.ts';
4
4
  import { type FileContext, type Link } from './common.ts';
5
5
  type Kind = 'test' | 'describe';
@@ -11,6 +11,7 @@ interface TestFunction {
11
11
  export declare class VitestAdapter implements Adapter {
12
12
  readonly problems: Problem[];
13
13
  readonly fileTags: string[];
14
+ readonly fileTagSpans: Span[];
14
15
  usesGlobals: boolean;
15
16
  private readonly file;
16
17
  private readonly readModule;
@@ -1,7 +1,7 @@
1
1
  import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
3
  import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
4
- import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
4
+ import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, takesWhole, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
5
5
  /** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
6
6
  function shared(a, b) {
7
7
  if (!a || a.kind !== b?.kind)
@@ -256,6 +256,7 @@ class VitestBindings {
256
256
  export class VitestAdapter {
257
257
  problems;
258
258
  fileTags = [];
259
+ fileTagSpans = [];
259
260
  usesGlobals = false;
260
261
  file;
261
262
  readModule;
@@ -293,8 +294,10 @@ export class VitestAdapter {
293
294
  if (atLeast(file.version, 4, 1)) {
294
295
  // Vitest's own pattern, over the whole text as Vitest reads it, so the same tags come out.
295
296
  for (const match of parsed.source.text.matchAll(/(\/\/|\*)\s*@module-tag\s+([\w\-/]+)\b/g)) {
297
+ const span = { start: match.index, end: match.index + match[0].length };
296
298
  this.fileTags.push(match[2]);
297
- this.tagsUsed({ start: match.index, end: match.index + match[0].length });
299
+ this.fileTagSpans.push(span);
300
+ this.tagsUsed(span);
298
301
  }
299
302
  }
300
303
  }
@@ -525,6 +528,7 @@ export class VitestAdapter {
525
528
  fn,
526
529
  testContext: index === null ? null : contextNames(fn, index),
527
530
  fixtures: fixturesOf(fn, index, this.file.version),
531
+ takesAllFixtures: takesWhole(fn, index),
528
532
  };
529
533
  }
530
534
  contextSkip(call, context) {
@@ -673,6 +677,7 @@ export class VitestAdapter {
673
677
  states,
674
678
  isOnly: flags.has('only'),
675
679
  mode,
680
+ ...(skips ? { skipPassedDown: passed.line } : {}),
676
681
  details: read.details,
677
682
  fn,
678
683
  hashed: fn ?? namedFunction(this.file, context.scopes, body),
@@ -697,6 +702,7 @@ export class VitestAdapter {
697
702
  ...base,
698
703
  testContext: contextNames(base.hashed, index),
699
704
  fixtures: fixturesOf(base.hashed, index, this.file.version),
705
+ takesAllFixtures: takesWhole(base.hashed, index),
700
706
  usesBench: atLeast(this.file.version, 5, 0) && destructuredNames(base.hashed, index).includes('bench'),
701
707
  };
702
708
  }
@@ -871,7 +877,7 @@ export class VitestAdapter {
871
877
  if (this.warnedAboutTags)
872
878
  return;
873
879
  this.warnedAboutTags = true;
874
- this.problems.push(problem('config-may-change-state', node, 'The file uses Vitest tags, and tags defined in the Vitest config can skip tests or change how they run; the config is not read.'));
880
+ this.problems.push(problem('config-may-change-state', node, "The file uses Vitest tags, and tags defined in the Vitest config can skip tests or change how they run; the scan can't work out the config's tags."));
875
881
  }
876
882
  }
877
883
  /** Rows in a tagged-template table such as ``each`a | b ${1} | ${2}` ``: values divided by header columns. */
package/dist/hash.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Node } from 'oxc-parser';
2
2
  import type { Span } from './model.ts';
3
- export declare function shortHash(input: string): string;
3
+ export declare function shortHash(input: string | Uint8Array): string;
4
4
  /**
5
5
  * Hashes code by its syntax tree, so formatting, comments, quote style, trailing commas and line endings don't change
6
6
  * the hash. Nodes at the `cut` spans, such as the tests inside a describe, are left out. Returns null when no code is
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export { scanSource } from './scan-source.ts';
3
3
  export { filterInventory, pickTestFields } from './filter.ts';
4
4
  export type { InventoryFilter, PickedInventory } from './filter.ts';
5
5
  export type { CommentsMode, ScanOptions, ScanSourceOptions } from './options.ts';
6
- export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, FileRecord, Framework, HookCounts, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
6
+ export type { Annotation, ConfigRecord, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, FileRecord, Framework, HookCounts, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
@@ -1,3 +1,3 @@
1
1
  import type { FileRecords } from './records.ts';
2
- import type { Diagnostic, Framework, Inventory } from './types.ts';
3
- export declare function createInventory(framework: Framework, frameworkVersion: string | null, fileCount: number, files: readonly FileRecords[], projectDiagnostics: readonly Diagnostic[]): Inventory;
2
+ import type { ConfigRecord, Diagnostic, Framework, Inventory } from './types.ts';
3
+ export declare function createInventory(framework: Framework, frameworkVersion: string | null, fileCount: number, files: readonly FileRecords[], projectDiagnostics: readonly Diagnostic[], configs: ConfigRecord[]): Inventory;
package/dist/inventory.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { summarize } from "./summary.js";
2
2
  import { toolVersion } from "./version.js";
3
- export function createInventory(framework, frameworkVersion, fileCount, files, projectDiagnostics) {
3
+ export function createInventory(framework, frameworkVersion, fileCount, files, projectDiagnostics, configs) {
4
4
  const suites = files.flatMap((file) => file.suites);
5
5
  const tests = files.flatMap((file) => file.tests);
6
6
  const diagnostics = [...projectDiagnostics, ...files.flatMap((file) => file.diagnostics)];
@@ -10,6 +10,7 @@ export function createInventory(framework, frameworkVersion, fileCount, files, p
10
10
  framework,
11
11
  frameworkVersion,
12
12
  summary: summarize(fileCount, suites, tests, diagnostics),
13
+ configs,
13
14
  files: files.map((file) => file.file),
14
15
  suites,
15
16
  tests,
package/dist/json.js CHANGED
@@ -48,6 +48,8 @@ export async function writeInventoryLines(inventory, write) {
48
48
  const line = (type, record) => emit(`${JSON.stringify({ type, ...record })}\n`);
49
49
  const { schemaVersion, tool, framework, frameworkVersion } = inventory;
50
50
  await line('inventory', { schemaVersion, tool, framework, frameworkVersion });
51
+ for (const config of inventory.configs)
52
+ await line('config', config);
51
53
  for (const file of inventory.files)
52
54
  await line('file', file);
53
55
  for (const suite of inventory.suites)
package/dist/model.d.ts CHANGED
@@ -11,7 +11,8 @@ export interface StateSetting {
11
11
  reason: string | null;
12
12
  /** The reason could not be worked out to a string, so `reason` holds its source text. */
13
13
  hasDynamicReason: boolean;
14
- line: number;
14
+ /** The line in the file that sets the state; null for a state from the config. */
15
+ line: number | null;
15
16
  /**
16
17
  * Set by code in a test or hook body, which runs only when the test does: a state set while the file loads, on the
17
18
  * test or a describe around it, keeps the test from getting there.
@@ -67,6 +68,8 @@ interface Declaration {
67
68
  states: StateSetting[];
68
69
  isOnly: boolean;
69
70
  mode: Mode | null;
71
+ /** Vitest: the line of the describe whose `skip` option reaches this declaration, merged into its options. */
72
+ skipPassedDown?: number;
70
73
  details: Details;
71
74
  fn: FunctionNode | null;
72
75
  /** The function whose code is hashed: `fn`, or a function declared in the file and passed by name. */
@@ -83,6 +86,8 @@ export interface TestDeclaration extends Declaration {
83
86
  testContext: unknown;
84
87
  /** Names the test function takes from its context, without the framework's own keys. */
85
88
  fixtures: string[];
89
+ /** The test function takes its fixtures whole, or keeps the rest of them, so it may use any. */
90
+ takesAllFixtures: boolean;
86
91
  /** Uses Vitest 5's `bench` fixture. */
87
92
  usesBench: boolean;
88
93
  }
@@ -125,6 +130,7 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
125
130
  fn: FunctionNode | null;
126
131
  testContext: unknown;
127
132
  fixtures: string[];
133
+ takesAllFixtures: boolean;
128
134
  }
129
135
  /** Playwright `test.step()` or `test.step.skip()`. */
130
136
  | {
@@ -193,6 +199,8 @@ export interface Adapter {
193
199
  problems: Problem[];
194
200
  /** Tags that apply to every test in the file. */
195
201
  fileTags: string[];
202
+ /** Where each of `fileTags` is written. */
203
+ fileTagSpans: Span[];
196
204
  usesGlobals: boolean;
197
205
  classify(call: CallExpression, context: CallContext): ApiCall | null;
198
206
  /**
package/dist/options.d.ts CHANGED
@@ -5,9 +5,9 @@ export type { CommentsMode };
5
5
  export interface ScanOptions {
6
6
  /**
7
7
  * Glob patterns for the test files, relative to `root`. As in Playwright and Vitest, they match names that start with
8
- * a dot too.
8
+ * a dot too. Without them, the test files are those the configs in the root and the folders below it run.
9
9
  */
10
- patterns: string[];
10
+ patterns?: string[];
11
11
  framework: Framework;
12
12
  /** The project root. Paths in the output are relative to it. Defaults to the current directory. */
13
13
  root?: string;