test-inventory 0.8.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 (60) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +16 -11
  3. package/dist/case-titles.d.ts +9 -0
  4. package/dist/case-titles.js +82 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +4 -2
  7. package/dist/config-dependencies.js +19 -0
  8. package/dist/config-runs.d.ts +95 -3
  9. package/dist/config-runs.js +752 -16
  10. package/dist/config.d.ts +24 -0
  11. package/dist/config.js +139 -19
  12. package/dist/diagnostics.js +14 -1
  13. package/dist/extract.d.ts +2 -0
  14. package/dist/extract.js +36 -10
  15. package/dist/files.js +2 -1
  16. package/dist/filter.js +1 -0
  17. package/dist/framework-version.d.ts +4 -2
  18. package/dist/framework-version.js +170 -15
  19. package/dist/frameworks/bun.d.ts +42 -0
  20. package/dist/frameworks/bun.js +272 -0
  21. package/dist/frameworks/common.d.ts +3 -1
  22. package/dist/frameworks/common.js +11 -9
  23. package/dist/frameworks/cypress.d.ts +31 -0
  24. package/dist/frameworks/cypress.js +70 -0
  25. package/dist/frameworks/jasmine.d.ts +28 -0
  26. package/dist/frameworks/jasmine.js +218 -0
  27. package/dist/frameworks/jest.js +1 -1
  28. package/dist/frameworks/mocha.d.ts +50 -0
  29. package/dist/frameworks/mocha.js +306 -0
  30. package/dist/frameworks/node.d.ts +30 -0
  31. package/dist/frameworks/node.js +268 -0
  32. package/dist/frameworks/playwright.d.ts +13 -0
  33. package/dist/frameworks/playwright.js +57 -0
  34. package/dist/frameworks/test-functions.d.ts +26 -1
  35. package/dist/frameworks/test-functions.js +64 -22
  36. package/dist/frameworks/vitest.d.ts +9 -1
  37. package/dist/frameworks/vitest.js +36 -5
  38. package/dist/frameworks/webdriverio.d.ts +27 -0
  39. package/dist/frameworks/webdriverio.js +37 -0
  40. package/dist/model.d.ts +4 -0
  41. package/dist/node-scripts.d.ts +6 -0
  42. package/dist/node-scripts.js +85 -0
  43. package/dist/options.d.ts +13 -4
  44. package/dist/options.js +22 -3
  45. package/dist/records.d.ts +8 -2
  46. package/dist/records.js +50 -13
  47. package/dist/resolve.d.ts +21 -4
  48. package/dist/resolve.js +52 -16
  49. package/dist/scan-file.d.ts +8 -0
  50. package/dist/scan-file.js +16 -2
  51. package/dist/scan.js +42 -7
  52. package/dist/toml.d.ts +13 -0
  53. package/dist/toml.js +172 -0
  54. package/dist/types.d.ts +19 -9
  55. package/dist/values.d.ts +5 -0
  56. package/dist/values.js +13 -0
  57. package/dist/yaml.d.ts +12 -0
  58. package/dist/yaml.js +148 -0
  59. package/package.json +7 -6
  60. package/schema/v1.json +26 -6
