test-inventory 0.4.0 → 0.5.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 (45) hide show
  1. package/CHANGELOG.md +85 -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/cli.d.ts +1 -1
  6. package/dist/cli.js +4 -5
  7. package/dist/config-runs.d.ts +97 -0
  8. package/dist/config-runs.js +784 -0
  9. package/dist/config.d.ts +90 -0
  10. package/dist/config.js +652 -0
  11. package/dist/diagnostics.js +5 -1
  12. package/dist/extract.d.ts +8 -0
  13. package/dist/extract.js +14 -1
  14. package/dist/filter.js +7 -0
  15. package/dist/frameworks/bindings.d.ts +22 -0
  16. package/dist/frameworks/bindings.js +19 -9
  17. package/dist/frameworks/common.d.ts +2 -0
  18. package/dist/frameworks/common.js +4 -0
  19. package/dist/frameworks/playwright.d.ts +2 -1
  20. package/dist/frameworks/playwright.js +3 -2
  21. package/dist/frameworks/vitest.d.ts +2 -1
  22. package/dist/frameworks/vitest.js +11 -7
  23. package/dist/index.d.ts +1 -1
  24. package/dist/inventory.d.ts +2 -2
  25. package/dist/inventory.js +2 -1
  26. package/dist/json.js +2 -0
  27. package/dist/model.d.ts +6 -1
  28. package/dist/options.d.ts +2 -2
  29. package/dist/records.d.ts +54 -1
  30. package/dist/records.js +192 -28
  31. package/dist/resolve.js +3 -3
  32. package/dist/scan-file.d.ts +5 -1
  33. package/dist/scan-file.js +5 -2
  34. package/dist/scan-source.js +1 -1
  35. package/dist/scan.d.ts +2 -1
  36. package/dist/scan.js +63 -17
  37. package/dist/types.d.ts +73 -7
  38. package/dist/values.d.ts +5 -0
  39. package/dist/values.js +5 -0
  40. package/dist/vitest-config.d.ts +6 -4
  41. package/dist/vitest-config.js +32 -28
  42. package/dist/vitest-defaults.d.ts +6 -0
  43. package/dist/vitest-defaults.js +16 -0
  44. package/package.json +10 -8
  45. package/schema/v1.json +180 -6
package/dist/extract.d.ts CHANGED
@@ -37,6 +37,9 @@ export interface PendingSuite {
37
37
  order: number;
38
38
  /** Statements of the tests and describes directly inside, left out of the describe's body hash. */
39
39
  cuts: Span[];
40
+ /** Declared under a condition, or in a function: it may not run while the file loads. */
41
+ isInCondition: boolean;
42
+ isInFunction: boolean;
40
43
  }
41
44
  export interface PendingTest {
42
45
  scope: Scope;
@@ -80,6 +83,11 @@ export interface Extraction {
80
83
  /** Hooks at the top level of the file. */
81
84
  hooks: HookCounts;
82
85
  usesGlobals: boolean;
86
+ /** The file's `@module-tag` tags, where each is written. */
87
+ fileTags: {
88
+ name: string;
89
+ span: Span;
90
+ }[];
83
91
  }
84
92
  /**
85
93
  * 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
@@ -110,7 +110,10 @@ class Walker {
110
110
  declaredIn = new Map();
111
111
  deferred = [];
112
112
  isDeferred = new Set();
113
- /** Functions walked at one of their calls, so that their tests are recorded where they are registered. */
113
+ /**
114
+ * Functions walked at one of their calls, or as a test or hook body passed by name, so that their tests are recorded
115
+ * where they are registered and what they set isn't reported as unapplied.
116
+ */
114
117
  walkedAtCall = new Set();
115
118
  /** The functions being walked at their calls, innermost last, to stop recursion. */
116
119
  callStack = [];
@@ -172,6 +175,10 @@ class Walker {
172
175
  problems: [...this.main.adapter.problems, ...this.problems],
173
176
  hooks: this.fileScope.hooks,
174
177
  usesGlobals: this.main.adapter.usesGlobals,
178
+ fileTags: this.main.adapter.fileTags.map((name, index) => ({
179
+ name,
180
+ span: this.main.adapter.fileTagSpans[index],
181
+ })),
175
182
  };
176
183
  }
