test-inventory 0.2.1 → 0.3.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.
@@ -1,7 +1,7 @@
1
1
  import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
- import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, requiredModule, } from "./bindings.js";
4
- import { atLeast, findFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, text, } from "./common.js";
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, 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)
@@ -37,6 +37,8 @@ const HOOKS = new Map([
37
37
  ['afterAll', null],
38
38
  ['aroundAll', null],
39
39
  ]);
40
+ /** First members of chains that never declare a test, even when Vitest refuses the rest of the chain. */
41
+ const NOT_DECLARING = new Set([...HOOKS.keys(), ...DERIVATIONS]);
40
42
  /** First members that show a name imported from a local file is used as Vitest's `test`. */
41
43
  const TEST_MEMBERS = new Set([...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS, ...KINDS.keys()]);
42
44
  /** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
@@ -102,9 +104,12 @@ class VitestBindings {
102
104
  /** Names imported from other modules, which may be test objects there. */
103
105
  imported;
104
106
  follow;
107
+ /** The module's functions and the variables they may build test objects in; see `ModuleFunctions`. */
108
+ functions;
105
109
  constructor(program, version, follow) {
106
110
  this.imported = moduleImports(program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
107
111
  this.follow = follow;
112
+ this.functions = new ModuleFunctions(program, this.imported, follow);
108
113
  for (const statement of program.body) {
109
114
  if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
110
115
  continue;
@@ -139,30 +144,49 @@ class VitestBindings {
139
144
  this.declarations.add(declarator);
140
145
  return bound;
141
146
  });
147
+ this.functions.keep(declarators.filter((declarator) => !this.declarations.has(declarator)));
142
148
  }
143
149
  isTestName(name) {
144
- return this.kinds.has(name) || this.importsTestObject(name);
150
+ return this.kinds.has(name) || this.importsTestObject(name) || this.holdsTestObject(name);
145
151
  }
146
- isTestObject(expression) {
152
+ /**
153
+ * `followsCalls` is cleared where a call is not followed to what it returns: while the names are bound, so that only
154
+ * a value asked about is followed, as following reads other modules, and in the scanned file, whose calls the walk
155
+ * follows in their scopes.
156
+ */
157
+ isTestObject(expression, followsCalls = true) {
147
158
  const node = unwrap(expression);
148
- if (node.type === 'Identifier')
149
- return this.kinds.get(node.name) === 'test' || this.importsTestObject(node.name);
159
+ if (node.type === 'Identifier') {
160
+ const { name } = node;
161
+ return (this.kinds.get(name) === 'test' || this.importsTestObject(name) || (followsCalls && this.holdsTestObject(name)));
162
+ }
150
163
  if (node.type === 'MemberExpression') {
151
164
  return KINDS.get(propertyName(node) ?? '') === 'test' && this.isNamespace(node.object);
152
165
  }
153
166
  // `process.env.CI ? vitestTest : skippedTest` is a test object either way.
154
167
  if (node.type === 'ConditionalExpression') {
155
- return this.isTestObject(node.consequent) || this.isTestObject(node.alternate);
168
+ return this.isTestObject(node.consequent, followsCalls) || this.isTestObject(node.alternate, followsCalls);
156
169
  }
157
170
  if (node.type !== 'CallExpression')
158
171
  return false;
159
172
  const callee = unwrap(node.callee);
173
+ // `defineBrokerTest()`, a function that returns a test object.
174
+ if (callee.type === 'Identifier')
175
+ return followsCalls && this.returnsTestObject(callee.name);
160
176
  return (callee.type === 'MemberExpression' &&
161
177
  DERIVATIONS.has(propertyName(callee) ?? '') &&
162
- this.isTestObject(callee.object));
178
+ this.isTestObject(callee.object, followsCalls));
179
+ }
180
+ returnsTestObject(name) {
181
+ return this.functions.returnsTestObject(name, (expression) => this.isTestObject(expression));
182
+ }
183
+ holdsTestObject(name) {
184
+ return this.functions.holdsTestObject(name, (expression) => this.isTestObject(expression));
163
185
  }
164
- reexports(source, imported) {
165
- return source === 'vitest' ? KINDS.has(imported) : this.follow(source, imported);
186
+ reexports(source, imported, exported) {
187
+ if (source !== 'vitest')
188
+ return this.follow(source, imported, exported);
189
+ return exported === 'test object' && KINDS.has(imported);
166
190
  }
167
191
  /** Whether a name is imported from another module that exports it as a test object, as `base` from './base'. */
168
192
  importsTestObject(name) {
@@ -189,7 +213,7 @@ class VitestBindings {
189
213
  const { id, init } = declarator;
190
214
  if (!init)
191
215
  return false;
192
- if (id.type === 'Identifier' && this.isTestObject(init)) {
216
+ if (id.type === 'Identifier' && this.isTestObject(init, false)) {
193
217
  this.kinds.set(id.name, 'test');
194
218
  return true;
195
219
  }
@@ -231,10 +255,11 @@ export class VitestAdapter {
231
255
  // A test object the file builds from an imported one, as in `const it = base.extend({...})`, is followed quietly:
232
256
  // a call through the import itself is what reports an import that can't be followed.
233
257
  const followed = new Map();
234
- this.bindings = new VitestBindings(parsed.program, file.version, (source, imported) => {
235
- const key = `${source}\0${imported}`;
236
- if (!followed.has(key))
237
- followed.set(key, followModule(source, imported, readModule, this.framework()).exported);
258
+ this.bindings = new VitestBindings(parsed.program, file.version, (source, imported, exported = 'test object') => {
259
+ const key = `${source}\0${exported}\0${imported}`;
260
+ if (!followed.has(key)) {
261
+ followed.set(key, followModule(source, imported, readModule, this.framework(), exported).exported);
262
+ }
238
263
  return followed.get(key) === true;
239
264
  });
240
265
  this.problems = [...this.bindings.problems];
@@ -259,8 +284,8 @@ export class VitestAdapter {
259
284
  this.contextSkip(call, context) ??
260
285
  this.chainCall(call, context));
261
286
  }
262
- testFunction(expression, scopes) {
263
- return this.aliasOf(expression, { scopes, inBody: false, returnsOf: null });
287
+ testFunction(expression, scopes, returnsOf) {
288
+ return this.aliasOf(expression, { scopes, inBody: false, returnsOf });
264
289
  }
265
290
  /** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
266
291
  benchCall(call, context) {
@@ -290,29 +315,24 @@ export class VitestAdapter {
290
315
  if (last && (TABLES.has(last.name) || CONDITIONS.has(last.name)) && !last.args && !last.template) {
291
316
  return { kind: 'other' };
292
317
  }
293
- for (const link of links) {
318
+ const callee = snippet(text(this.file, call.callee));
319
+ const removed = (link) => link.name === 'sequential' && atLeast(this.file.version, 5, 0);
320
+ const valid = (link) => {
294
321
  const called = link.args !== null || link.template !== null;
295
- const valid = MODIFIERS[kind].has(link.name)
296
- ? !called
297
- : TABLES.has(link.name)
298
- ? called
299
- : CONDITIONS.has(link.name) && link.args !== null;
300
- if (!valid) {
301
- const callee = snippet(text(this.file, call.callee));
302
- return {
303
- kind: 'invalid',
304
- problem: problem('invalid-chain', call, `${callee} is not part of Vitest's API.`, callee),
305
- };
306
- }
307
- if (link.name === 'sequential' && atLeast(this.file.version, 5, 0)) {
308
- const callee = snippet(text(this.file, call.callee));
309
- return {
310
- kind: 'invalid',
311
- problem: problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee),
312
- };
313
- }
314
- }
315
- return this.declaration(call, kind, links, context);
322
+ if (MODIFIERS[kind].has(link.name))
323
+ return !called && !removed(link);
324
+ return TABLES.has(link.name) ? called : CONDITIONS.has(link.name) && link.args !== null;
325
+ };
326
+ const invalid = links.find((link) => !valid(link));
327
+ if (!invalid)
328
+ return this.declaration(call, kind, links, context);
329
+ const refusal = removed(invalid)
330
+ ? problem('removed-api', call, `sequential was removed in Vitest 5; ${callee}(...) fails.`, callee)
331
+ : problem('invalid-chain', call, `${callee} is not part of Vitest's API.`, callee);
332
+ if (NOT_DECLARING.has(links[0].name))
333
+ return { kind: 'invalid', problem: refusal };
334
+ // Which of its parts the chain means is not known, so it declares a plain test or describe.
335
+ return refused(call, refusal, context, () => this.declaration(call, kind, [], context));
316
336
  }
317
337
  /**
318
338
  * The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
@@ -402,7 +422,7 @@ export class VitestAdapter {
402
422
  */
403
423
  returned(call, links, args, lookup) {
404
424
  const callee = unwrap(call.callee);
405
- if (callee.type !== 'Identifier' || !lookup.returnsOf)
425
+ if (callee.type !== 'Identifier')
406
426
  return null;
407
427
  const returns = lookup.returnsOf(callee.name, lookup.scopes);
408
428
  if (returns)
@@ -528,6 +548,7 @@ export class VitestAdapter {
528
548
  // (title, fn, timeout), (title, options, fn) or the removed (title, fn, options)
529
549
  let options;
530
550
  let body;
551
+ let refusal = null;
531
552
  const [second, third] = [args.at(1), args.at(2)];
532
553
  if (args.some((arg) => arg.type === 'SpreadElement')) {
533
554
  // With spread arguments, as in test(...args), the function and options are unknown.
@@ -540,29 +561,49 @@ export class VitestAdapter {
540
561
  body = second;
541
562
  if (third !== undefined && this.isOptions(third, undefined, context)) {
542
563
  if (atLeast(this.file.version, 4, 0)) {
543
- return {
544
- kind: 'invalid',
545
- problem: problem('removed-api', third, `${callee}() takes options as its second argument since Vitest 4; with options third the call fails.`, snippet(text(this.file, third))),
546
- };
564
+ refusal = problem('removed-api', third, `${callee}() takes options as its second argument since Vitest 4; with options third the call fails.`, snippet(text(this.file, third)));
547
565
  }
548
566
  options = third;
549
567
  }
550
568
  }
551
569
  const fn = isFunctionNode(body) ? body : null;
552
570
  const read = this.readDeclarationOptions(options, context, problems);
553
- const flags = new Set([
554
- ...links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name),
555
- ...read.flags,
556
- ]);
557
- const conditions = [
558
- ...links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context)),
559
- ...read.conditions,
560
- ];
571
+ const chain = new Set(links.filter((link) => MODIFIERS[kind].has(link.name)).map((link) => link.name));
572
+ const flags = new Set(chain);
573
+ let conditions = links.filter((link) => CONDITIONS.has(link.name)).map((link) => this.linkCondition(link, context));
574
+ // A test's own options win over its chain, so `test.skip('title', { skip: false }, fn)` runs. The chain wins for a
575
+ // test's mode, and for everything on a describe.
576
+ for (const [flag, value] of read.flags) {
577
+ if (value)
578
+ flags.add(flag);
579
+ else if (kind === 'test' && flag !== 'sequential')
580
+ flags.delete(flag);
581
+ }
582
+ if (read.skip && kind === 'test') {
583
+ flags.delete('skip');
584
+ conditions = [read.skip];
585
+ }
586
+ else if (read.skip) {
587
+ conditions.push(read.skip);
588
+ }
589
+ // Vitest passes a describe's options down to the tests and describes inside, where their own options and `only`
590
+ // win. A `skip` passed down skips them, and turns todo into skip, as skip goes before todo.
591
+ const line = this.file.source.line(call.start);
592
+ const around = context.passedDown;
593
+ const passed = read.skip ? null : around;
594
+ const skips = passed?.condition.applies === 'always' && !flags.has('only');
561
595
  const table = links.find((link) => TABLES.has(link.name)) ?? null;
562
596
  const isTodo = flags.has('todo') || body === undefined;
563
- const states = this.states(flags, conditions, isTodo, this.file.source.line(call.start));
564
- // `{ concurrent: false }` is how Vitest 5 turns parallel off, in place of `sequential`.
565
- const parallel = read.concurrent ?? flags.has('concurrent');
597
+ const states = this.states(flags, conditions, isTodo && !skips, line);
598
+ const skipped = () => states.some((setting) => setting.state === 'skip' && !setting.condition);
599
+ // The skip is set where the describe that passes it down sets it. A skipped describe around already skips what is
600
+ // inside, as it does when its own `only` beats its skip; a todo describe gets the skip itself.
601
+ if (skips && !skipped() && (!passed.covered || (kind === 'describe' && isTodo))) {
602
+ states.push({ state: 'skip', condition: null, reason: null, hasDynamicReason: false, line: passed.line });
603
+ }
604
+ // The chain's mode wins over the options'. `{ concurrent: false }` is how Vitest 5 turns parallel off, in place of
605
+ // `sequential`.
606
+ const parallel = chain.has('concurrent') || (!chain.has('sequential') && read.concurrent === true);
566
607
  const mode = parallel
567
608
  ? 'parallel'
568
609
  : flags.has('sequential') || read.concurrent === false
@@ -580,18 +621,20 @@ export class VitestAdapter {
580
621
  caseCount: table ? this.caseCount(call, table, context, problems) : null,
581
622
  problems,
582
623
  };
624
+ let declared;
583
625
  if (kind === 'describe') {
584
626
  // A table's cases are passed instead.
585
627
  const first = table ? null : fn?.params.at(0);
586
628
  if (fn && first?.type === 'Identifier')
587
629
  this.suiteParameters.set(fn, first.name);
588
- return { kind: 'describe', ...base, isShuffled: flags.has('shuffle') };
630
+ const own = read.skip ? { condition: read.skip, line } : around;
631
+ const passesDown = own && { ...own, covered: (around?.covered ?? false) || skipped() };
632
+ declared = { kind: 'describe', ...base, isShuffled: flags.has('shuffle'), passesDown };
589
633
  }
590
- return {
591
- kind: 'test',
592
- ...base,
593
- testContext: skipNames(fn, table?.name === 'for' ? 1 : table ? null : 0),
594
- };
634
+ else {
635
+ declared = { kind: 'test', ...base, testContext: skipNames(fn, table?.name === 'for' ? 1 : table ? null : 0) };
636
+ }
637
+ return refusal ? refused(call, refusal, context, () => declared) : declared;
595
638
  }
596
639
  /** Whether an argument holds options: an object, a name for one, or anything but a function before the body. */
597
640
  isOptions(node, next, context) {
@@ -639,9 +682,9 @@ export class VitestAdapter {
639
682
  readDeclarationOptions(node, context, problems) {
640
683
  const details = { tags: [], annotations: [], locks: [], meta: null };
641
684
  const result = {
642
- flags: [],
685
+ flags: new Map(),
643
686
  concurrent: null,
644
- conditions: [],
687
+ skip: null,
645
688
  details,
646
689
  };
647
690
  if (!node)
@@ -657,15 +700,15 @@ export class VitestAdapter {
657
700
  else if (option?.resolved.ok && flag === 'concurrent') {
658
701
  result.concurrent = Boolean(option.resolved.value);
659
702
  }
660
- else if (option?.resolved.ok && option.resolved.value) {
661
- result.flags.push(flag);
703
+ else if (option?.resolved.ok) {
704
+ result.flags.set(flag, Boolean(option.resolved.value));
662
705
  }
663
706
  }
664
707
  const skip = options.values.get('skip');
665
708
  if (skip) {
666
- result.conditions.push(skip.resolved.ok
709
+ result.skip = skip.resolved.ok
667
710
  ? { applies: skip.resolved.value ? 'always' : 'never', text: null }
668
- : { applies: 'sometimes', text: snippet(text(this.file, skip.node)) });
711
+ : { applies: 'sometimes', text: snippet(text(this.file, skip.node)) };
669
712
  }
670
713
  const tags = options.values.get('tags');
671
714
  if (tags && atLeast(this.file.version, 4, 1)) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { scan } from './scan.ts';
2
2
  export { scanSource } from './scan-source.ts';
3
+ export { filterInventory, pickTestFields } from './filter.ts';
4
+ export type { InventoryFilter, PickedInventory } from './filter.ts';
3
5
  export type { CommentsMode, ScanOptions, ScanSourceOptions } from './options.ts';
4
- export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, Framework, Inventory, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
6
+ export type { Annotation, DeclaredAt, Diagnostic, DiagnosticCode, DiagnosticLevel, Framework, Inventory, InventoryLine, MetaValue, StateSource, SuiteRecord, Summary, TestRecord, TestState, Tool, } from './types.ts';
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { scan } from "./scan.js";
2
2
  export { scanSource } from "./scan-source.js";
3
+ export { filterInventory, pickTestFields } from "./filter.js";
package/dist/json.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import type { Inventory } from './types.ts';
2
+ /** An inventory as the command line writes it, with every key of the test records or only the picked ones. */
3
+ export type Output = Omit<Inventory, 'tests'> & {
4
+ tests: object[];
5
+ };
2
6
  /**
3
7
  * Writes an inventory as JSON one record at a time, so a large inventory never becomes one huge string.
4
8
  * The text is exactly what `JSON.stringify(inventory, null, pretty ? 2 : undefined)` returns.
5
9
  */
6
- export declare function writeInventory(inventory: Inventory, pretty: boolean, write: (chunk: string) => Promise<void>): Promise<void>;
10
+ export declare function writeInventory(inventory: Output, pretty: boolean, write: (chunk: string) => Promise<void>): Promise<void>;
11
+ /** Writes an inventory as JSON Lines: one line per record, each marked by `type`, as `InventoryLine` describes. */
12
+ export declare function writeInventoryLines(inventory: Output, write: (chunk: string) => Promise<void>): Promise<void>;
package/dist/json.js CHANGED
@@ -1,18 +1,24 @@
1
1
  /** Collects small pieces into larger chunks, so a stream gets few writes. */
2
2
  const CHUNK_SIZE = 64 * 1024;
3
+ function chunked(write) {
4
+ let buffer = '';
5
+ return {
6
+ emit: async (text) => {
7
+ buffer += text;
8
+ if (buffer.length >= CHUNK_SIZE) {
9
+ await write(buffer);
10
+ buffer = '';
11
+ }
12
+ },
13
+ flush: () => write(buffer),
14
+ };
15
+ }
3
16
  /**
4
17
  * Writes an inventory as JSON one record at a time, so a large inventory never becomes one huge string.
5
18
  * The text is exactly what `JSON.stringify(inventory, null, pretty ? 2 : undefined)` returns.
6
19
  */
7
20
  export async function writeInventory(inventory, pretty, write) {
8
- let buffer = '';
9
- const emit = async (text) => {
10
- buffer += text;
11
- if (buffer.length >= CHUNK_SIZE) {
12
- await write(buffer);
13
- buffer = '';
14
- }
15
- };
21
+ const { emit, flush } = chunked(write);
16
22
  const stringify = (value, depth) => {
17
23
  const json = JSON.stringify(value, null, pretty ? 2 : undefined);
18
24
  return pretty ? json.replaceAll('\n', `\n${' '.repeat(depth)}`) : json;
@@ -34,5 +40,20 @@ export async function writeInventory(inventory, pretty, write) {
34
40
  }
35
41
  }
36
42
  await emit(`${newline(0)}}`);
37
- await write(buffer);
43
+ await flush();
44
+ }
45
+ /** Writes an inventory as JSON Lines: one line per record, each marked by `type`, as `InventoryLine` describes. */
46
+ export async function writeInventoryLines(inventory, write) {
47
+ const { emit, flush } = chunked(write);
48
+ const line = (type, record) => emit(`${JSON.stringify({ type, ...record })}\n`);
49
+ const { schemaVersion, tool, framework, frameworkVersion } = inventory;
50
+ await line('inventory', { schemaVersion, tool, framework, frameworkVersion });
51
+ for (const suite of inventory.suites)
52
+ await line('suite', suite);
53
+ for (const test of inventory.tests)
54
+ await line('test', test);
55
+ for (const diagnostic of inventory.diagnostics)
56
+ await line('diagnostic', diagnostic);
57
+ await line('summary', inventory.summary);
58
+ await flush();
38
59
  }
package/dist/model.d.ts CHANGED
@@ -12,6 +12,11 @@ export interface StateSetting {
12
12
  /** The reason could not be worked out to a string, so `reason` holds its source text. */
13
13
  hasDynamicReason: boolean;
14
14
  line: number;
15
+ /**
16
+ * 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
+ * test or a describe around it, keeps the test from getting there.
18
+ */
19
+ afterLoad?: boolean;
15
20
  }
16
21
  export interface Span {
17
22
  start: number;
@@ -64,6 +69,8 @@ export interface TestDeclaration extends Declaration {
64
69
  export interface SuiteDeclaration extends Declaration {
65
70
  kind: 'describe';
66
71
  isShuffled: boolean;
72
+ /** What the describe passes down to the calls inside it, such as Vitest's `skip` option; null for nothing. */
73
+ passesDown: unknown;
67
74
  }
68
75
  export interface Condition {
69
76
  /** `always` and `never` are conditions that could be worked out from the file. */
@@ -92,9 +99,15 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
92
99
  kind: 'annotations';
93
100
  items: Annotation[];
94
101
  problems: Problem[];
95
- } | {
102
+ }
103
+ /**
104
+ * A call the runner refuses. When it is shaped like a declaration, as `describe.sequential('title', fn)` is in Vitest
105
+ * 5, `declaration` is what it would declare, so that its tests are listed as not loaded rather than missing.
106
+ */
107
+ | {
96
108
  kind: 'invalid';
97
109
  problem: Problem;
110
+ declaration?: TestDeclaration | SuiteDeclaration;
98
111
  } | {
99
112
  kind: 'other';
100
113
  };
@@ -105,6 +118,11 @@ export interface CallContext {
105
118
  inTest: boolean;
106
119
  /** Inside a test or hook body, where calls don't declare tests. */
107
120
  inBody: boolean;
121
+ /**
122
+ * What the describe the call is registered in passes down, as the framework returned it: the describe around a
123
+ * helper's call, not where the helper is written. Null outside any describe.
124
+ */
125
+ passedDown: unknown;
108
126
  /**
109
127
  * What a function declared in the file, or imported from a local module, returns, for a call on its result such as
110
128
  * `conditionalTest({ min: 22 })('title', fn)`; null when the name is not a function the scan can find.
@@ -119,6 +137,8 @@ export interface Returns {
119
137
  expression: Expression;
120
138
  scopes: readonly Node[];
121
139
  }[] | null;
140
+ /** What the functions the values call return, looked up in the file that writes the function. */
141
+ returnsOf: CallContext['returnsOf'];
122
142
  }
123
143
  /** A module read to see whether it exports a custom `test` object. */
124
144
  export interface ModuleSource {
@@ -140,8 +160,8 @@ export interface Adapter {
140
160
  classify(call: CallExpression, context: CallContext): ApiCall | null;
141
161
  /**
142
162
  * The test or describe function a value holds, in the adapter's own terms, as `it.skip` in `return it.skip`; null
143
- * when it holds none. A function the value calls is not followed to what it returns.
163
+ * when it holds none. A function the value calls is followed to what it returns through `returnsOf`.
144
164
  */
145
- testFunction(expression: Expression, scopes: readonly Node[]): unknown;
165
+ testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): unknown;
146
166
  }
147
167
  export {};
package/dist/records.js CHANGED
@@ -2,12 +2,14 @@ import { REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
2
2
  import { problem } from "./frameworks/common.js";
3
3
  import { hashNodes, shortHash } from "./hash.js";
4
4
  /**
5
- * The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo
6
- * wins over an unconditional expected failure, since the test doesn't run; then the nearest conditional state.
5
+ * The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo wins
6
+ * over an unconditional expected failure, since the test doesn't run, and one set while the file loads wins over one
7
+ * in a test or hook body, which then never runs; then the nearest conditional state.
7
8
  */
8
9
  function chooseState(candidates) {
9
10
  const always = candidates.filter((candidate) => candidate.setting.condition === null);
10
- return (always.find((candidate) => candidate.setting.state !== 'expectedToFail') ?? always.at(0) ?? candidates.at(0) ?? null);
11
+ const stops = always.filter((candidate) => candidate.setting.state !== 'expectedToFail');
12
+ return (stops.find((candidate) => !candidate.setting.afterLoad) ?? stops.at(0) ?? always.at(0) ?? candidates.at(0) ?? null);
11
13
  }
12
14
  /** The scope and the scopes around it, innermost first. */
13
15
  function outward(scope) {
package/dist/types.d.ts CHANGED
@@ -49,6 +49,22 @@ export interface Inventory {
49
49
  tests: TestRecord[];
50
50
  diagnostics: Diagnostic[];
51
51
  }
52
+ /**
53
+ * One line of the JSON Lines output (`--format jsonl`): the inventory with each record on its own line, marked by
54
+ * `type`. The `inventory` line comes first, then the suites, tests and diagnostics in the order of the JSON output,
55
+ * and the `summary` line last.
56
+ */
57
+ export type InventoryLine = ({
58
+ type: 'inventory';
59
+ } & Pick<Inventory, 'schemaVersion' | 'tool' | 'framework' | 'frameworkVersion'>) | ({
60
+ type: 'suite';
61
+ } & SuiteRecord) | ({
62
+ type: 'test';
63
+ } & TestRecord) | ({
64
+ type: 'diagnostic';
65
+ } & Diagnostic) | ({
66
+ type: 'summary';
67
+ } & Summary);
52
68
  /** @inline */
53
69
  export interface Tool {
54
70
  name: 'test-inventory';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-inventory",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Test metadata from source code, not from test results.",
5
5
  "keywords": [
6
6
  "testing",
@@ -71,7 +71,7 @@
71
71
  "lint": "eslint .",
72
72
  "lint:fix": "eslint . --fix",
73
73
  "format": "prettier --write . --ignore-path .gitignore --ignore-path .prettierignore --ignore-path .git/info/exclude",
74
- "format:check": "prettier --check . --ignore-path .gitignore --ignore-path .prettierignore --ignore-path .git/info/exclude",
74
+ "format:check": "node scripts/format-check.ts",
75
75
  "docs:api": "typedoc",
76
76
  "docs:dev": "npm run docs:api && vitepress dev docs",
77
77
  "docs:build": "npm run docs:api && vitepress build docs",