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
package/dist/scan.js CHANGED
@@ -4,18 +4,19 @@ import path from 'node:path';
4
4
  import { glob } from 'tinyglobby';
5
5
  import { ConfigDependencies } from "./config-dependencies.js";
6
6
  import { ConfigRuns, fileConfig } from "./config-runs.js";
7
- import { Configs } from "./config.js";
7
+ import { Configs, settingAt } from "./config.js";
8
8
  import { DependencyGraph } from "./dependency-hash.js";
9
+ import { ExportedValues } from "./imported-values.js";
9
10
  import { fileDiagnostic } from "./diagnostics.js";
10
11
  import { findFiles, normalizePattern, readFailure } from "./files.js";
11
- import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
12
+ import { detectFramework, detectFrameworkVersion, FRAMEWORK_NAMES, OLDEST } from "./framework-version.js";
12
13
  import { createInventory } from "./inventory.js";
13
- import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
14
+ import { checkComments, checkFlag, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
14
15
  import { moduleResolver } from "./resolve.js";
15
16
  import { noRecords, scanFile } from "./scan-file.js";
16
17
  import { globalsCheck } from "./vitest-config.js";
17
18
  function versionDiagnostics(framework, version, source, range) {
18
- const name = framework === 'playwright' ? 'Playwright' : 'Vitest';
19
+ const name = FRAMEWORK_NAMES[framework];
19
20
  if (version === null) {
20
21
  return [
21
22
  fileDiagnostic('unknown-framework-version', source, `The ${name} version could not be read from the project, so the rules of the newest supported version apply.`),
@@ -36,6 +37,17 @@ function versionDiagnostics(framework, version, source, range) {
36
37
  fileDiagnostic('approximate-framework-version', source, `Only the range "${range}" was found for ${name}, so the rules of the newest ${major}.x release apply.`),
37
38
  ];
38
39
  }
40
+ /** Why Jest gives a file that uses its globals none: a project that runs it sets `injectGlobals` to false. */
41
+ function jestGlobals(placement) {
42
+ const off = placement.projects?.find(({ settings }) => {
43
+ const setting = settingAt(settings, ['injectGlobals']);
44
+ return setting?.kind === 'value' && setting.value === false;
45
+ });
46
+ if (!off)
47
+ return null;
48
+ const config = off.config.record.relativeFilePath;
49
+ return `The file uses describe, it or test without importing them, but ${config} sets injectGlobals to false, so Jest doesn't define them; import them from '@jest/globals'.`;
50
+ }
39
51
  /** For a pattern that names a folder, which matches no file, a pattern that matches the test files in it. */
40
52
  function folderHint(root, patterns, framework) {
41
53
  const folder = patterns.find((pattern) => {
@@ -99,7 +111,7 @@ function noFiles(root, patterns, framework, placements) {
99
111
  if ([...placements.values()].some((placement) => placement.config)) {
100
112
  return 'No test files were found from the configs in the root.';
101
113
  }
102
- const name = framework === 'playwright' ? 'Playwright' : 'Vitest';
114
+ const name = FRAMEWORK_NAMES[framework];
103
115
  return `No patterns were given, and no ${name} config is in the root or a folder below it, so there are no test files to find.`;
104
116
  }
105
117
  /**
@@ -111,9 +123,9 @@ function noFiles(root, patterns, framework, placements) {
111
123
  export async function scan(options) {
112
124
  const checked = checkObject(options, 'options');
113
125
  const patterns = checked.patterns === undefined ? null : checkStrings(checked.patterns, 'options.patterns', true);
114
- const framework = checkFramework(checked.framework, 'options.framework');
115
126
  const ignore = checkStrings(checked.ignore, 'options.ignore', false);
116
127
  const comments = checkComments(checked.comments, 'options.comments');
128
+ const expandCases = checkFlag(checked.expandCases, 'options.expandCases');
117
129
  const override = checkVersion(checked.frameworkVersion, 'options.frameworkVersion');
118
130
  if (checked.root !== undefined && (typeof checked.root !== 'string' || checked.root === '')) {
119
131
  throw invalid('options.root', 'a non-empty string');
@@ -121,7 +133,13 @@ export async function scan(options) {
121
133
  const root = path.resolve(checked.root ?? process.cwd());
122
134
  if (!existsSync(root) || !statSync(root).isDirectory())
123
135
  throw invalid('options.root', `a directory, and ${root} is not one`);
124
- const detected = override === null ? detectFrameworkVersion(root, framework) : null;
136
+ const framework = checked.framework === undefined
137
+ ? detectFramework(root, ignore)
138
+ : checkFramework(checked.framework, 'options.framework');
139
+ // Without patterns, the configs choose among all JavaScript and TypeScript files.
140
+ const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
141
+ const scanned = found.files.map((file) => file.relativeFilePath);
142
+ const detected = override === null ? detectFrameworkVersion(root, framework, scanned) : null;
125
143
  const frameworkVersion = override ?? detected?.version ?? null;
126
144
  const range = detected?.range ?? null;
127
145
  // With only a range, the newest rules of its major apply, as the newest release in it is the likeliest installed.
@@ -130,11 +148,10 @@ export async function scan(options) {
130
148
  : majorMinor(frameworkVersion);
131
149
  const source = override === null ? (detected?.source ?? 'package.json') : '.';
132
150
  const projectDiagnostics = versionDiagnostics(framework, frameworkVersion, source, range);
133
- // Without patterns, the configs choose among all JavaScript and TypeScript files.
134
- const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
135
151
  projectDiagnostics.push(...found.diagnostics);
136
152
  const readerFor = moduleResolver(root);
137
153
  const graph = new DependencyGraph();
154
+ const exports = new ExportedValues();
138
155
  const configs = new Configs(root, framework, readerFor, rules);
139
156
  const runs = new ConfigRuns(configs, framework, rules, patterns === null);
140
157
  const placements = new Map(found.files.map((file) => [file.absolutePath, runs.placementOf(file.absolutePath)]));
@@ -193,11 +210,13 @@ export async function scan(options) {
193
210
  framework,
194
211
  version: rules,
195
212
  comments,
213
+ expandCases,
196
214
  readModule: readerFor(absolutePath),
197
215
  root,
198
216
  isBenchmarkFile: placement.runs === null ? null : placement.runs === 'benchmark',
199
217
  config: fileConfig(placement, framework, rules, absolutePath),
200
218
  graph,
219
+ exports,
201
220
  runsWith,
202
221
  snapshotFiles: { ...snapshotFiles, moved: runsWith.moved },
203
222
  });
@@ -210,7 +229,9 @@ export async function scan(options) {
210
229
  }
211
230
  results.push({ ...result, file: { ...result.file, config } });
212
231
  }
213
- const missingGlobals = globalsCheck(root, configs, (file) => placements.get(file));
232
+ const missingGlobals = framework === 'jest'
233
+ ? (file) => jestGlobals(placements.get(file))
234
+ : globalsCheck(root, configs, (file) => placements.get(file));
214
235
  let globals = null;
215
236
  for (const { absolutePath, relativeFilePath } of globalsFiles) {
216
237
  const message = missingGlobals(absolutePath);
package/dist/types.d.ts CHANGED
@@ -11,7 +11,7 @@ type Position = number;
11
11
  /** @pattern ^[0-9a-f]{16}$ */
12
12
  type Hash = string;
13
13
  /** @inline */
14
- export type Framework = 'playwright' | 'vitest';
14
+ export type Framework = 'playwright' | 'vitest' | 'jest';
15
15
  /**
16
16
  * The state the runner applies to a test, after describes and file-level calls are taken into account. `notLoaded`:
17
17
  * the runner refuses to load the file, so none of its tests run.
@@ -38,7 +38,7 @@ export type SettingSource = 'test' | 'suite' | 'file' | 'config';
38
38
  * @inline
39
39
  */
40
40
  export type DiagnosticLevel = 'error' | 'warning';
41
- export type DiagnosticCode = 'unresolved-test-import' | 'parse-error' | 'read-error' | 'framework-mismatch' | 'unresolved-tag' | 'unresolved-annotation' | 'unresolved-cases' | 'test-in-loop' | 'dynamic-title' | 'duplicate-title' | 'duplicate-id' | 'internal-error' | 'invalid-chain' | 'skip-without-body' | 'test-without-body' | 'describe-not-called' | 'async-describe' | 'nested-test' | 'tag-without-at' | 'removed-api' | 'only-in-skipped-describe' | 'config-may-change-state' | 'undefined-tag' | 'symlink-ignored' | 'invalid-import' | 'globals-not-enabled' | 'unknown-framework-version' | 'unsupported-framework-version' | 'local-test-object' | 'state-in-helper' | 'unresolved-option' | 'dynamic-reason' | 'no-files-matched' | 'unsupported-file' | 'approximate-framework-version' | 'test-in-function' | 'empty-title' | 'bench-outside-benchmark-file' | 'config-value-unknown' | 'file-not-run' | 'forbidden-only' | 'dependency-not-followed';
41
+ export type DiagnosticCode = 'unresolved-test-import' | 'parse-error' | 'read-error' | 'framework-mismatch' | 'unresolved-tag' | 'unresolved-annotation' | 'unresolved-cases' | 'cases-not-expanded' | 'test-in-loop' | 'dynamic-title' | 'duplicate-title' | 'duplicate-id' | 'internal-error' | 'invalid-chain' | 'skip-without-body' | 'test-without-body' | 'describe-not-called' | 'async-describe' | 'describe-throws' | 'nested-test' | 'tag-without-at' | 'removed-api' | 'only-in-skipped-describe' | 'config-may-change-state' | 'undefined-tag' | 'symlink-ignored' | 'invalid-import' | 'globals-not-enabled' | 'unknown-framework-version' | 'unsupported-framework-version' | 'local-test-object' | 'state-in-helper' | 'unresolved-option' | 'dynamic-reason' | 'no-files-matched' | 'unsupported-file' | 'approximate-framework-version' | 'test-in-function' | 'empty-title' | 'bench-outside-benchmark-file' | 'config-value-unknown' | 'file-not-run' | 'forbidden-only' | 'dependency-not-followed';
42
42
  export type MetaValue = string | number | boolean | null | MetaValue[] | {
43
43
  [key: string]: MetaValue;
44
44
  };
@@ -222,6 +222,11 @@ export interface SuiteRecord {
222
222
  isShuffled: boolean;
223
223
  /** Declared with `.each` or `.for`. */
224
224
  isParameterized: boolean;
225
+ /**
226
+ * With `expandCases`, which case of its table or pass of its loop the describe is, from 0, as Vitest's `%#` counts;
227
+ * null without `expandCases`, or when the describe isn't one of several.
228
+ */
229
+ caseIndex: Count | null;
225
230
  tags: string[];
226
231
  annotations: Annotation[];
227
232
  /** Playwright only. */
@@ -237,7 +242,10 @@ export interface SuiteRecord {
237
242
  /** Source text of the `test.slow()` condition; null when unconditional or not slow. */
238
243
  slowCondition: string | null;
239
244
  retries: Count | null;
240
- /** Vitest only: milliseconds between retries, from the `retry` object (4.1). */
245
+ /**
246
+ * Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of
247
+ * `jest.retryTimes()` (30.0); null otherwise.
248
+ */
241
249
  retryDelay: Count | null;
242
250
  /** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
243
251
  retryCondition: string | null;
@@ -334,7 +342,10 @@ export interface TestRecord {
334
342
  slowCondition: string | null;
335
343
  /** How many times the runner retries the test when it fails; null when the file sets none. */
336
344
  retries: Count | null;
337
- /** Vitest only: milliseconds between retries, from the `retry` object (4.1). */
345
+ /**
346
+ * Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of
347
+ * `jest.retryTimes()` (30.0); null otherwise.
348
+ */
338
349
  retryDelay: Count | null;
339
350
  /** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
340
351
  retryCondition: string | null;
@@ -381,10 +392,17 @@ export interface TestRecord {
381
392
  /** Declared with `.each` or `.for`, or inside a describe declared with them, so the runner repeats it. */
382
393
  isParameterized: boolean;
383
394
  /**
384
- * How many times the runner repeats the test: the cases of its own table times those of each describe table around
385
- * it. Null when a table is not written out in the file, or the test is not parameterized.
395
+ * How many times the runner repeats the test: the cases of its own table times those of each describe table and the
396
+ * passes of each loop around it. Null when the scan can't work out a table or a loop's values, or the test is neither
397
+ * parameterized nor in a loop.
386
398
  */
387
399
  caseCount: Count | null;
400
+ /**
401
+ * With `expandCases`, which case of its own table, or pass of the innermost loop around it inside its describe, the
402
+ * test is, from 0, as Vitest's `%#` counts; null without `expandCases`, or when the test isn't one of several. The
403
+ * case of a describe table or loop around the describe is the describe's `caseIndex`.
404
+ */
405
+ caseIndex: Count | null;
388
406
  /** Declared inside a loop, `forEach` or `map`. */
389
407
  isInLoop: boolean;
390
408
  /** Declared inside `if`, `?:`, `&&`, `switch` or a `catch` block. */
package/dist/values.d.ts CHANGED
@@ -11,6 +11,8 @@ export type Resolved = {
11
11
  ok: false;
12
12
  importedFrom: string | null;
13
13
  };
14
+ /** What another module exports under a name, `default` included, for an import of the file. */
15
+ export type ImportReader = (source: string, name: string) => Resolved;
14
16
  /** Whether a value can be used as a property name. */
15
17
  export declare function isKey(value: Value): value is string | number;
16
18
  export declare function isScopeNode(node: Node): boolean;
@@ -22,6 +24,11 @@ export declare class Values {
22
24
  private readonly pending;
23
25
  /** The functions being read at one of their calls, innermost last. */
24
26
  private readonly calls;
27
+ private readonly imports;
28
+ private readonly assigned;
29
+ private readonly changed;
30
+ /** Without `imports`, a value imported from another module is not worked out. */
31
+ constructor(imports?: ImportReader | null);
25
32
  /**
26
33
  * Runs `read` with a function's parameters bound to the arguments of one call, worked out where the call is, so that
27
34
  * titles and values built from them resolve, as in `test(\`${side} rejects a zero quantity\`)` inside
@@ -29,6 +36,18 @@ export declare class Values {
29
36
  * takes the parameter's default value, worked out in `scopes`, the scopes around the function.
30
37
  */
31
38
  withArguments<T>(fn: FunctionNode, args: readonly Resolved[], scopes: readonly Node[], read: () => T): T;
39
+ /**
40
+ * Runs `read` with the names a loop declares bound to one of the values it goes over, as in
41
+ * `for (const [tier, bps] of Object.entries(FEES))`. `declarator` is the loop's variable declarator.
42
+ */
43
+ withLoopValue<T>(declarator: Node, pattern: Node, value: Resolved, body: Node, scopes: readonly Node[], read: () => T): T;
44
+ private assignedIn;
45
+ private withBound;
46
+ /**
47
+ * Binds the names of a parameter or a loop variable to the parts of `value` they take, as destructuring does. A
48
+ * missing `value`, after a spread argument, leaves them unknown, unless a default gives them one.
49
+ */
50
+ private bind;
32
51
  /** `scopes` lists the scope nodes around the expression, outermost first. */
33
52
  resolve(node: Expression, scopes: readonly Node[]): Resolved;
34
53
  /**
@@ -41,14 +60,43 @@ export declare class Values {
41
60
  init: Expression;
42
61
  scopes: readonly Node[];
43
62
  } | null;
63
+ /** The name a module exports that a name imported from it stands for, `default` for a default import; null otherwise. */
64
+ importedName(name: string, scopes: readonly Node[]): string | null;
44
65
  /** The module a name is imported from where it is used; null when it isn't an import. */
45
66
  importSource(name: string, scopes: readonly Node[]): string | null;
67
+ /**
68
+ * Whether a name refers to a parameter or loop variable bound to a value where it is used: an argument of the helper
69
+ * call being walked, or a value of the case or pass of an expansion.
70
+ */
71
+ isBound(name: string, scopes: readonly Node[]): boolean;
46
72
  /** Whether a name refers to a function or class declared in the file. */
47
73
  isFunction(name: string, scopes: readonly Node[]): boolean;
48
74
  private propertyKey;
49
- private resolveName;
75
+ /**
76
+ * The calls titles are often built with, all of them pure: `Object.keys()`, `Object.values()`, `Object.entries()`,
77
+ * `JSON.stringify()` with one argument, `String()`, and a string's `toUpperCase()`, `toLowerCase()` and `trim()` and
78
+ * an array's `join()`.
79
+ */
80
+ private call;
81
+ /** Whether a name refers to a global, as no scope around the use declares it. */
82
+ private isGlobal;
83
+ /** `data.name` where `data` is `import * as data`: what the module exports as `name`; null for any other member. */
84
+ private namespaceMember;
85
+ private imported;
86
+ /**
87
+ * How many items a list has when not all its values are known, as `[NaN, Infinity]`: a list written out, its spreads
88
+ * included, held in a constant the module doesn't change, or the keys, values or entries of an object written out;
89
+ * null otherwise. `seen` stops at a constant defined through itself.
90
+ */
91
+ lengthOf(node: Expression, scopes: readonly Node[], seen?: Set<string>): number | null;
92
+ /** How many keys an object written out has, held in a constant the module doesn't change; null otherwise. */
93
+ private keyCount;
94
+ /** What a name refers to where it is used; `scopes` lists the scope nodes around the use, outermost first. */
95
+ resolveName(name: string, scopes: readonly Node[]): Resolved;
50
96
  /** A parameter of a function read at one of its calls: the argument given there. */
51
97
  private resolveParameter;
98
+ /** Names the module changes while it loads, as in `WATCHED.push('MSFT')`, whose declared value isn't the one used. */
99
+ private changedAtLoad;
52
100
  private resolveConstant;
53
101
  private enumMembers;
54
102
  private lookup;