177
184
  visit(node, context) {
@@ -413,6 +420,8 @@ class Walker {
413
420
  context.scope.hookFixtures.push(...api.fixtures);
414
421
  }
415
422
  if (api.fn) {
423
+ if (!call.arguments.includes(api.fn))
424
+ this.walkedAtCall.add(api.fn);
416
425
  this.visitCallback(api.fn, {
417
426
  ...context,
418
427
  hookScope: context.scope,
@@ -938,6 +947,8 @@ class Walker {
938
947
  // A function declared in the file and passed by name runs as the test body too.
939
948
  const body = declaration.fn ?? declaration.hashed;
940
949
  if (body) {
950
+ if (!declaration.fn)
951
+ this.walkedAtCall.add(body);
941
952
  this.visitCallback(body, {
942
953
  ...context,
943
954
  test,
@@ -975,6 +986,8 @@ class Walker {
975
986
  declaredAt: this.declaredAt(call),
976
987
  order: context.registeredAt ?? call.start,
977
988
  cuts: [],
989
+ isInCondition: context.isInCondition,
990
+ isInFunction: context.isInFunction,
978
991
  };
979
992
  scope.suite = suite;
980
993
  this.suites.push(suite);
package/dist/filter.js CHANGED
@@ -55,6 +55,12 @@ const TEST_KEYS = Object.keys({
55
55
  retryDelay: true,
56
56
  retryCondition: true,
57
57
  repeats: true,
58
+ timeoutSource: true,
59
+ retriesSource: true,
60
+ repeatsSource: true,
61
+ timeoutConfigText: true,
62
+ retriesConfigText: true,
63
+ repeatsConfigText: true,
58
64
  fixtures: true,
59
65
  expectCount: true,
60
66
  expectedAssertions: true,
@@ -67,6 +73,7 @@ const TEST_KEYS = Object.keys({
67
73
  isInLoop: true,
68
74
  isInCondition: true,
69
75
  isInFunction: true,
76
+ projects: true,
70
77
  });
71
78
  function list(value, option, allowed) {
72
79
  if (value === undefined)
@@ -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);
@@ -75,6 +75,8 @@ export declare function mayDeclareTests(name: string, links: Link[]): boolean;
75
75
  /** A function declared in the file and passed by name, as in `test('fills an order', checkFill)`; null otherwise. */
76
76
  export declare function namedFunction(file: FileContext, scopes: readonly Node[], node: Argument | undefined): FunctionNode | null;
77
77
  export declare function findFunction(args: readonly Argument[], from?: number): FunctionNode | null;
78
+ /** A hook's function: written in the call, or declared in the file and passed by name, as in `beforeEach(signIn)`. */
79
+ export declare function hookFunction(file: FileContext, scopes: readonly Node[], args: readonly Argument[]): FunctionNode | null;
78
80
  /** Reads a title. A title that cannot be worked out keeps its source text and gets a `dynamic-title` warning. */
79
81
  export declare function readTitle(file: FileContext, scopes: readonly Node[], node: Argument, options: {
80
82
  stripTags: boolean;
@@ -166,6 +166,10 @@ export function findFunction(args, from = 0) {
166
166
  }
167
167
  return null;
168
168
  }
169
+ /** A hook's function: written in the call, or declared in the file and passed by name, as in `beforeEach(signIn)`. */
170
+ export function hookFunction(file, scopes, args) {
171
+ return findFunction(args) ?? args.map((arg) => namedFunction(file, scopes, arg)).find((fn) => fn !== null) ?? null;
172
+ }
169
173
  /** Reads a title. A title that cannot be worked out keeps its source text and gets a `dynamic-title` warning. */
170
174
  export function readTitle(file, scopes, node, options) {
171
175
  const expression = node.type === 'SpreadElement' ? null : node;
@@ -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, 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, 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;
@@ -257,7 +258,7 @@ export class PlaywrightAdapter {
257
258
  if (key === 'describe.configure')
258
259
  return this.configure(call, context);
259
260
  if (HOOKS.has(key)) {
260
- const fn = findFunction(call.arguments);
261
+ const fn = hookFunction(this.file, context.scopes, call.arguments);
261
262
  return { kind: 'hook', name: key, fn, testContext: null, fixtures: destructuredNames(fn, 0) };
262
263
  }
263
264
  if (RUN_MEMBERS.has(key))
@@ -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, findFunction, 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, 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
  }
@@ -328,7 +331,7 @@ export class VitestAdapter {
328
331
  return null;
329
332
  const { kind, links } = resolved;
330
333
  if (kind === 'test' && links.length === 1 && isHook(links[0].name))
331
- return this.hook(call, links[0].name);
334
+ return this.hook(call, links[0].name, context);
332
335
  if (links.length === 1 && DERIVATIONS.has(links[0].name))
333
336
  return { kind: 'other' };
334
337
  const last = links.at(-1);
@@ -514,10 +517,10 @@ export class VitestAdapter {
514
517
  const hook = declaration === null
515
518
  ? isHook(callee.name) && callee.name
516
519
  : this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
517
- return hook ? this.hook(call, hook) : null;
520
+ return hook ? this.hook(call, hook, context) : null;
518
521
  }
519
- hook(call, name) {
520
- const fn = findFunction(call.arguments);
522
+ hook(call, name, context) {
523
+ const fn = hookFunction(this.file, context.scopes, call.arguments);
521
524
  const index = HOOKS.get(name);
522
525
  return {
523
526
  kind: 'hook',
@@ -673,6 +676,7 @@ export class VitestAdapter {
673
676
  states,
674
677
  isOnly: flags.has('only'),
675
678
  mode,
679
+ ...(skips ? { skipPassedDown: passed.line } : {}),
676
680
  details: read.details,
677
681
  fn,
678
682
  hashed: fn ?? namedFunction(this.file, context.scopes, body),
@@ -871,7 +875,7 @@ export class VitestAdapter {
871
875
  if (this.warnedAboutTags)
872
876
  return;
873
877
  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.'));
878
+ 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
879
  }
876
880
  }
877
881
  /** Rows in a tagged-template table such as ``each`a | b ${1} | ${2}` ``: values divided by header columns. */
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. */
@@ -193,6 +196,8 @@ export interface Adapter {
193
196
  problems: Problem[];
194
197
  /** Tags that apply to every test in the file. */
195
198
  fileTags: string[];
199
+ /** Where each of `fileTags` is written. */
200
+ fileTagSpans: Span[];
196
201
  usesGlobals: boolean;
197
202
  classify(call: CallExpression, context: CallContext): ApiCall | null;
198
203
  /**
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;
package/dist/records.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type Extraction } from './extract.ts';
2
+ import type { RunSettings } from './model.ts';
2
3
  import type { ParsedFile } from './parse.ts';
3
- import type { Diagnostic, FileRecord, Framework, HookCounts, SuiteRecord, TestRecord } from './types.ts';
4
+ import type { Diagnostic, FileRecord, Framework, HookCounts, MetaValue, SuiteRecord, TestRecord } from './types.ts';
4
5
  /** @inline */
5
6
  export type CommentsMode = 'all' | 'non-active' | 'none';
6
7
  export interface FileRecords {
@@ -9,6 +10,57 @@ export interface FileRecords {
9
10
  tests: TestRecord[];
10
11
  diagnostics: Diagnostic[];
11
12
  }
13
+ /** What the config the file uses sets for its tests, where the file sets nothing. */
14
+ export interface ConfigDefaults {
15
+ /** A run setting the config sets; null when its projects set different values or it depends on a condition. */
16
+ timeout?: number | null;
17
+ retry?: RunSettings['retry'] | null;
18
+ repeats?: number | null;
19
+ /** The source text of a run setting the config sets under a condition; null for any other. */
20
+ timeoutText: string | null;
21
+ retryText: string | null;
22
+ repeatsText: string | null;
23
+ /** Whether tests run in parallel unless the file says otherwise: Playwright's `fullyParallel`, Vitest's `sequence.concurrent`. */
24
+ parallel: boolean;
25
+ }
26
+ /** What a Vitest tag the config defines sets on the tests that carry it. */
27
+ export interface TagOptions {
28
+ skip?: boolean;
29
+ only?: boolean;
30
+ todo?: boolean;
31
+ fails?: boolean;
32
+ concurrent?: boolean;
33
+ run: RunSettings;
34
+ meta?: Record<string, MetaValue>;
35
+ }
36
+ export interface TagDefinition {
37
+ name: string;
38
+ /** Lower numbers win when two tags set the same option; a tag without one loses to every tag with one. */
39
+ priority: number | null;
40
+ options: TagOptions;
41
+ }
42
+ /** The tags the Vitest config defines (Vitest 4.1). */
43
+ export interface VitestTags {
44
+ definitions: TagDefinition[];
45
+ /** `strictTags`: a tag the config doesn't define fails the file while it loads. */
46
+ isStrict: boolean;
47
+ /** From Vitest 5 a tag's options win over the describe's around the test; before, the describe's win. */
48
+ beatsDescribes: boolean;
49
+ /** Vitest 4.1 passes the config's `sequence.concurrent`, when it sets one, to every test, over a tag's `concurrent`. */
50
+ concurrentBeatsTags: boolean;
51
+ }
52
+ /** What the config the file uses gives it. */
53
+ export interface FileConfig {
54
+ /** Whether a `.only` fails the run: Playwright's `forbidOnly`, always, never, or under the condition with this text. */
55
+ forbidOnly: boolean | string;
56
+ /** The names of the projects that run a test, null when unknown, and what they set for it. */
57
+ forTest(test: TestRecord, suites: SuiteRecord[]): {
58
+ projects: string[] | null;
59
+ defaults: ConfigDefaults | null;
60
+ };
61
+ /** Vitest's tags; null when the scan can't work them out, or the projects that run the file define different ones. */
62
+ tags: VitestTags | null;
63
+ }
12
64
  interface RecordOptions {
13
65
  relativeFilePath: string;
14
66
  framework: Framework;
@@ -16,6 +68,7 @@ interface RecordOptions {
16
68
  isTypeTest: boolean;
17
69
  /** A Vitest 5 benchmark file, such as `*.bench.ts`. */
18
70
  isBenchmarkFile: boolean;
71
+ config: FileConfig | null;
19
72
  }
20
73
  /** The record of a scanned file, from its records and the hooks at its top level. */
21
74
  export declare function fileRecord(relativeFilePath: string, hooks: HookCounts, records: Pick<FileRecords, 'suites' | 'tests'>): FileRecord;