test-inventory 0.1.0 → 0.1.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # test-inventory
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - A custom test object imported from `'.'` or `'..'` is followed like other relative imports.
8
+
9
+ - [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - When a pattern names a folder, the `no-files-matched` message suggests a pattern for the test files in it, and the command line prints that message.
10
+
11
+ - [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - A pattern whose folder doesn't exist now gives only the `no-files-matched` warning, not a `read-error`.
12
+
13
+ - [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - Messages for problems that make Playwright refuse a file now say that Playwright runs no tests at all, in any file.
14
+
15
+ - [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - A new `test-in-function` warning marks a test declared in a function that is called more than once, from another describe, never, or from other files. The runner registers such a test in the describe around each call, while the record stays where the test is written.
16
+
17
+ - [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - A version range that starts below the oldest supported version, such as `^1.40.0`, now gets the `approximate-framework-version` warning, since the newest rules of its major apply.
18
+
19
+ - [#5](https://github.com/Huldoser/test-inventory/pull/5) [`c812450`](https://github.com/Huldoser/test-inventory/commit/c812450a0652339ccc713ce305a14882154dd6e6) - Every test in a Vitest file that fails to load is now `notLoaded`, as Vitest runs none of them: a chain Vitest doesn't have, `sequential` or `bench` from Vitest 5, or options as the third argument from Vitest 4. A call to `bench` is reported where it is made, not where it is imported.
20
+
3
21
  ## 0.1.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # test-inventory
1
+ # <picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Huldoser/test-inventory/main/docs/public/logo-dark.svg"><img src="https://raw.githubusercontent.com/Huldoser/test-inventory/main/docs/public/logo-light.svg" alt="test-inventory" height="48"></picture>
2
2
 
3
3
  Test metadata from source code, not from test results.
4
4
 
5
5
  test-inventory reads Playwright and Vitest test files and lists every test declaration as JSON: its title, file and
6
6
  lines, the describes around it, its tags and annotations, and whether it is active, skipped, fixme, todo, expected
7
- to fail or in a file Playwright refuses to load, with the condition when the state only applies sometimes. It parses
7
+ to fail or in a file the runner refuses to load, with the condition when the state only applies sometimes. It parses
8
8
  the code and runs nothing, so it needs no browsers, no build and no runner configuration.
9
9
 
10
10
  Documentation: [huldoser.github.io/test-inventory](https://huldoser.github.io/test-inventory/)
package/dist/cli.js CHANGED
@@ -130,8 +130,9 @@ export async function run(argv, io) {
130
130
  io.stderr.write(`test-inventory: unexpected error: ${error instanceof Error ? String(error.stack) : String(error)}\n`);
131
131
  return FAILED;
132
132
  }
133
- if (inventory.diagnostics.some((diagnostic) => diagnostic.code === 'no-files-matched')) {
134
- io.stderr.write('test-inventory: no test files match the patterns.\n');
133
+ const unmatched = inventory.diagnostics.find((diagnostic) => diagnostic.code === 'no-files-matched');
134
+ if (unmatched) {
135
+ io.stderr.write(`test-inventory: ${unmatched.message[0].toLowerCase()}${unmatched.message.slice(1)}\n`);
135
136
  }
136
137
  // A reader that stops early, as `| head` does, closes the pipe: an EPIPE error, which ends the output quietly.
137
138
  const quiet = () => undefined;
@@ -3,7 +3,10 @@ import type { SourceText } from './source.ts';
3
3
  import type { Diagnostic, DiagnosticCode, DiagnosticLevel, Framework } from './types.ts';
4
4
  /** `error` when tests may be missing from the output; `warning` when a value is missing or approximate. */
5
5
  export declare const LEVELS: Record<DiagnosticCode, DiagnosticLevel>;
6
- /** Problems for which the runner refuses to load the whole file, so that none of its tests run. */
6
+ /**
7
+ * Problems for which the runner refuses to load the whole file, so that none of its tests run. In Vitest, these are
8
+ * calls that throw while the file is collected.
9
+ */
7
10
  export declare const REJECTS_FILE: Record<Framework, ReadonlySet<DiagnosticCode>>;
8
11
  export declare function toDiagnostic(problem: Problem, relativeFilePath: string, source: SourceText | null, testId?: string | null): Diagnostic;
9
12
  export declare function fileDiagnostic(code: DiagnosticCode, relativeFilePath: string, message: string): Diagnostic;
@@ -34,11 +34,15 @@ export const LEVELS = {
34
34
  'no-files-matched': 'warning',
35
35
  'unsupported-file': 'warning',
36
36
  'approximate-framework-version': 'warning',
37
+ 'test-in-function': 'warning',
37
38
  };
38
- /** Problems for which the runner refuses to load the whole file, so that none of its tests run. */
39
+ /**
40
+ * Problems for which the runner refuses to load the whole file, so that none of its tests run. In Vitest, these are
41
+ * calls that throw while the file is collected.
42
+ */
39
43
  export const REJECTS_FILE = {
40
44
  playwright: new Set(['test-without-body', 'tag-without-at', 'invalid-chain', 'duplicate-title']),
41
- vitest: new Set(),
45
+ vitest: new Set(['invalid-chain', 'removed-api']),
42
46
  };
43
47
  export function toDiagnostic(problem, relativeFilePath, source, testId = null) {
44
48
  const position = problem.node && source ? source.position(problem.node.start) : null;
package/dist/extract.js CHANGED
@@ -3,6 +3,14 @@ import { exportName } from "./frameworks/bindings.js";
3
3
  import { problem } from "./frameworks/common.js";
4
4
  import { snippet, splitLines } from "./source.js";
5
5
  import { isScopeNode } from "./values.js";
6
+ /** Whether a function is called where it is written, so that its tests are registered there. */
7
+ function calledWhereWritten(call, written) {
8
+ return (call.scope === written.scope &&
9
+ call.helper === written.helper &&
10
+ call.guards === written.guards &&
11
+ call.isInLoop === written.isInLoop &&
12
+ !call.inBody);
13
+ }
6
14
  const LOOP_METHODS = new Set(['forEach', 'map']);
7
15
  /** Joins conditions with `&&`, adding parentheses where a part could otherwise be read differently. */
8
16
  function joinConditions(parts) {
@@ -30,6 +38,12 @@ class Walker {
30
38
  values;
31
39
  effects = new Map();
32
40
  helperCalls = [];
41
+ /** Where each function with a name is written, outside test and hook bodies. */
42
+ written = new Map();
43
+ /** Every call by name to a function declared in the file. */
44
+ callSites = new Map();
45
+ /** The function with a name each test is declared in, which registers the test when it is called. */
46
+ declaredIn = new Map();
33
47
  constructor(parsed, adapter, values) {
34
48
  this.parsed = parsed;
35
49
  this.adapter = adapter;
@@ -55,8 +69,11 @@ class Walker {
55
69
  isInCondition: false,
56
70
  isInFunction: false,
57
71
  helper: null,
72
+ declaredIn: null,
58
73
  });
59
- this.applyHelpers();
74
+ const exported = this.exportedFunctions(this.parsed.program);
75
+ this.applyHelpers(exported);
76
+ this.checkFunctionTests(exported);
60
77
  return {
61
78
  suites: this.suites,
62
79
  tests: this.tests,
@@ -134,17 +151,36 @@ class Walker {
134
151
  case 'ArrowFunctionExpression': {
135
152
  // A function outside any test or hook runs when it is called; what it sets applies where it is called.
136
153
  const helper = context.test || context.hookScope ? null : node;
154
+ const named = helper !== null && this.isNamed(node);
155
+ if (named)
156
+ this.written.set(node, { ...this.placement(context), declaredIn: context.declaredIn });
137
157
  this.visitChildren(node, {
138
158
  ...context,
139
159
  isInFunction: true,
140
160
  helper: helper ?? context.helper,
141
161
  guards: helper ? [] : context.guards,
162
+ declaredIn: named ? node : context.declaredIn,
142
163
  });
143
164
  return;
144
165
  }
145
166
  }
146
167
  this.visitChildren(node, context);
147
168
  }
169
+ /** Whether a function has a name it can be called by: `function name() {}` or `const name = () => {}`. */
170
+ isNamed(fn) {
171
+ const parent = this.parents.at(-2);
172
+ return ((fn.type === 'FunctionDeclaration' && fn.id !== null) ||
173
+ (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.init === fn));
174
+ }
175
+ placement(context) {
176
+ return {
177
+ scope: context.scope,
178
+ helper: context.helper,
179
+ guards: joinConditions(context.guards),
180
+ isInLoop: context.isInLoop,
181
+ inBody: context.test !== null || context.hookScope !== null,
182
+ };
183
+ }
148
184
  guarded(context, guard) {
149
185
  return { ...context, isInCondition: true, guards: guard === null ? context.guards : [...context.guards, guard] };
150
186
  }
@@ -266,6 +302,9 @@ class Walker {
266
302
  const fn = callee.type === 'Identifier' ? this.functionNamed(callee.name, this.scopes) : null;
267
303
  if (!fn)
268
304
  return;
305
+ const sites = this.callSites.get(fn) ?? [];
306
+ sites.push(this.placement(context));
307
+ this.callSites.set(fn, sites);
269
308
  if (context.helper) {
270
309
  this.effectsOf(context.helper).push({ kind: 'call', fn, guards: context.guards });
271
310
  }
@@ -307,8 +346,7 @@ class Walker {
307
346
  * Applies what helper functions set to the tests, hooks and describes that call them, through nested helpers too,
308
347
  * with the conditions around each call. A helper no test or hook reaches, or that the file exports, is reported.
309
348
  */
310
- applyHelpers() {
311
- const exported = this.exportedFunctions(this.parsed.program);
349
+ applyHelpers(exported) {
312
350
  const reached = new Set();
313
351
  const expand = (fn, guards, path) => {
314
352
  if (path.has(fn) || exported.has(fn))
@@ -340,6 +378,35 @@ class Walker {
340
378
  this.problems.push(problem('state-in-helper', own.call, `${callee}() is in a function that ${why}; it applies to the tests that call the function, so it is not applied here.`, callee));
341
379
  }
342
380
  }
381
+ /** Why calling a function with a name doesn't register its tests where they are written; null when it does. */
382
+ misplaced(fn, exported) {
383
+ if (exported.has(fn))
384
+ return 'the file exports the function';
385
+ const sites = this.callSites.get(fn) ?? [];
386
+ if (sites.length === 0)
387
+ return 'nothing in the file calls the function by name';
388
+ if (sites.length > 1)
389
+ return `the function is called ${sites.length} times`;
390
+ const written = this.written.get(fn);
391
+ return calledWhereWritten(sites[0], written)
392
+ ? null
393
+ : 'the function is called in another place than where it is written';
394
+ }
395
+ /**
396
+ * Warns about tests declared in a function with a name, or in one around it, whose calls register them somewhere
397
+ * else than where they are written: the runner adds a test to the describe around each call.
398
+ */
399
+ checkFunctionTests(exported) {
400
+ for (const [test, innermost] of this.declaredIn) {
401
+ let why = null;
402
+ for (let fn = innermost; fn && !why; fn = this.written.get(fn).declaredIn) {
403
+ why = this.misplaced(fn, exported);
404
+ }
405
+ if (!why)
406
+ continue;
407
+ test.problems.push(problem('test-in-function', test.call, `The test is declared in a function, and ${why}. The runner registers the test at each call of the function, in the describe around the call; it is listed once, where it is written.`));
408
+ }
409
+ }
343
410
  nested(call, context) {
344
411
  if (!context.test && !context.hookScope)
345
412
  return false;
@@ -363,6 +430,8 @@ class Walker {
363
430
  problems: [...declaration.problems],
364
431
  };
365
432
  this.tests.push(test);
433
+ if (context.declaredIn)
434
+ this.declaredIn.set(test, context.declaredIn);
366
435
  context.scope.suite?.cuts.push(statement);
367
436
  if (declaration.fn) {
368
437
  this.visitCallback(declaration.fn, {
package/dist/files.js CHANGED
@@ -59,6 +59,9 @@ export async function findFiles(root, patterns, ignore, framework) {
59
59
  for (const failure of failures) {
60
60
  const where = relative(failure.path);
61
61
  if (failure.kind === 'folder') {
62
+ // A folder a pattern names that isn't there holds no tests; `no-files-matched` reports a scan that found none.
63
+ if (failure.error.code === 'ENOENT' || failure.error.code === 'ENOTDIR')
64
+ continue;
62
65
  const message = `The folder could not be read: ${readFailure(failure.error)}; test files in it are missing.`;
63
66
  diagnostics.push(fileDiagnostic('read-error', `${where}/`, message));
64
67
  }
@@ -345,7 +345,7 @@ export class PlaywrightAdapter {
345
345
  const callee = snippet(text(this.file, call.callee));
346
346
  return {
347
347
  kind: 'invalid',
348
- problem: problem('test-without-body', call, `${callee}() has no test body, so Playwright fails to load the file.`),
348
+ problem: problem('test-without-body', call, `${callee}() has no test body, so Playwright fails to load the file and runs no tests at all.`),
349
349
  };
350
350
  }
351
351
  const state = STATES.get(key) ?? null;
@@ -468,7 +468,7 @@ export class PlaywrightAdapter {
468
468
  details.tags.push(value);
469
469
  }
470
470
  else {
471
- problems.push(problem('tag-without-at', tag.node, `Tag "${value}" does not start with "@", so Playwright fails to load the file.`, value));
471
+ problems.push(problem('tag-without-at', tag.node, `Tag "${value}" does not start with "@", so Playwright fails to load the file and runs no tests at all.`, value));
472
472
  }
473
473
  }
474
474
  for (const { node: unresolved, importedFrom } of tags.unresolved) {
@@ -19,6 +19,8 @@ export declare class VitestAdapter implements Adapter {
19
19
  private warnedAboutTags;
20
20
  constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
21
21
  classify(call: CallExpression, context: CallContext): ApiCall | null;
22
+ /** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
23
+ private benchCall;
22
24
  private chainCall;
23
25
  /**
24
26
  * The function a chain calls and the links after it, or null when it isn't one of Vitest's. An import that can't be
@@ -33,6 +33,8 @@ class VitestBindings {
33
33
  kinds = new Map();
34
34
  hooks = new Map();
35
35
  namespaces = new Set();
36
+ /** Names `bench` is imported as from Vitest 5, which no longer exports it. */
37
+ benches = new Set();
36
38
  /** The import specifiers and declarators that bind those names, to tell them from local names that shadow them. */
37
39
  declarations = new Set();
38
40
  problems = [];
@@ -55,7 +57,7 @@ class VitestBindings {
55
57
  this.problems.push(problem('invalid-import', specifier, "Vitest has no default export; import { test, describe } from 'vitest' instead."));
56
58
  }
57
59
  else {
58
- this.bindImported(exportName(specifier.imported), specifier.local.name, specifier, version);
60
+ this.bindImported(exportName(specifier.imported), specifier.local.name, version);
59
61
  }
60
62
  }
61
63
  }
@@ -100,7 +102,7 @@ class VitestBindings {
100
102
  const node = unwrap(expression);
101
103
  return node.type === 'Identifier' && this.namespaces.has(node.name);
102
104
  }
103
- bindImported(imported, local, node, version) {
105
+ bindImported(imported, local, version) {
104
106
  const kind = KINDS.get(imported);
105
107
  if (kind) {
106
108
  this.kinds.set(local, kind);
@@ -109,7 +111,7 @@ class VitestBindings {
109
111
  this.hooks.set(local, imported);
110
112
  }
111
113
  else if (imported === 'bench' && atLeast(version, 5, 0)) {
112
- this.problems.push(problem('removed-api', node, "Vitest 5 no longer exports bench from 'vitest'; benchmarks in this file are not tests."));
114
+ this.benches.add(local);
113
115
  }
114
116
  }
115
117
  bind(declarator) {
@@ -166,7 +168,24 @@ export class VitestAdapter {
166
168
  }
167
169
  }
168
170
  classify(call, context) {
169
- return this.hookCall(call, context) ?? this.contextSkip(call, context) ?? this.chainCall(call, context);
171
+ return (this.benchCall(call, context) ??
172
+ this.hookCall(call, context) ??
173
+ this.contextSkip(call, context) ??
174
+ this.chainCall(call, context));
175
+ }
176
+ /** A call to `bench` imported from Vitest 5, which is undefined there, so the call throws. */
177
+ benchCall(call, context) {
178
+ const base = readChain(call.callee)?.base;
179
+ if (base?.type !== 'Identifier' || !this.bindings.benches.has(base.name))
180
+ return null;
181
+ const declaration = this.file.values.declarationOf(base.name, context.scopes);
182
+ if (declaration === null || !this.bindings.declarations.has(declaration))
183
+ return null;
184
+ const callee = snippet(text(this.file, call.callee));
185
+ return {
186
+ kind: 'invalid',
187
+ problem: problem('removed-api', call, `Vitest 5 no longer exports bench from 'vitest'; ${callee}(...) fails. Benchmarks are not tests.`, callee),
188
+ };
170
189
  }
171
190
  chainCall(call, context) {
172
191
  const chain = readChain(call.callee);
package/dist/records.js CHANGED
@@ -231,7 +231,7 @@ class RecordBuilder {
231
231
  seen.set(group, keys);
232
232
  if (keys.has(key)) {
233
233
  const message = playwright
234
- ? 'Another test in the file has the same title path, so Playwright fails to load the file.'
234
+ ? 'Another test in the file has the same title path, so Playwright fails to load the file and runs no tests at all.'
235
235
  : 'Another test in the same describe has the same title.';
236
236
  const item = problem('duplicate-title', test.call, message, test.declaration.title.text);
237
237
  found.push({ index, problem: playwright ? { ...item, level: 'error' } : item });
package/dist/resolve.js CHANGED
@@ -179,7 +179,8 @@ export function moduleResolver(root) {
179
179
  return (fromFile) => (specifier) => {
180
180
  const directory = path.dirname(fromFile);
181
181
  let candidates;
182
- if (specifier.startsWith('./') || specifier.startsWith('../')) {
182
+ // `.` and `..` are paths too: the index of the file's folder, or of the folder above.
183
+ if (/^\.\.?(?:\/|$)/.test(specifier)) {
183
184
  candidates = [path.resolve(directory, specifier)];
184
185
  }
185
186
  else if (specifier.startsWith('#')) {
package/dist/scan.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, statSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { fileDiagnostic } from "./diagnostics.js";
5
- import { findFiles, readFailure } from "./files.js";
5
+ import { findFiles, normalizePattern, readFailure } from "./files.js";
6
6
  import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
7
7
  import { createInventory } from "./inventory.js";
8
8
  import { checkComments, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, } from "./options.js";
@@ -18,7 +18,9 @@ function versionDiagnostics(framework, version, source, range) {
18
18
  }
19
19
  const [major, minor] = majorMinor(version);
20
20
  const [oldestMajor, oldestMinor] = OLDEST[framework];
21
- if (major < oldestMajor || (major === oldestMajor && minor < oldestMinor)) {
21
+ // A range gets the newest rules of its major, so only its major has to be supported.
22
+ const tooOld = major < oldestMajor || (major === oldestMajor && minor < oldestMinor && range === null);
23
+ if (tooOld) {
22
24
  return [
23
25
  fileDiagnostic('unsupported-framework-version', source, `${name} ${version} is older than ${oldestMajor}.${oldestMinor}, the oldest supported version; the results follow ${oldestMajor}.${oldestMinor}.`),
24
26
  ];
@@ -29,6 +31,17 @@ function versionDiagnostics(framework, version, source, range) {
29
31
  fileDiagnostic('approximate-framework-version', source, `Only the range "${range}" was found for ${name}, so the rules of the newest ${major}.x release apply.`),
30
32
  ];
31
33
  }
34
+ /** For a pattern that names a folder, which matches no file, a pattern that matches the test files in it. */
35
+ function folderHint(root, patterns, framework) {
36
+ const folder = patterns.find((pattern) => {
37
+ const target = path.resolve(root, normalizePattern(pattern));
38
+ return existsSync(target) && statSync(target).isDirectory();
39
+ });
40
+ if (folder === undefined)
41
+ return '';
42
+ const example = `${folder.replace(/[\\/]+$/, '')}/**/*.${framework === 'playwright' ? 'spec' : 'test'}.ts`;
43
+ return ` "${folder}" is a folder; to scan the test files in it, use a pattern such as "${example}".`;
44
+ }
32
45
  /**
33
46
  * Scans the test files that match `patterns` under `root`, one file at a time.
34
47
  *
@@ -60,7 +73,8 @@ export async function scan(options) {
60
73
  projectDiagnostics.push(...found.diagnostics);
61
74
  if (found.files.length === 0) {
62
75
  const list = patterns.map((pattern) => `"${pattern}"`).join(', ');
63
- projectDiagnostics.push(fileDiagnostic('no-files-matched', '.', `No test files match ${list} in the root.`));
76
+ const message = `No test files match ${list} in the root.${folderHint(root, patterns, framework)}`;
77
+ projectDiagnostics.push(fileDiagnostic('no-files-matched', '.', message));
64
78
  }
65
79
  const readerFor = moduleResolver(root);
66
80
  const results = [];
package/dist/types.d.ts CHANGED
@@ -20,7 +20,7 @@ export type TestState = 'active' | 'skip' | 'fixme' | 'todo' | 'expectedToFail'
20
20
  export type StateSource = 'test' | 'suite' | 'file';
21
21
  /** `error`: tests may be missing from the output. `warning`: a value is missing or approximate. */
22
22
  export type DiagnosticLevel = 'error' | 'warning';
23
- 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' | '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';
23
+ 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' | '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';
24
24
  export type MetaValue = string | number | boolean | null | MetaValue[] | {
25
25
  [key: string]: MetaValue;
26
26
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-inventory",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Test metadata from source code, not from test results.",
5
5
  "keywords": [
6
6
  "testing",
@@ -65,6 +65,7 @@
65
65
  "licenses": "node scripts/check-licenses.ts",
66
66
  "compare": "node scripts/compare-runners.ts",
67
67
  "corpus": "node scripts/scan-corpus.ts",
68
+ "release": "node scripts/release.ts",
68
69
  "lint": "eslint .",
69
70
  "lint:fix": "eslint . --fix",
70
71
  "format": "prettier --write . --ignore-path .gitignore --ignore-path .prettierignore --ignore-path .git/info/exclude",
package/schema/v1.json CHANGED
@@ -880,7 +880,8 @@
880
880
  "dynamic-reason",
881
881
  "no-files-matched",
882
882
  "unsupported-file",
883
- "approximate-framework-version"
883
+ "approximate-framework-version",
884
+ "test-in-function"
884
885
  ]
885
886
  },
886
887
  {