test-inventory 0.7.0 → 0.9.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 (67) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +16 -10
  3. package/dist/case-titles.d.ts +22 -0
  4. package/dist/case-titles.js +184 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +5 -5
  7. package/dist/config-dependencies.js +27 -3
  8. package/dist/config-runs.d.ts +117 -3
  9. package/dist/config-runs.js +1015 -19
  10. package/dist/config.d.ts +42 -1
  11. package/dist/config.js +287 -21
  12. package/dist/dependency-hash.d.ts +13 -5
  13. package/dist/dependency-hash.js +56 -12
  14. package/dist/diagnostics.js +16 -1
  15. package/dist/extract.d.ts +2 -0
  16. package/dist/extract.js +89 -29
  17. package/dist/files.js +4 -2
  18. package/dist/filter.js +1 -0
  19. package/dist/framework-version.d.ts +13 -2
  20. package/dist/framework-version.js +207 -7
  21. package/dist/frameworks/bindings.d.ts +14 -0
  22. package/dist/frameworks/bindings.js +99 -6
  23. package/dist/frameworks/bun.d.ts +42 -0
  24. package/dist/frameworks/bun.js +272 -0
  25. package/dist/frameworks/common.d.ts +3 -1
  26. package/dist/frameworks/common.js +18 -9
  27. package/dist/frameworks/cypress.d.ts +31 -0
  28. package/dist/frameworks/cypress.js +70 -0
  29. package/dist/frameworks/jasmine.d.ts +28 -0
  30. package/dist/frameworks/jasmine.js +218 -0
  31. package/dist/frameworks/jest.d.ts +46 -0
  32. package/dist/frameworks/jest.js +442 -0
  33. package/dist/frameworks/mocha.d.ts +50 -0
  34. package/dist/frameworks/mocha.js +306 -0
  35. package/dist/frameworks/node.d.ts +30 -0
  36. package/dist/frameworks/node.js +268 -0
  37. package/dist/frameworks/playwright.d.ts +13 -0
  38. package/dist/frameworks/playwright.js +57 -0
  39. package/dist/frameworks/test-functions.d.ts +194 -0
  40. package/dist/frameworks/test-functions.js +534 -0
  41. package/dist/frameworks/vitest.d.ts +15 -62
  42. package/dist/frameworks/vitest.js +69 -389
  43. package/dist/frameworks/webdriverio.d.ts +27 -0
  44. package/dist/frameworks/webdriverio.js +37 -0
  45. package/dist/imported-values.js +15 -1
  46. package/dist/model.d.ts +9 -2
  47. package/dist/node-scripts.d.ts +6 -0
  48. package/dist/node-scripts.js +85 -0
  49. package/dist/options.d.ts +16 -3
  50. package/dist/options.js +22 -3
  51. package/dist/parse.js +17 -2
  52. package/dist/records.d.ts +8 -2
  53. package/dist/records.js +94 -22
  54. package/dist/resolve.d.ts +21 -4
  55. package/dist/resolve.js +91 -52
  56. package/dist/scan-file.d.ts +8 -0
  57. package/dist/scan-file.js +21 -3
  58. package/dist/scan.js +62 -12
  59. package/dist/toml.d.ts +13 -0
  60. package/dist/toml.js +172 -0
  61. package/dist/types.d.ts +27 -11
  62. package/dist/values.d.ts +15 -0
  63. package/dist/values.js +120 -2
  64. package/dist/yaml.d.ts +12 -0
  65. package/dist/yaml.js +148 -0
  66. package/package.json +7 -6
  67. package/schema/v1.json +30 -8
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' | 'mocha' | 'jasmine' | 'cypress' | 'webdriverio' | 'node' | 'bun';
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' | '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' | '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' | 'invalid-fixture-argument' | '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
  };
@@ -115,8 +115,9 @@ export interface Summary {
115
115
  annotationCounts: Record<string, Count>;
116
116
  }