package/dist/toml.js ADDED
@@ -0,0 +1,172 @@
1
+ /** A line of TOML the reader doesn't take, with its line number. */
2
+ export class TomlError extends Error {
3
+ line;
4
+ constructor(message, line) {
5
+ super(message);
6
+ this.line = line;
7
+ }
8
+ }
9
+ /**
10
+ * The part of TOML a `bunfig.toml` uses: tables and dotted keys, strings, numbers, booleans, arrays over several lines,
11
+ * inline tables and comments. Dates and arrays of tables are refused with a `TomlError`.
12
+ */
13
+ export function readToml(text) {
14
+ const root = {};
15
+ let table = root;
16
+ const reader = { text, at: 0, line: 1 };
17
+ for (;;) {
18
+ skipBlank(reader, true);
19
+ if (reader.at >= text.length)
20
+ return root;
21
+ if (text[reader.at] === '[') {
22
+ if (text[reader.at + 1] === '[')
23
+ throw new TomlError('Arrays of tables are not read', reader.line);
24
+ reader.at++;
25
+ const keys = readKeys(reader, ']');
26
+ reader.at++;
27
+ table = tableAt(root, keys, reader.line);
28
+ }
29
+ else {
30
+ const keys = readKeys(reader, '=');
31
+ reader.at++;
32
+ skipBlank(reader, false);
33
+ const value = readValue(reader);
34
+ const owner = tableAt(table, keys.slice(0, -1), reader.line);
35
+ owner[keys[keys.length - 1]] = value;
36
+ }
37
+ skipBlank(reader, false);
38
+ if (reader.at < text.length && text[reader.at] !== '\n') {
39
+ throw new TomlError(`Unexpected "${text[reader.at]}"`, reader.line);
40
+ }
41
+ }
42
+ }
43
+ /** Skips spaces and comments, and line breaks too when `lines` is set. */
44
+ function skipBlank(reader, lines) {
45
+ const { text } = reader;
46
+ while (reader.at < text.length) {
47
+ const char = text[reader.at];
48
+ if (char === '#') {
49
+ while (reader.at < text.length && text[reader.at] !== '\n')
50
+ reader.at++;
51
+ }
52
+ else if (char === ' ' || char === '\t' || char === '\r') {
53
+ reader.at++;
54
+ }
55
+ else if (char === '\n' && lines) {
56
+ reader.line++;
57
+ reader.at++;
58
+ }
59
+ else {
60
+ return;
61
+ }
62
+ }
63
+ }
64
+ /** A dotted key, bare or quoted, up to `end`. */
65
+ function readKeys(reader, end) {
66
+ const keys = [];
67
+ for (;;) {
68
+ skipBlank(reader, false);
69
+ const char = reader.text[reader.at];
70
+ if (char === '"' || char === "'")
71
+ keys.push(readString(reader));
72
+ else {
73
+ const bare = /^[A-Za-z0-9_-]+/.exec(reader.text.slice(reader.at));
74
+ if (!bare)
75
+ throw new TomlError('Expected a key', reader.line);
76
+ keys.push(bare[0]);
77
+ reader.at += bare[0].length;
78
+ }
79
+ skipBlank(reader, false);
80
+ if (reader.text[reader.at] === end)
81
+ return keys;
82
+ if (reader.text[reader.at] !== '.')
83
+ throw new TomlError(`Expected "${end}"`, reader.line);
84
+ reader.at++;
85
+ }
86
+ }
87
+ function tableAt(root, keys, line) {
88
+ let table = root;
89
+ for (const key of keys) {
90
+ table[key] ??= {};
91
+ const next = table[key];
92
+ if (typeof next !== 'object' || Array.isArray(next)) {
93
+ throw new TomlError(`${key} is not a table`, line);
94
+ }
95
+ table = next;
96
+ }
97
+ return table;
98
+ }
99
+ function readValue(reader) {
100
+ const { text } = reader;
101
+ const char = text[reader.at];
102
+ if (char === '"' || char === "'")
103
+ return readString(reader);
104
+ if (char === '[') {
105
+ reader.at++;
106
+ const items = [];
107
+ for (;;) {
108
+ skipBlank(reader, true);
109
+ if (text[reader.at] === ']')
110
+ break;
111
+ items.push(readValue(reader));
112
+ skipBlank(reader, true);
113
+ if (text[reader.at] === ',')
114
+ reader.at++;
115
+ else if (text[reader.at] !== ']')
116
+ throw new TomlError('Expected "," or "]"', reader.line);
117
+ }
118
+ reader.at++;
119
+ return items;
120
+ }
121
+ if (char === '{') {
122
+ reader.at++;
123
+ const table = {};
124
+ skipBlank(reader, false);
125
+ while (text[reader.at] !== '}') {
126
+ const keys = readKeys(reader, '=');
127
+ reader.at++;
128
+ skipBlank(reader, false);
129
+ tableAt(table, keys.slice(0, -1), reader.line)[keys[keys.length - 1]] = readValue(reader);
130
+ skipBlank(reader, false);
131
+ if (text[reader.at] === ',')
132
+ reader.at++;
133
+ skipBlank(reader, false);
134
+ }
135
+ reader.at++;
136
+ return table;
137
+ }
138
+ const word = /^[^\s,\]}#]+/.exec(text.slice(reader.at))?.[0] ?? '';
139
+ reader.at += word.length;
140
+ if (word === 'true' || word === 'false')
141
+ return word === 'true';
142
+ const number = word.replaceAll('_', '');
143
+ if (/^[+-]?(\d+(\.\d+)?([eE][+-]?\d+)?|inf|nan)$/.test(number)) {
144
+ return number.endsWith('inf') ? (number.startsWith('-') ? -Infinity : Infinity) : Number(number);
145
+ }
146
+ throw new TomlError(`Value ${word || text[reader.at]} is not read`, reader.line);
147
+ }
148
+ /** A basic string, with escapes, or a literal one; one line each. */
149
+ function readString(reader) {
150
+ const { text } = reader;
151
+ const quote = text[reader.at++];
152
+ let value = '';
153
+ while (text[reader.at] !== quote) {
154
+ const char = text[reader.at];
155
+ if (reader.at >= text.length || char === '\n')
156
+ throw new TomlError('The string is not closed', reader.line);
157
+ if (char === '\\' && quote === '"') {
158
+ const escape = text[reader.at + 1];
159
+ const escapes = { n: '\n', t: '\t', r: '\r', '"': '"', '\\': '\\' };
160
+ if (!(escape in escapes))
161
+ throw new TomlError(`Escape \\${escape} is not read`, reader.line);
162
+ value += escapes[escape];
163
+ reader.at += 2;
164
+ }
165
+ else {
166
+ value += char;
167
+ reader.at++;
168
+ }
169
+ }
170
+ reader.at++;
171
+ return value;
172
+ }
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' | 'jest';
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' | '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';
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[];
@@ -270,7 +275,12 @@ export interface TestRecord {
270
275
  hasUnfollowedDependencies: boolean;
271
276
  /** The nearest describe; null outside any describe. */
272
277
  suiteId: Hash | null;
273
- /** 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. */
274
284
  suitePath: string[];
275
285
  /** Path from the project root, with `/` separators. */
276
286
  relativeFilePath: string;
@@ -350,8 +360,8 @@ export interface TestRecord {
350
360
  /** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
351
361
  retryCondition: string | null;
352
362
  /**
353
- * How many times the runner runs the test again after the first run: Vitest's `repeats`, or Playwright's `repeatEach`
354
- * 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.
355
365
  */
356
366
  repeats: Count | null;
357
367
  /**
package/dist/values.d.ts CHANGED
@@ -55,6 +55,11 @@ 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;
package/dist/values.js CHANGED
@@ -309,6 +309,19 @@ 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;
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.8.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
@@ -99,7 +99,13 @@
99
99
  "enum": [
100
100
  "playwright",
101
101
  "vitest",
102
- "jest"
102
+ "jest",
103
+ "mocha",
104
+ "jasmine",
105
+ "cypress",
106
+ "webdriverio",
107
+ "node",
108
+ "bun"
103
109
  ]
104
110
  },
105
111
  {
@@ -260,7 +266,7 @@
260
266
  "type": "null"
261
267
  }
262
268
  ],
263
- "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."
264
270
  }
265
271
  },
266
272
  "required": [
@@ -268,7 +274,7 @@
268
274
  "extends",
269
275
  "projects"
270
276
  ],
271
- "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."
272
278
  },
273
279
  "ProjectRecord": {
274
280
  "type": "object",
@@ -279,7 +285,7 @@
279
285
  },
280
286
  "testDir": {
281
287
  "type": "string",
282
- "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."
283
289
  },
284
290
  "dependencies": {
285
291
  "type": "array",
@@ -796,12 +802,24 @@
796
802
  ],
797
803
  "description": "The nearest describe; null outside any describe."
798
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
+ },
799
817
  "suitePath": {
800
818
  "type": "array",
801
819
  "items": {
802
820
  "type": "string"
803
821
  },
804
- "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."
805
823
  },
806
824
  "relativeFilePath": {
807
825
  "type": "string",
@@ -1045,7 +1063,7 @@
1045
1063
  "type": "null"
1046
1064
  }
1047
1065
  ],
1048
- "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."
1049
1067
  },
1050
1068
  "timeoutSource": {
1051
1069
  "anyOf": [
@@ -1206,6 +1224,7 @@
1206
1224
  "dependencies",
1207
1225
  "hasUnfollowedDependencies",
1208
1226
  "suiteId",
1227
+ "parentTestId",
1209
1228
  "suitePath",
1210
1229
  "relativeFilePath",
1211
1230
  "fileName",
@@ -1454,6 +1473,7 @@
1454
1473
  "nested-test",
1455
1474
  "tag-without-at",
1456
1475
  "removed-api",
1476
+ "invalid-fixture-argument",
1457
1477
  "only-in-skipped-describe",
1458
1478
  "config-may-change-state",
1459
1479
  "undefined-tag",