test-inventory 0.4.0 → 0.4.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,20 @@
1
1
  # test-inventory
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#48](https://github.com/Huldoser/test-inventory/pull/48) [`6549682`](https://github.com/Huldoser/test-inventory/commit/65496820548c0980cef2f290f6d54bd89160236b) - A hook given a function the file declares, as in `test.beforeEach(skipOnCI)` or `beforeEach(requireBroker)`, is read
8
+ like one written in place: the skips, timeouts and other settings it sets apply to the tests it runs before, and its
9
+ fixtures are in their `fixtures`. Before, they were missed. A test or hook function passed by name no longer gets a
10
+ `state-in-helper` warning saying no test, hook or describe in the file calls it.
11
+
12
+ - [#47](https://github.com/Huldoser/test-inventory/pull/47) [`a603623`](https://github.com/Huldoser/test-inventory/commit/a603623b2431e51c3e48ae0413964d5a7125de59) - A `#` import or workspace package export whose target repeats the `*` of its pattern, as in
13
+ `"#desks/*": "./desks/*/*-fixtures.ts"`, is followed to the file Node.js loads, with every `*` filled in. Only the
14
+ first was filled in before, so the import wasn't followed and its tests were missing, with an `unresolved-test-import`
15
+ error. A folder given as the pattern with a long run of slashes, as in `tests////unit`, no longer slows the scan down
16
+ while the hint for it is written.
17
+
3
18
  ## 0.4.0
4
19
 
5
20
  ### Minor Changes
package/dist/extract.js CHANGED
@@ -110,7 +110,10 @@ class Walker {
110
110
  declaredIn = new Map();
111
111
  deferred = [];
112
112
  isDeferred = new Set();
113
- /** Functions walked at one of their calls, so that their tests are recorded where they are registered. */
113
+ /**
114
+ * Functions walked at one of their calls, or as a test or hook body passed by name, so that their tests are recorded
115
+ * where they are registered and what they set isn't reported as unapplied.
116
+ */
114
117
  walkedAtCall = new Set();
115
118
  /** The functions being walked at their calls, innermost last, to stop recursion. */
116
119
  callStack = [];
@@ -413,6 +416,8 @@ class Walker {
413
416
  context.scope.hookFixtures.push(...api.fixtures);
414
417
  }
415
418
  if (api.fn) {
419
+ if (!call.arguments.includes(api.fn))
420
+ this.walkedAtCall.add(api.fn);
416
421
  this.visitCallback(api.fn, {
417
422
  ...context,
418
423
  hookScope: context.scope,
@@ -938,6 +943,8 @@ class Walker {
938
943
  // A function declared in the file and passed by name runs as the test body too.
939
944
  const body = declaration.fn ?? declaration.hashed;
940
945
  if (body) {
946
+ if (!declaration.fn)
947
+ this.walkedAtCall.add(body);
941
948
  this.visitCallback(body, {
942
949
  ...context,
943
950
  test,
@@ -75,6 +75,8 @@ export declare function mayDeclareTests(name: string, links: Link[]): boolean;
75
75
  /** A function declared in the file and passed by name, as in `test('fills an order', checkFill)`; null otherwise. */
76
76
  export declare function namedFunction(file: FileContext, scopes: readonly Node[], node: Argument | undefined): FunctionNode | null;
77
77
  export declare function findFunction(args: readonly Argument[], from?: number): FunctionNode | null;
78
+ /** A hook's function: written in the call, or declared in the file and passed by name, as in `beforeEach(signIn)`. */
79
+ export declare function hookFunction(file: FileContext, scopes: readonly Node[], args: readonly Argument[]): FunctionNode | null;
78
80
  /** Reads a title. A title that cannot be worked out keeps its source text and gets a `dynamic-title` warning. */
79
81
  export declare function readTitle(file: FileContext, scopes: readonly Node[], node: Argument, options: {
80
82
  stripTags: boolean;
@@ -166,6 +166,10 @@ export function findFunction(args, from = 0) {
166
166
  }
167
167
  return null;
168
168
  }
169
+ /** A hook's function: written in the call, or declared in the file and passed by name, as in `beforeEach(signIn)`. */
170
+ export function hookFunction(file, scopes, args) {
171
+ return findFunction(args) ?? args.map((arg) => namedFunction(file, scopes, arg)).find((fn) => fn !== null) ?? null;
172
+ }
169
173
  /** Reads a title. A title that cannot be worked out keeps its source text and gets a `dynamic-title` warning. */
170
174
  export function readTitle(file, scopes, node, options) {
171
175
  const expression = node.type === 'SpreadElement' ? null : node;
@@ -1,7 +1,7 @@
1
1
  import { childNodes, isFunctionNode, propertyKey, propertyName, unwrap } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
3
  import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
4
- import { atLeast, findFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
4
+ import { atLeast, findFunction, hookFunction, isTestName, isTitleLiteral, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, namedFunction, origin, problem, readChain, destructuredNames, readCondition, readCount, readOptions, readReason, readStrings, readTitle, refused, returnedFunction, returnedTestObject, text, unresolvedOption, unresolvedOptions, } from "./common.js";
5
5
  const MISMATCHED_SOURCES = new Set(['vitest', '@jest/globals']);
6
6
  // Maps rather than object literals, so names such as `constructor` don't match inherited properties.
7
7
  const DESCRIBES = new Map([
@@ -257,7 +257,7 @@ export class PlaywrightAdapter {
257
257
  if (key === 'describe.configure')
258
258
  return this.configure(call, context);
259
259
  if (HOOKS.has(key)) {
260
- const fn = findFunction(call.arguments);
260
+ const fn = hookFunction(this.file, context.scopes, call.arguments);
261
261
  return { kind: 'hook', name: key, fn, testContext: null, fixtures: destructuredNames(fn, 0) };
262
262
  }
263
263
  if (RUN_MEMBERS.has(key))
@@ -1,7 +1,7 @@
1
1
  import { childNodes, isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap, } from "../ast.js";
2
2
  import { snippet } from "../source.js";
3
3
  import { allDeclarators, bindAll, exportName, followImport, followModule, moduleImports, ModuleFunctions, requiredModule, } from "./bindings.js";
4
- import { atLeast, findFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
4
+ import { atLeast, hookFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, returnedFunction, returnedTestObject, destructuredNames, readCount, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, refused, text, } from "./common.js";
5
5
  /** What a test function chosen at run time, as in `isCI ? it.skip : it`, is either way: only the links both have. */
6
6
  function shared(a, b) {
7
7
  if (!a || a.kind !== b?.kind)
@@ -328,7 +328,7 @@ export class VitestAdapter {
328
328
  return null;
329
329
  const { kind, links } = resolved;
330
330
  if (kind === 'test' && links.length === 1 && isHook(links[0].name))
331
- return this.hook(call, links[0].name);
331
+ return this.hook(call, links[0].name, context);
332
332
  if (links.length === 1 && DERIVATIONS.has(links[0].name))
333
333
  return { kind: 'other' };
334
334
  const last = links.at(-1);
@@ -514,10 +514,10 @@ export class VitestAdapter {
514
514
  const hook = declaration === null
515
515
  ? isHook(callee.name) && callee.name
516
516
  : this.bindings.declarations.has(declaration) && this.bindings.hooks.get(callee.name);
517
- return hook ? this.hook(call, hook) : null;
517
+ return hook ? this.hook(call, hook, context) : null;
518
518
  }
519
- hook(call, name) {
520
- const fn = findFunction(call.arguments);
519
+ hook(call, name, context) {
520
+ const fn = hookFunction(this.file, context.scopes, call.arguments);
521
521
  const index = HOOKS.get(name);
522
522
  return {
523
523
  kind: 'hook',
package/dist/resolve.js CHANGED
@@ -180,7 +180,7 @@ function aliasFiles(specifier, options) {
180
180
  const match = options.paths ? matchPattern(Object.keys(options.paths), specifier) : null;
181
181
  if (!match || !options.paths)
182
182
  return null;
183
- return targetStrings(options.paths[match.key]).map((target) => path.resolve(options.pathsBase, target.replaceAll('${configDir}', options.configDir).replace('*', match.star)));
183
+ return targetStrings(options.paths[match.key]).map((target) => path.resolve(options.pathsBase, target.replaceAll('${configDir}', options.configDir).replaceAll('*', match.star)));
184
184
  }
185
185
  /** Files a `#name` import may stand for, from `imports` in the nearest package.json. */
186
186
  function subpathImportFiles(specifier, directory) {
@@ -191,7 +191,7 @@ function subpathImportFiles(specifier, directory) {
191
191
  const match = imports ? matchPattern(Object.keys(imports), specifier) : null;
192
192
  if (!match || !imports)
193
193
  return [];
194
- return targetStrings(imports[match.key]).map((target) => path.resolve(folder, target.replace('*', match.star)));
194
+ return targetStrings(imports[match.key]).map((target) => path.resolve(folder, target.replaceAll('*', match.star)));
195
195
  }
196
196
  if (path.dirname(folder) === folder)
197
197
  return [];
@@ -223,7 +223,7 @@ function packageEntry(folder, subpath) {
223
223
  let targets;
224
224
  if (isSubpathMap) {
225
225
  const match = matchPattern(Object.keys(map), subpath);
226
- targets = match ? targetStrings(map[match.key]).map((target) => target.replace('*', match.star)) : [];
226
+ targets = match ? targetStrings(map[match.key]).map((target) => target.replaceAll('*', match.star)) : [];
227
227
  }
228
228
  else {
229
229
  targets = subpath === '.' ? targetStrings(exports) : [];
package/dist/scan.js CHANGED
@@ -39,7 +39,11 @@ function folderHint(root, patterns, framework) {
39
39
  });
40
40
  if (folder === undefined)
41
41
  return '';
42
- const example = `${folder.replace(/[\\/]+$/, '')}/**/*.${framework === 'playwright' ? 'spec' : 'test'}.ts`;
42
+ // A loop, as a regular expression for trailing slashes takes quadratic time on a long run of slashes.
43
+ let end = folder.length;
44
+ while (end > 0 && '/\\'.includes(folder[end - 1]))
45
+ end -= 1;
46
+ const example = `${folder.slice(0, end)}/**/*.${framework === 'playwright' ? 'spec' : 'test'}.ts`;
43
47
  return ` "${folder}" is a folder; to scan the test files in it, use a pattern such as "${example}".`;
44
48
  }
45
49
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-inventory",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Test metadata from source code, not from test results.",
5
5
  "keywords": [
6
6
  "testing",