test-inventory 0.6.1 → 0.8.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 (49) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +4 -3
  3. package/dist/case-titles.d.ts +27 -0
  4. package/dist/case-titles.js +329 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +6 -5
  7. package/dist/config-dependencies.js +8 -3
  8. package/dist/config-runs.d.ts +22 -0
  9. package/dist/config-runs.js +263 -3
  10. package/dist/config.d.ts +18 -1
  11. package/dist/config.js +160 -14
  12. package/dist/dependency-hash.d.ts +13 -5
  13. package/dist/dependency-hash.js +56 -12
  14. package/dist/diagnostics.js +3 -0
  15. package/dist/extract.d.ts +19 -1
  16. package/dist/extract.js +391 -41
  17. package/dist/files.js +3 -2
  18. package/dist/filter.js +1 -0
  19. package/dist/framework-version.d.ts +13 -2
  20. package/dist/framework-version.js +64 -5
  21. package/dist/frameworks/bindings.d.ts +14 -0
  22. package/dist/frameworks/bindings.js +99 -6
  23. package/dist/frameworks/common.d.ts +6 -0
  24. package/dist/frameworks/common.js +27 -0
  25. package/dist/frameworks/jest.d.ts +46 -0
  26. package/dist/frameworks/jest.js +442 -0
  27. package/dist/frameworks/playwright.d.ts +3 -1
  28. package/dist/frameworks/playwright.js +14 -5
  29. package/dist/frameworks/test-functions.d.ts +169 -0
  30. package/dist/frameworks/test-functions.js +492 -0
  31. package/dist/frameworks/vitest.d.ts +13 -58
  32. package/dist/frameworks/vitest.js +84 -378
  33. package/dist/imported-values.d.ts +22 -0
  34. package/dist/imported-values.js +149 -0
  35. package/dist/model.d.ts +11 -0
  36. package/dist/options.d.ts +16 -1
  37. package/dist/options.js +9 -2
  38. package/dist/parse.js +17 -2
  39. package/dist/records.js +57 -15
  40. package/dist/resolve.js +39 -36
  41. package/dist/scan-file.d.ts +5 -0
  42. package/dist/scan-file.js +15 -8
  43. package/dist/scan-source.js +3 -1
  44. package/dist/scan.js +31 -10
  45. package/dist/types.d.ts +24 -6
  46. package/dist/values.d.ts +49 -1
  47. package/dist/values.js +531 -19
  48. package/package.json +1 -1
  49. package/schema/v1.json +33 -4
@@ -3,7 +3,7 @@ import type { ConfigGap, RunsWith } from './config-dependencies.ts';
3
3
  import type { Extraction, Frame, PendingTest } from './extract.ts';
4
4
  import type { ModuleReader, ModuleSource, Problem, Span } from './model.ts';
5
5
  import { type ParsedFile } from './parse.ts';
6
- import { type SourceText } from './source.ts';
6
+ import { SourceText } from './source.ts';
7
7
  import type { Framework } from './types.ts';
8
8
  import { Values } from './values.ts';
9
9
  /** A file whose code a test can run: the scanned file, a module it imports, or a data file such as JSON. */
@@ -96,6 +96,14 @@ interface Reach {
96
96
  asks: string[] | null;
97
97
  snapshots: SnapshotUse[];
98
98
  }
99
+ /** What several reaches cover together, by file. */
100
+ interface Merged {
101
+ files: string[];
102
+ /** Each file's hashes, sorted. */
103
+ byFile: Map<string, string[]>;
104
+ /** Each file's part of the text the dependency hash is taken of. */
105
+ parts: Map<string, string>;
106
+ }
99
107
  /** A fixture one `extend()` call defines, and the fixtures it asks for; null when it takes them whole. */