117
117
  /**
118
- * A Playwright or Vitest config that scanned files use: the nearest one in a file's folder or a folder above it, up to
119
- * the root, as the runner started in that folder would. Read as code, never run.
118
+ * A runner's config that scanned files use. For Playwright and Vitest, the nearest one in a file's folder or a folder
119
+ * above it, up to the root, as the runner started in that folder would; for Jest, the one at the root and the projects
120
+ * it names; for the other runners, the one config they read for the whole run. Read as code, never run.
120
121
  */
121
122
  export interface ConfigRecord {
122
123
  /** Path from the project root, with `/` separators. */
@@ -125,7 +126,8 @@ export interface ConfigRecord {
125
126
  extends: string[];
126
127
  /**
127
128
  * The projects the config runs tests in: Playwright's `projects`, or one for a config without them; Vitest's, with
128
- * each browser instance as one. Null when the scan can't work them out.
129
+ * each browser instance as one; Cypress's testing types, `e2e` and `component`; one for the other runners. Null when
130
+ * the scan can't work them out.
129
131
  */
130
132
  projects: ProjectRecord[] | null;
131
133
  }
@@ -133,7 +135,10 @@ export interface ConfigRecord {
133
135
  export interface ProjectRecord {
134
136
  /** As the runner names it: `''` for a project without a name. */
135
137
  name: string;
136
- /** The folder the project's test files are found in, from the project root: Playwright's `testDir`, Vitest's `dir`. */
138
+ /**
139
+ * The folder the project's test files are found in, from the project root: Playwright's `testDir`, Vitest's `dir`,
140
+ * Jest's `rootDir`, Jasmine's `spec_dir`, Bun's `[test] root`; for the other runners, the config's folder.
141
+ */
137
142
  testDir: string;
138
143
  /** Playwright's projects that run before this one; `[]` for Vitest. */
139
144
  dependencies: string[];
@@ -242,7 +247,10 @@ export interface SuiteRecord {
242
247
  /** Source text of the `test.slow()` condition; null when unconditional or not slow. */
243
248
  slowCondition: string | null;
244
249
  retries: Count | null;
245
- /** Vitest only: milliseconds between retries, from the `retry` object (4.1). */
250
+ /**
251
+ * Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of
252
+ * `jest.retryTimes()` (30.0); null otherwise.
253
+ */
246
254
  retryDelay: Count | null;
247
255
  /** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
248
256
  retryCondition: string | null;
@@ -267,7 +275,12 @@ export interface TestRecord {
267
275
  hasUnfollowedDependencies: boolean;
268
276
  /** The nearest describe; null outside any describe. */
269
277
  suiteId: Hash | null;
270
- /** Titles of the describes around the test, outermost first. */
278
+ /**
279
+ * The test whose body declares this one as a subtest, which `node:test` runs while it runs that test; null for every
280
+ * other test.
281
+ */
282
+ parentTestId: Hash | null;
283
+ /** Titles of the describes around the test, and of the tests a subtest is declared in, outermost first. */
271
284
  suitePath: string[];
272
285
  /** Path from the project root, with `/` separators. */
273
286
  relativeFilePath: string;
@@ -339,13 +352,16 @@ export interface TestRecord {
339
352
  slowCondition: string | null;
340
353
  /** How many times the runner retries the test when it fails; null when the file sets none. */
341
354
  retries: Count | null;
342
- /** Vitest only: milliseconds between retries, from the `retry` object (4.1). */
355
+ /**
356
+ * Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of
357
+ * `jest.retryTimes()` (30.0); null otherwise.
358
+ */
343
359
  retryDelay: Count | null;
344
360
  /** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
345
361
  retryCondition: string | null;
346
362
  /**
347
- * How many times the runner runs the test again after the first run: Vitest's `repeats`, or Playwright's `repeatEach`
348
- * from the config less one.
363
+ * How many times the runner runs the test again after the first run: Vitest's or Bun's `repeats`, or Playwright's
364
+ * `repeatEach` from the config less one.
349
365
  */
350
366
  repeats: Count | null;
351
367
  /**
package/dist/values.d.ts CHANGED
@@ -55,11 +55,18 @@ export declare class Values {
55
55
  * the file does not declare it, as with globals.
56
56
  */
57
57
  declarationOf(name: string, scopes: readonly Node[]): Node | null;
58
+ /**
59
+ * The function a name is declared as, with `function name() {}` or a variable holding one; null for any other name.
60
+ * A parameter is not its function, though the function declares it.
61
+ */
62
+ functionOf(name: string, scopes: readonly Node[]): FunctionNode | null;
58
63
  /** The value a `const` name is declared with, and the scopes around that declaration; null for any other name. */
59
64
  constantOf(name: string, scopes: readonly Node[]): {
60
65
  init: Expression;
61
66
  scopes: readonly Node[];
62
67
  } | null;
68
+ /** The name a module exports that a name imported from it stands for, `default` for a default import; null otherwise. */
69
+ importedName(name: string, scopes: readonly Node[]): string | null;
63
70
  /** The module a name is imported from where it is used; null when it isn't an import. */
64
71
  importSource(name: string, scopes: readonly Node[]): string | null;
65
72
  /**
@@ -81,6 +88,14 @@ export declare class Values {
81
88
  /** `data.name` where `data` is `import * as data`: what the module exports as `name`; null for any other member. */
82
89
  private namespaceMember;
83
90
  private imported;
91
+ /**
92
+ * How many items a list has when not all its values are known, as `[NaN, Infinity]`: a list written out, its spreads
93
+ * included, held in a constant the module doesn't change, or the keys, values or entries of an object written out;
94
+ * null otherwise. `seen` stops at a constant defined through itself.
95
+ */
96
+ lengthOf(node: Expression, scopes: readonly Node[], seen?: Set<string>): number | null;
97
+ /** How many keys an object written out has, held in a constant the module doesn't change; null otherwise. */
98
+ private keyCount;
84
99
  /** What a name refers to where it is used; `scopes` lists the scope nodes around the use, outermost first. */
85
100
  resolveName(name: string, scopes: readonly Node[]): Resolved;
86
101
  /** A parameter of a function read at one of its calls: the argument given there. */
package/dist/values.js CHANGED
@@ -1,4 +1,4 @@
1
- import { childNodes, isFunctionNode, propertyName, unwrap } from "./ast.js";
1
+ import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "./ast.js";
2
2
  const UNRESOLVED = { ok: false, importedFrom: null };
3
3
  function isPrimitive(value) {
4
4
  return typeof value !== 'object' || value === null;
@@ -309,11 +309,29 @@ export class Values {
309
309
  declarationOf(name, scopes) {
310
310
  return this.lookup(name, scopes)?.binding.declaration ?? null;
311
311
  }
312
+ /**
313
+ * The function a name is declared as, with `function name() {}` or a variable holding one; null for any other name.
314
+ * A parameter is not its function, though the function declares it.
315
+ */
316
+ functionOf(name, scopes) {
317
+ const binding = this.lookup(name, scopes)?.binding;
318
+ const declaration = binding?.declaration;
319
+ // A class is bound like a function, but has no body to walk.
320
+ if (binding?.kind === 'function' && declaration?.type === 'FunctionDeclaration')
321
+ return declaration;
322
+ const init = declaration?.type === 'VariableDeclarator' && declaration.init ? unwrap(declaration.init) : null;
323
+ return isFunctionNode(init) ? init : null;
324
+ }
312
325
  /** The value a `const` name is declared with, and the scopes around that declaration; null for any other name. */
313
326
  constantOf(name, scopes) {
314
327
  const binding = this.lookup(name, scopes)?.binding;
315
328
  return binding?.kind === 'const' ? { init: binding.init, scopes: binding.scopes } : null;
316
329
  }
330
+ /** The name a module exports that a name imported from it stands for, `default` for a default import; null otherwise. */
331
+ importedName(name, scopes) {
332
+ const binding = this.lookup(name, scopes)?.binding;
333
+ return binding?.kind === 'import' ? binding.imported : null;
334
+ }
317
335
  /** The module a name is imported from where it is used; null when it isn't an import. */
318
336
  importSource(name, scopes) {
319
337
  const binding = this.lookup(name, scopes)?.binding;
@@ -421,6 +439,61 @@ export class Values {
421
439
  const resolved = name !== null && this.imports ? this.imports(source, name) : UNRESOLVED;
422
440
  return resolved.ok ? resolved : { ok: false, importedFrom: source };
423
441
  }
442
+ /**
443
+ * How many items a list has when not all its values are known, as `[NaN, Infinity]`: a list written out, its spreads
444
+ * included, held in a constant the module doesn't change, or the keys, values or entries of an object written out;
445
+ * null otherwise. `seen` stops at a constant defined through itself.
446
+ */
447
+ lengthOf(node, scopes, seen = new Set()) {
448
+ const resolved = this.resolve(node, scopes);
449
+ if (resolved.ok)
450
+ return Array.isArray(resolved.value) ? resolved.value.length : null;
451
+ const expression = unwrap(node);
452
+ if (expression.type === 'Identifier') {
453
+ const binding = this.lookup(expression.name, scopes)?.binding;
454
+ if (binding?.kind !== 'const' || seen.has(expression.name))
455
+ return null;
456
+ if (this.changedAtLoad(binding.scopes[0]).has(expression.name))
457
+ return null;
458
+ return this.lengthOf(binding.init, binding.scopes, new Set([...seen, expression.name]));
459
+ }
460
+ if (expression.type === 'CallExpression' && isObjectListing(expression)) {
461
+ const target = expression.arguments[0];
462
+ return this.keyCount(target, scopes, seen);
463
+ }
464
+ if (expression.type !== 'ArrayExpression')
465
+ return null;
466
+ let length = 0;
467
+ for (const element of expression.elements) {
468
+ const inner = element?.type === 'SpreadElement' ? this.lengthOf(element.argument, scopes, seen) : 1;
469
+ if (inner === null)
470
+ return null;
471
+ length += inner;
472
+ }
473
+ return length;
474
+ }
475
+ /** How many keys an object written out has, held in a constant the module doesn't change; null otherwise. */
476
+ keyCount(node, scopes, seen) {
477
+ const expression = unwrap(node);
478
+ if (expression.type === 'Identifier') {
479
+ const binding = this.lookup(expression.name, scopes)?.binding;
480
+ if (binding?.kind !== 'const' || seen.has(expression.name))
481
+ return null;
482
+ if (this.changedAtLoad(binding.scopes[0]).has(expression.name))
483
+ return null;
484
+ return this.keyCount(binding.init, binding.scopes, new Set([...seen, expression.name]));
485
+ }
486
+ if (expression.type !== 'ObjectExpression')
487
+ return null;
488
+ const keys = new Set();
489
+ for (const property of expression.properties) {
490
+ const key = property.type === 'Property' ? propertyKey(property) : null;
491
+ if (key === null)
492
+ return null;
493
+ keys.add(key);
494
+ }
495
+ return keys.size;
496
+ }
424
497
  /** What a name refers to where it is used; `scopes` lists the scope nodes around the use, outermost first. */
425
498
  resolveName(name, scopes) {
426
499
  const found = this.lookup(name, scopes);
@@ -716,7 +789,33 @@ function collectBindings(scope, scopes) {
716
789
  if (statement.declare)
717
790
  break;
718
791
  for (const declarator of statement.declarations) {
719
- if (statement.kind === 'const' && declarator.id.type === 'Identifier' && declarator.init) {
792
+ const required = declarator.init ? localRequire(declarator.init) : null;
793
+ if (required !== null && declarator.id.type === 'Identifier') {
794
+ // `const symbols = require('./symbols')` stands for what the module exports, as a default import does.
795
+ bindings.set(declarator.id.name, {
796
+ kind: 'import',
797
+ source: required,
798
+ imported: 'default',
799
+ declaration: declarator,
800
+ });
801
+ }
802
+ else if (required !== null && declarator.id.type === 'ObjectPattern') {
803
+ for (const property of declarator.id.properties) {
804
+ const key = property.type === 'Property' ? propertyKey(property) : null;
805
+ if (key !== null && property.type === 'Property' && property.value.type === 'Identifier') {
806
+ bindings.set(property.value.name, {
807
+ kind: 'import',
808
+ source: required,
809
+ imported: key,
810
+ declaration: declarator,
811
+ });
812
+ }
813
+ else {
814
+ other(bindingNames(property.type === 'Property' ? property.value : property), declarator);
815
+ }
816
+ }
817
+ }
818
+ else if (statement.kind === 'const' && declarator.id.type === 'Identifier' && declarator.init) {
720
819
  bindings.set(declarator.id.name, { kind: 'const', init: declarator.init, scopes, declaration: declarator });
721
820
  }
722
821
  else {
@@ -783,3 +882,22 @@ function collectBindings(scope, scopes) {
783
882
  }
784
883
  return bindings;
785
884
  }
885
+ /** `Object.keys(x)`, `Object.values(x)` or `Object.entries(x)`, which list as many items as `x` has keys. */
886
+ function isObjectListing(call) {
887
+ const callee = unwrap(call.callee);
888
+ return (callee.type === 'MemberExpression' &&
889
+ callee.object.type === 'Identifier' &&
890
+ callee.object.name === 'Object' &&
891
+ ['keys', 'values', 'entries'].includes(propertyName(callee)) &&
892
+ call.arguments.length === 1 &&
893
+ call.arguments[0].type !== 'SpreadElement');
894
+ }
895
+ /** The local file `require('./symbols')` loads, as an import of it; null for a package or any other expression. */
896
+ function localRequire(expression) {
897
+ const node = unwrap(expression);
898
+ if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier' || node.callee.name !== 'require')
899
+ return null;
900
+ const [argument] = node.arguments;
901
+ const isLocal = argument.type === 'Literal' && typeof argument.value === 'string' && /^\.\.?\//.test(argument.value);
902
+ return isLocal ? argument.value : null;
903
+ }
package/dist/yaml.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Value } from './values.ts';
2
+ /** Where a YAML text uses something `readYaml` doesn't read, with its 1-based line. */
3
+ export declare class YamlError extends Error {
4
+ readonly line: number;
5
+ constructor(message: string, line: number);
6
+ }
7
+ /**
8
+ * The value of a YAML config as Mocha's `.mocharc.yml` files write it: a map of keys to scalars, to lists written one
9
+ * item per line or in brackets, and to maps of those. Anchors, multi-line strings, flow maps and documents after the
10
+ * first are refused with a `YamlError`, as a guess at them could read the wrong test files.
11
+ */
12
+ export declare function readYaml(text: string): Value;
package/dist/yaml.js ADDED
@@ -0,0 +1,148 @@
1
+ /** Where a YAML text uses something `readYaml` doesn't read, with its 1-based line. */
2
+ export class YamlError extends Error {
3
+ line;
4
+ constructor(message, line) {
5
+ super(message);
6
+ this.line = line;
7
+ }
8
+ }
9
+ /**
10
+ * The value of a YAML config as Mocha's `.mocharc.yml` files write it: a map of keys to scalars, to lists written one
11
+ * item per line or in brackets, and to maps of those. Anchors, multi-line strings, flow maps and documents after the
12
+ * first are refused with a `YamlError`, as a guess at them could read the wrong test files.
13
+ */
14
+ export function readYaml(text) {
15
+ const lines = [];
16
+ text.split(/\r?\n/).forEach((raw, index) => {
17
+ const content = withoutComment(raw);
18
+ if (content.trim() === '' || (index === 0 && content.trim() === '---'))
19
+ return;
20
+ lines.push({ number: index + 1, indent: content.length - content.trimStart().length, text: content.trim() });
21
+ });
22
+ if (lines.length === 0)
23
+ return {};
24
+ const [value, next] = block(lines, 0, lines[0].indent);
25
+ if (next < lines.length)
26
+ throw new YamlError('a line is indented less than the one it continues', lines[next].number);
27
+ return value;
28
+ }
29
+ /** A map or a list whose lines start at `indent`, and the index of the line after it. */
30
+ function block(lines, start, indent) {
31
+ if (lines[start].text.startsWith('-'))
32
+ return list(lines, start, indent);
33
+ const map = {};
34
+ let index = start;
35
+ while (index < lines.length && lines[index].indent === indent) {
36
+ const line = lines[index];
37
+ const match = /^("[^"]*"|'[^']*'|[^\s:'"][^:]*?)\s*:(?:\s+(.*))?$/.exec(line.text);
38
+ if (!match)
39
+ throw new YamlError(`"${line.text}" is not a key and its value`, line.number);
40
+ const key = unquote(match[1]);
41
+ // An optional group that matched nothing is undefined, though the type says it is a string.
42
+ const value = match[2];
43
+ if (value !== undefined) {
44
+ map[key] = scalarOrFlow(value, line.number);
45
+ index += 1;
46
+ }
47
+ else if (index + 1 < lines.length && lines[index + 1].indent > indent) {
48
+ [map[key], index] = block(lines, index + 1, lines[index + 1].indent);
49
+ }
50
+ else if (index + 1 < lines.length &&
51
+ lines[index + 1].indent === indent &&
52
+ lines[index + 1].text.startsWith('-')) {
53
+ // A list may sit at the same indent as its key.
54
+ [map[key], index] = list(lines, index + 1, indent);
55
+ }
56
+ else {
57
+ map[key] = null;
58
+ index += 1;
59
+ }
60
+ }
61
+ return [map, index];
62
+ }
63
+ function list(lines, start, indent) {
64
+ const items = [];
65
+ let index = start;
66
+ while (index < lines.length && lines[index].indent === indent && lines[index].text.startsWith('-')) {
67
+ const line = lines[index];
68
+ const item = line.text.slice(1).trim();
69
+ if (item === '' || /^[^'"[\]]*:(\s|$)/.test(item)) {
70
+ throw new YamlError('a list item that is a map or spans lines is not read', line.number);
71
+ }
72
+ items.push(scalarOrFlow(item, line.number));
73
+ index += 1;
74
+ }
75
+ return [items, index];
76
+ }
77
+ /** A scalar, or a list written in brackets, as `[ts, js]`. */
78
+ function scalarOrFlow(text, line) {
79
+ if (text.startsWith('[')) {
80
+ if (!text.endsWith(']'))
81
+ throw new YamlError('a list in brackets must end on its line', line);
82
+ // A trailing comma ends the list without another item, as in `[ts, js,]`.
83
+ const items = text
84
+ .slice(1, -1)
85
+ .split(',')
86
+ .map((item) => item.trim());
87
+ if (items.at(-1) === '')
88
+ items.pop();
89
+ return items.map((item) => scalar(item, line));
90
+ }
91
+ return scalar(text, line);
92
+ }
93
+ function scalar(text, line) {
94
+ if (/^["']/.test(text)) {
95
+ if (text.length < 2 || text.at(-1) !== text[0])
96
+ throw new YamlError('a quoted value must end on its line', line);
97
+ try {
98
+ return unquote(text);
99
+ }
100
+ catch {
101
+ throw new YamlError(`${text} has an escape that is not read`, line);
102
+ }
103
+ }
104
+ if (/^[&*!|>{]/.test(text))
105
+ throw new YamlError(`"${text}" uses YAML that is not read`, line);
106
+ if (/^(true|True|TRUE)$/.test(text))
107
+ return true;
108
+ if (/^(false|False|FALSE)$/.test(text))
109
+ return false;
110
+ if (/^(null|Null|NULL|~)$/.test(text))
111
+ return null;
112
+ // Numbers as YAML 1.2 writes them, which js-yaml, Mocha's reader, follows: `1_000` is a string.
113
+ if (/^0o[0-7]+$/.test(text))
114
+ return Number.parseInt(text.slice(2), 8);
115
+ if (/^0x[\da-fA-F]+$/.test(text))
116
+ return Number.parseInt(text.slice(2), 16);
117
+ if (/^[-+]?(\.\d+|\d+(\.\d*)?)([eE][-+]?\d+)?$/.test(text))
118
+ return Number(text);
119
+ return text;
120
+ }
121
+ function unquote(text) {
122
+ if (text.startsWith("'"))
123
+ return text.slice(1, -1).replaceAll("''", "'");
124
+ if (text.startsWith('"'))
125
+ return JSON.parse(text);
126
+ return text;
127
+ }
128
+ /**
129
+ * The line without a `#` comment, which starts after a space or at the start, outside quotes. A quote opens only where
130
+ * a value or key starts, so the apostrophe in `grep: don't` is part of the value.
131
+ */
132
+ function withoutComment(line) {
133
+ let quote = null;
134
+ for (let index = 0; index < line.length; index++) {
135
+ const char = line[index];
136
+ if (quote) {
137
+ if (char === quote)
138
+ quote = null;
139
+ }
140
+ else if ((char === '"' || char === "'") && /(^|[\s:,[-])\s*$/.test(line.slice(0, index))) {
141
+ quote = char;
142
+ }
143
+ else if (char === '#' && (index === 0 || /\s/.test(line[index - 1]))) {
144
+ return line.slice(0, index).trimEnd();
145
+ }
146
+ }
147
+ return line.trimEnd();
148
+ }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "test-inventory",
3
- "version": "0.7.0",
4
- "description": "Test metadata from source code, not from test results.",
3
+ "version": "0.9.0",
4
+ "description": "Lists every Playwright, Vitest and Jest test from the source code, without running it: its state and why, tags, an id that survives a move, a hash of everything it runs, and the tests a change can affect.",
5
5
  "keywords": [
6
6
  "testing",
7
7
  "playwright",
8
8
  "vitest",
9
+ "jest",
9
10
  "test-suite",
10
11
  "test-inventory",
11
12
  "static-analysis",
@@ -79,7 +80,7 @@
79
80
  "changeset": "changeset"
80
81
  },
81
82
  "dependencies": {
82
- "oxc-parser": "0.156.0",
83
+ "oxc-parser": "0.157.0",
83
84
  "picomatch": "^4.0.7",
84
85
  "tinyglobby": "^0.2.17"
85
86
  },
@@ -89,9 +90,9 @@
89
90
  "@changesets/cli": "^3.0.3",
90
91
  "@types/node": "~22.12.0",
91
92
  "@types/picomatch": "^4.0.3",
92
- "@vitest/coverage-v8": "5.0.5",
93
+ "@vitest/coverage-v8": "5.1.0",
93
94
  "ajv": "^8.20.0",
94
- "eslint": "~10.13.0",
95
+ "eslint": "~10.14.0",
95
96
  "prettier": "3.9.10",
96
97
  "publint": "^0.3.25",
97
98
  "ts-json-schema-generator": "2.9.0",
@@ -102,6 +103,6 @@
102
103
  "typescript-eslint": "~8.72.1",
103
104
  "vitepress": "2.0.0-alpha.21",
104
105
  "vitepress-plugin-llms": "^1.14.0",
105
- "vitest": "5.0.5"
106
+ "vitest": "5.1.0"
106
107
  }
107
108
  }
package/schema/v1.json CHANGED
@@ -98,7 +98,14 @@
98
98
  "type": "string",
99
99
  "enum": [
100
100
  "playwright",
101
- "vitest"
101
+ "vitest",
102
+ "jest",
103
+ "mocha",
104
+ "jasmine",
105
+ "cypress",
106
+ "webdriverio",
107
+ "node",
108
+ "bun"
102
109
  ]
103
110
  },
104
111
  {
@@ -259,7 +266,7 @@
259
266
  "type": "null"
260
267
  }
261
268
  ],
262
- "description": "The projects the config runs tests in: Playwright's `projects`, or one for a config without them; Vitest's, with each browser instance as one. Null when the scan can't work them out."
269
+ "description": "The projects the config runs tests in: Playwright's `projects`, or one for a config without them; Vitest's, with each browser instance as one; Cypress's testing types, `e2e` and `component`; one for the other runners. Null when the scan can't work them out."
263
270
  }
264
271
  },
265
272
  "required": [
@@ -267,7 +274,7 @@
267
274
  "extends",
268
275
  "projects"
269
276
  ],
270
- "description": "A Playwright or Vitest config that scanned files use: the nearest one in a file's folder or a folder above it, up to the root, as the runner started in that folder would. Read as code, never run."
277
+ "description": "A runner's config that scanned files use. For Playwright and Vitest, the nearest one in a file's folder or a folder above it, up to the root, as the runner started in that folder would; for Jest, the one at the root and the projects it names; for the other runners, the one config they read for the whole run. Read as code, never run."
271
278
  },
272
279
  "ProjectRecord": {
273
280
  "type": "object",
@@ -278,7 +285,7 @@
278
285
  },
279
286
  "testDir": {
280
287
  "type": "string",
281
- "description": "The folder the project's test files are found in, from the project root: Playwright's `testDir`, Vitest's `dir`."
288
+ "description": "The folder the project's test files are found in, from the project root: Playwright's `testDir`, Vitest's `dir`, Jest's `rootDir`, Jasmine's `spec_dir`, Bun's `[test] root`; for the other runners, the config's folder."
282
289
  },
283
290
  "dependencies": {
284
291
  "type": "array",
@@ -607,7 +614,7 @@
607
614
  "type": "null"
608
615
  }
609
616
  ],
610
- "description": "Vitest only: milliseconds between retries, from the `retry` object (4.1)."
617
+ "description": "Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of `jest.retryTimes()` (30.0); null otherwise."
611
618
  },
612
619
  "retryCondition": {
613
620
  "type": [
@@ -795,12 +802,24 @@
795
802
  ],
796
803
  "description": "The nearest describe; null outside any describe."
797
804
  },
805
+ "parentTestId": {
806
+ "anyOf": [
807
+ {
808
+ "type": "string",
809
+ "pattern": "^[0-9a-f]{16}$"
810
+ },
811
+ {
812
+ "type": "null"
813
+ }
814
+ ],
815
+ "description": "The test whose body declares this one as a subtest, which `node:test` runs while it runs that test; null for every other test."
816
+ },
798
817
  "suitePath": {
799
818
  "type": "array",
800
819
  "items": {
801
820
  "type": "string"
802
821
  },
803
- "description": "Titles of the describes around the test, outermost first."
822
+ "description": "Titles of the describes around the test, and of the tests a subtest is declared in, outermost first."
804
823
  },
805
824
  "relativeFilePath": {
806
825
  "type": "string",
@@ -1025,7 +1044,7 @@
1025
1044
  "type": "null"
1026
1045
  }
1027
1046
  ],
1028
- "description": "Vitest only: milliseconds between retries, from the `retry` object (4.1)."
1047
+ "description": "Milliseconds between retries: Vitest's from the `retry` object (4.1), Jest's from `waitBeforeRetry` of `jest.retryTimes()` (30.0); null otherwise."
1029
1048
  },
1030
1049
  "retryCondition": {
1031
1050
  "type": [
@@ -1044,7 +1063,7 @@
1044
1063
  "type": "null"
1045
1064
  }
1046
1065
  ],
1047
- "description": "How many times the runner runs the test again after the first run: Vitest's `repeats`, or Playwright's `repeatEach` from the config less one."
1066
+ "description": "How many times the runner runs the test again after the first run: Vitest's or Bun's `repeats`, or Playwright's `repeatEach` from the config less one."
1048
1067
  },
1049
1068
  "timeoutSource": {
1050
1069
  "anyOf": [
@@ -1205,6 +1224,7 @@
1205
1224
  "dependencies",
1206
1225
  "hasUnfollowedDependencies",
1207
1226
  "suiteId",
1227
+ "parentTestId",
1208
1228
  "suitePath",
1209
1229
  "relativeFilePath",
1210
1230
  "fileName",
@@ -1449,9 +1469,11 @@
1449
1469
  "test-without-body",
1450
1470
  "describe-not-called",
1451
1471
  "async-describe",
1472
+ "describe-throws",
1452
1473
  "nested-test",
1453
1474
  "tag-without-at",
1454
1475
  "removed-api",
1476
+ "invalid-fixture-argument",
1455
1477
  "only-in-skipped-describe",
1456
1478
  "config-may-change-state",
1457
1479
  "undefined-tag",