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 +18 -0
- package/README.md +2 -2
- package/dist/cli.js +3 -2
- package/dist/diagnostics.d.ts +4 -1
- package/dist/diagnostics.js +6 -2
- package/dist/extract.js +72 -3
- package/dist/files.js +3 -0
- package/dist/frameworks/playwright.js +2 -2
- package/dist/frameworks/vitest.d.ts +2 -0
- package/dist/frameworks/vitest.js +23 -4
- package/dist/records.js +1 -1
- package/dist/resolve.js +2 -1
- package/dist/scan.js +17 -3
- package/dist/types.d.ts +1 -1
- package/package.json +2 -1
- package/schema/v1.json +2 -1
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
|
|
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
|
-
|
|
134
|
-
|
|
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;
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/diagnostics.js
CHANGED
|
@@ -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
|
-
/**
|
|
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.
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|