100
108
  interface Fixture {
101
109
  unit: Unit;
@@ -145,11 +153,11 @@ export declare class DependencyGraph {
145
153
  /** The units a set of roots reaches, leaving out those `skip` reached, with their hashes by file. */
146
154
  reach(roots: Unit[], skip?: Reach[]): Reach;
147
155
  /**
148
- * Everything a test's roots reach on top of `base`, the code its file runs when loaded, which all its tests share,
149
- * with the fixtures it needs of each test object reached: the hash, the files, and whether all of it could be
150
- * followed.
156
+ * Everything a test's roots reach on top of `bases`, such as the code its file runs when loaded, which all its tests
157
+ * share, with the fixtures it needs of each test object reached: the hash, the files, and whether all of it could be
158
+ * followed. `shared` is what the bases cover, put together once for the many tests that share them.
151
159
  */
152
- dependencies(roots: Unit[], bases: Reach[], needs: FixtureNeeds, snapshots: (uses: SnapshotUse[]) => Unit[], report: (place: Place) => void): Dependencies;
160
+ dependencies(roots: Unit[], bases: Reach[], shared: Merged, needs: FixtureNeeds, snapshots: (uses: SnapshotUse[]) => Unit[], report: (place: Place) => void): Dependencies;
153
161
  private newModule;
154
162
  private newUnit;
155
163
  private hashOf;
@@ -5,8 +5,9 @@ import { exportName, parseModule } from "./frameworks/bindings.js";
5
5
  import { destructuredNames, problem, takesWhole } from "./frameworks/common.js";
6
6
  import { shortHash, hashNodes } from "./hash.js";
7
7
  import { parseFile } from "./parse.js";
8
- import { snippet } from "./source.js";
8
+ import { snippet, SourceText } from "./source.js";
9
9
  import { bindingNames, isScopeNode, Values } from "./values.js";
10
+ const NOTHING_MERGED = { files: [], byFile: new Map(), parts: new Map() };
10
11
  /** Statements that only declare something, unless code in them runs while they are evaluated. */
11
12
  const DECLARATIONS = new Set([
12
13
  'ImportDeclaration',
@@ -386,11 +387,11 @@ export class DependencyGraph {
386
387
  return { units, byFile, places, fixtures, asks, snapshots };
387
388
  }
388
389
  /**
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.
390
+ * Everything a test's roots reach on top of `bases`, such as the code its file runs when loaded, which all its tests
391
+ * share, with the fixtures it needs of each test object reached: the hash, the files, and whether all of it could be
392
+ * followed. `shared` is what the bases cover, put together once for the many tests that share them.
392
393
  */
393
- dependencies(roots, bases, needs, snapshots, report) {
394
+ dependencies(roots, bases, shared, needs, snapshots, report) {
394
395
  const reached = [...bases, this.reach(roots, bases)];
395
396
  // A fixture can ask for one an object it extends defines, or use another test object, so names are shared by
396
397
  // every test object reached, and each name and object is read once.
@@ -476,11 +477,13 @@ export class DependencyGraph {
476
477
  this.reported.add(key);
477
478
  report(place);
478
479
  }
479
- const files = [...new Set(reached.flatMap((item) => [...item.byFile.keys()]))].sort();
480
+ const added = hashesByFile(reached.slice(bases.length));
481
+ const files = [...new Set([...shared.files, ...added.keys()])].sort();
480
482
  const parts = files.map((file) => {
481
- return [file, reached.flatMap((item) => item.byFile.get(file) ?? []).sort()];
483
+ const own = added.get(file);
484
+ return own ? part(file, [...(shared.byFile.get(file) ?? []), ...own]) : shared.parts.get(file);
482
485
  });
483
- return { hash: shortHash(JSON.stringify(parts)), files, isComplete: places.length === 0 };
486
+ return { hash: shortHash(`[${parts.join(',')}]`), files, isComplete: places.length === 0 };
484
487
  }
485
488
  newModule(relativeFilePath, parsed, text, values, readModule) {
486
489
  return {
@@ -632,7 +635,8 @@ export class DependencyGraph {
632
635
  }
633
636
  /** A place in a module the scan can't follow, as a unit that marks the tests that reach it. */
634
637
  placeUnit(module, at, message, isQuiet = false) {
635
- const { source } = module.parsed;
638
+ // A JSON config, such as Jest's package.json, is taken by its text.
639
+ const source = module.parsed?.source ?? new SourceText(module.text);
636
640
  const unit = this.newUnit(module, [], new Map(), false);
637
641
  unit.edges = new Set();
638
642
  const text = snippet(source.slice(at.start, at.end)) || null;
@@ -665,7 +669,11 @@ export class DependencyGraph {
665
669
  if ('module' in gap) {
666
670
  const module = this.moduleOf(gap.module);
667
671
  // The path a setup key names, where the config writes it.
668
- const at = gap.text === null ? null : literalIn(module.parsed.program, gap.text);
672
+ let at = null;
673
+ if (gap.text !== null && module.parsed)
674
+ at = literalIn(module.parsed.program, gap.text);
675
+ else if (gap.text !== null)
676
+ at = jsonStringIn(module.text, gap.text);
669
677
  return this.placeUnit(module, at ?? { start: 0, end: 0 }, gap.message, gap.isQuiet);
670
678
  }
671
679
  const module = this.newModule(gap.relativeFilePath, null, null, new Values(), null);
@@ -1300,6 +1308,33 @@ export class DependencyGraph {
1300
1308
  return edges;
1301
1309
  }
1302
1310
  }
1311
+ /** The hashes of several reaches by file, in the order the reaches come. */
1312
+ function hashesByFile(reaches) {
1313
+ const byFile = new Map();
1314
+ for (const item of reaches) {
1315
+ for (const [file, hashes] of item.byFile)
1316
+ byFile.set(file, [...(byFile.get(file) ?? []), ...hashes]);
1317
+ }
1318
+ return byFile;
1319
+ }
1320
+ /** A file and its hashes, sorted, as text: the hash is taken of these parts, in the order of the files. */
1321
+ function part(file, hashes) {
1322
+ return JSON.stringify([file, hashes.sort()]);
1323
+ }
1324
+ /** What `merged` covers with what one more reach adds, the parts of the files it adds hashes to written again. */
1325
+ function combine(merged, reach) {
1326
+ if (reach.byFile.size === 0)
1327
+ return merged;
1328
+ const byFile = new Map(merged.byFile);
1329
+ const parts = new Map(merged.parts);
1330
+ for (const [file, hashes] of reach.byFile) {
1331
+ const all = [...(merged.byFile.get(file) ?? []), ...hashes];
1332
+ parts.set(file, part(file, all));
1333
+ byFile.set(file, all);
1334
+ }
1335
+ const files = [...new Set([...merged.files, ...reach.byFile.keys()])].sort();
1336
+ return { files, byFile, parts };
1337
+ }
1303
1338
  /** Whether an identifier refers to a binding, rather than naming a property, a label or a member. */
1304
1339
  function isReference(key, parent) {
1305
1340
  switch (parent?.type) {
@@ -1360,6 +1395,12 @@ function literalIn(node, value) {
1360
1395
  }
1361
1396
  return null;
1362
1397
  }
1398
+ /** Where a JSON file writes a string, as `"./setup.js"`; null when it doesn't. */
1399
+ function jsonStringIn(text, value) {
1400
+ const written = JSON.stringify(value);
1401
+ const start = text.indexOf(written);
1402
+ return start === -1 ? null : { start, end: start + written.length };
1403
+ }
1363
1404
  /**
1364
1405
  * The snapshot files the matchers of a test compare against, from the project root, at the runners' default places:
1365
1406
  * Vitest's `__snapshots__/<file>.snap`, paths given to `toMatchFileSnapshot()`, and browser screenshots in
@@ -1468,6 +1509,7 @@ export function fileDependencies(graph, main, extraction, types, report, runsWit
1468
1509
  ...runsWith.gaps.map((gap) => graph.gapUnit(gap)),
1469
1510
  ];
1470
1511
  const base = graph.reach([load, ...config]);
1512
+ const baseMerged = combine(NOTHING_MERGED, base);
1471
1513
  // The describes around a test and the helper call that registers it are shared by the tests there, often thousands
1472
1514
  // for one helper call, so what they reach is walked once for all of them.
1473
1515
  const contexts = new Map();
@@ -1481,7 +1523,8 @@ export function fileDependencies(graph, main, extraction, types, report, runsWit
1481
1523
  if (around.suite)
1482
1524
  roots.push(...suiteUnit(around.suite));
1483
1525
  }
1484
- context = graph.reach(roots, [base]);
1526
+ const reach = graph.reach(roots, [base]);
1527
+ context = { reach, merged: combine(baseMerged, reach) };
1485
1528
  byCall.set(registeredBy, context);
1486
1529
  }
1487
1530
  return context;
@@ -1528,6 +1571,7 @@ export function fileDependencies(graph, main, extraction, types, report, runsWit
1528
1571
  needs.names.push(...scope.setupFixtures);
1529
1572
  needs.all ||= scope.takesAllFixtures;
1530
1573
  }
1531
- return graph.dependencies(roots, [base, contextOf(test.scope, test.registeredBy)], needs, snapshots, report);
1574
+ const context = contextOf(test.scope, test.registeredBy);
1575
+ return graph.dependencies(roots, [base, context.reach], context.merged, needs, snapshots, report);
1532
1576
  };
1533
1577
  }
@@ -7,6 +7,7 @@ export const LEVELS = {
7
7
  'unresolved-tag': 'warning',
8
8
  'unresolved-annotation': 'warning',
9
9
  'unresolved-cases': 'warning',
10
+ 'cases-not-expanded': 'warning',
10
11
  'test-in-loop': 'warning',
11
12
  'dynamic-title': 'warning',
12
13
  'duplicate-title': 'warning',
@@ -17,6 +18,7 @@ export const LEVELS = {
17
18
  'test-without-body': 'error',
18
19
  'describe-not-called': 'error',
19
20
  'async-describe': 'error',
21
+ 'describe-throws': 'error',
20
22
  'nested-test': 'warning',
21
23
  'tag-without-at': 'error',
22
24
  'removed-api': 'error',
@@ -50,6 +52,7 @@ export const LEVELS = {
50
52
  export const REJECTS_FILE = {
51
53
  playwright: new Set(['test-without-body', 'tag-without-at', 'invalid-chain', 'duplicate-title']),
52
54
  vitest: new Set(['invalid-chain', 'removed-api', 'undefined-tag']),
55
+ jest: new Set(['invalid-chain', 'test-without-body', 'async-describe', 'describe-throws']),
53
56
  };
54
57
  export function toDiagnostic(problem, relativeFilePath, source, testId = null) {
55
58
  const position = problem.node && source ? source.position(problem.node.start) : null;
package/dist/extract.d.ts CHANGED
@@ -48,6 +48,8 @@ export interface PendingSuite {
48
48
  /** Declared under a condition, or in a function: it may not run while the file loads. */
49
49
  isInCondition: boolean;
50
50
  isInFunction: boolean;
51
+ /** With `expandCases`, the index of the case of a table or pass of a loop this describe is; null otherwise. */
52
+ caseIndex: number | null;
51
53
  }
52
54
  export interface PendingTest {
53
55
  scope: Scope;
@@ -66,8 +68,17 @@ export interface PendingTest {
66
68
  states: StateSetting[];
67
69
  annotations: Annotation[];
68
70
  isInLoop: boolean;
71
+ /**
72
+ * How many times each loop around the test registers it, outermost first; null for a loop the scan can't count, or
73
+ * one that registers it under a condition.
74
+ */
75
+ loops: (number | null)[];
76
+ /** How many passes each loop around the test makes, whether or not it registers the test on each. */
77
+ passes: (number | null)[];
69
78
  isInCondition: boolean;
70
79
  isInFunction: boolean;
80
+ /** With `expandCases`, the index of the case of a table or pass of a loop this test is; null otherwise. */
81
+ caseIndex: number | null;
71
82
  comments: string[];
72
83
  /** Run settings from calls in the body, which win over everything around the test. */
73
84
  run: RunSettings;
@@ -85,6 +96,13 @@ export interface Frame {
85
96
  readModule: ModuleReader | null;
86
97
  relativeFilePath: string;
87
98
  }
99
+ /** How the walk lists tables and loops. */
100
+ export interface ExtractOptions {
101
+ /** One test or describe per case of a table and per pass of a loop whose values the scan can work out. */
102
+ expandCases: boolean;
103
+ /** Vitest's `[major, minor]`, whose rules give each case its title; null for the newest rules. */
104
+ version: readonly [number, number] | null;
105
+ }
88
106
  /** Reads a module a helper is imported from as a frame; null when it can't be parsed. */
89
107
  export type OpenModule = (module: ModuleSource) => Frame | null;
90
108
  export interface Extraction {
@@ -112,4 +130,4 @@ export declare function anySlow(slows: NonNullable<RunSettings['slow']>[]): NonN
112
130
  export declare function eitherCondition(parts: string[]): string;
113
131
  export declare function noHooks(): HookCounts;
114
132
  /** Walks the scanned file; `openModule` reads the modules of imported helpers that the file calls. */
115
- export declare function extract(main: Frame, openModule: OpenModule): Extraction;
133
+ export declare function extract(main: Frame, openModule: OpenModule, options?: ExtractOptions): Extraction;