test-inventory 0.1.1 → 0.1.2
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 +8 -0
- package/dist/extract.js +16 -7
- package/dist/frameworks/playwright.js +24 -10
- package/dist/frameworks/vitest.js +57 -1
- package/dist/model.d.ts +7 -3
- package/dist/options.d.ts +2 -0
- package/dist/records.d.ts +1 -0
- package/dist/records.js +5 -1
- package/dist/types.d.ts +16 -3
- package/package.json +1 -1
- package/schema/v1.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# test-inventory
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#13](https://github.com/Huldoser/test-inventory/pull/13) [`89a029f`](https://github.com/Huldoser/test-inventory/commit/89a029f9e45c2aaa57b5f907717bb8f00829e335) - Locks set with `test.describe.configure({ lock })`, new in Playwright 1.64, are added to every test in the file or describe where the call is made. They were dropped before.
|
|
8
|
+
|
|
9
|
+
- [#13](https://github.com/Huldoser/test-inventory/pull/13) [`89a029f`](https://github.com/Huldoser/test-inventory/commit/89a029f9e45c2aaa57b5f907717bb8f00829e335) - From Vitest 5, a file that calls `vi.mock`, `vi.unmock` or `vi.hoisted` anywhere but its top level gets a `removed-api` error at the call, and its tests are `notLoaded`, as Vitest refuses the file. Files with in-source tests are not checked, as in Vitest.
|
|
10
|
+
|
|
3
11
|
## 0.1.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/dist/extract.js
CHANGED
|
@@ -12,6 +12,12 @@ function calledWhereWritten(call, written) {
|
|
|
12
12
|
!call.inBody);
|
|
13
13
|
}
|
|
14
14
|
const LOOP_METHODS = new Set(['forEach', 'map']);
|
|
15
|
+
/** What `test.describe.configure()` sets on the describe or file it is called in: the last mode wins, locks add up. */
|
|
16
|
+
function configure(scope, settings) {
|
|
17
|
+
if (settings.mode)
|
|
18
|
+
scope.mode = settings.mode;
|
|
19
|
+
scope.details.locks.push(...settings.locks);
|
|
20
|
+
}
|
|
15
21
|
/** Joins conditions with `&&`, adding parentheses where a part could otherwise be read differently. */
|
|
16
22
|
function joinConditions(parts) {
|
|
17
23
|
if (parts.length <= 1)
|
|
@@ -251,11 +257,14 @@ class Walker {
|
|
|
251
257
|
(context.test ?? context.hookScope ?? context.scope).states.push(setting);
|
|
252
258
|
return true;
|
|
253
259
|
}
|
|
254
|
-
case '
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
case 'configure':
|
|
261
|
+
this.problems.push(...api.problems);
|
|
262
|
+
if (context.helper) {
|
|
263
|
+
this.effectsOf(context.helper).push({ kind: 'configure', mode: api.mode, locks: api.locks, call });
|
|
264
|
+
}
|
|
265
|
+
else if (!context.test && !context.hookScope) {
|
|
266
|
+
configure(context.scope, api);
|
|
267
|
+
}
|
|
259
268
|
return true;
|
|
260
269
|
case 'hook':
|
|
261
270
|
if (context.test)
|
|
@@ -355,7 +364,7 @@ class Walker {
|
|
|
355
364
|
return (this.effects.get(fn) ?? []).flatMap((effect) => {
|
|
356
365
|
if (effect.kind === 'call')
|
|
357
366
|
return expand(effect.fn, [...guards, ...effect.guards], new Set([...path, fn]));
|
|
358
|
-
if (effect.kind === '
|
|
367
|
+
if (effect.kind === 'configure')
|
|
359
368
|
return [effect];
|
|
360
369
|
const condition = joinConditions([...guards, ...(effect.setting.condition ? [effect.setting.condition] : [])]);
|
|
361
370
|
return [{ ...effect, setting: { ...effect.setting, condition } }];
|
|
@@ -366,7 +375,7 @@ class Walker {
|
|
|
366
375
|
if (effect.kind === 'state')
|
|
367
376
|
call.target.states.push(effect.setting);
|
|
368
377
|
else if (call.scope)
|
|
369
|
-
call.scope
|
|
378
|
+
configure(call.scope, effect);
|
|
370
379
|
}
|
|
371
380
|
}
|
|
372
381
|
for (const [fn, effects] of this.effects) {
|
|
@@ -429,17 +429,31 @@ export class PlaywrightAdapter {
|
|
|
429
429
|
}
|
|
430
430
|
configure(call, context) {
|
|
431
431
|
const options = call.arguments[0] ? readOptions(this.file, context.scopes, call.arguments[0]) : null;
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
432
|
+
const option = options?.values.get('mode');
|
|
433
|
+
const mode = option?.resolved.ok && ['parallel', 'serial', 'default'].includes(option.resolved.value)
|
|
434
|
+
? option.resolved.value
|
|
435
|
+
: null;
|
|
436
|
+
const problems = mode
|
|
437
|
+
? []
|
|
438
|
+
: [
|
|
439
|
+
...(options ? unresolvedOptions(this.file, options, 'the mode is unknown') : []),
|
|
440
|
+
...(option && !option.resolved.ok
|
|
441
|
+
? [unresolvedOption(this.file, 'mode', option.node, option.resolved.importedFrom, 'the mode is unknown')]
|
|
442
|
+
: []),
|
|
443
|
+
];
|
|
444
|
+
const locks = [];
|
|
445
|
+
const lock = options?.values.get('lock');
|
|
446
|
+
if (lock && atLeast(this.file.version, 1, 64)) {
|
|
447
|
+
const strings = readStrings(this.file, context.scopes, lock);
|
|
448
|
+
locks.push(...strings.values);
|
|
449
|
+
for (const { node: unresolved, importedFrom } of strings.unresolved) {
|
|
450
|
+
problems.push(unresolvedOption(this.file, 'lock', unresolved, importedFrom, 'the lock is missing'));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!mode && locks.length === 0) {
|
|
454
|
+
return problems.length > 0 ? { kind: 'invalid', problem: problems[0] } : { kind: 'other' };
|
|
435
455
|
}
|
|
436
|
-
|
|
437
|
-
...(options ? unresolvedOptions(this.file, options, 'the mode is unknown') : []),
|
|
438
|
-
...(mode && !mode.resolved.ok
|
|
439
|
-
? [unresolvedOption(this.file, 'mode', mode.node, mode.resolved.importedFrom, 'the mode is unknown')]
|
|
440
|
-
: []),
|
|
441
|
-
];
|
|
442
|
-
return problems.length > 0 ? { kind: 'invalid', problem: problems[0] } : { kind: 'other' };
|
|
456
|
+
return { kind: 'configure', mode, locks, problems };
|
|
443
457
|
}
|
|
444
458
|
/** `test.info().annotations.push(...)` inside a test body. */
|
|
445
459
|
annotationsPush(call, links, context) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isFunctionNode, isImportMetaVitest, propertyKey, propertyName, unwrap } from "../ast.js";
|
|
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, moduleImports, requiredModule, } from "./bindings.js";
|
|
4
4
|
import { atLeast, findFunction, isTestName, localTestObject, looksLikeDeclaration, mayDeclareTests, mayHoldTestObject, origin, problem, readChain, readCondition, readOptions, readStrings, isTitleLiteral, readReason, namedFunction, unresolvedOption, unresolvedOptions, readTitle, text, } from "./common.js";
|
|
@@ -28,6 +28,56 @@ const HOOKS = new Map([
|
|
|
28
28
|
]);
|
|
29
29
|
/** First members that show a name imported from a local file is used as Vitest's `test`. */
|
|
30
30
|
const TEST_MEMBERS = new Set([...MODIFIERS.test, ...MODIFIERS.describe, ...TABLES, ...CONDITIONS, ...KINDS.keys()]);
|
|
31
|
+
/** Calls Vitest moves to the top of the file before running it: `vi.mock`, `vi.unmock` and `vi.hoisted`. */
|
|
32
|
+
const HOISTED_OBJECTS = new Set(['vi', 'vitest']);
|
|
33
|
+
const HOISTED_METHODS = new Set(['mock', 'unmock', 'hoisted']);
|
|
34
|
+
/**
|
|
35
|
+
* The hoisted calls that are not written at the top level of the file, where Vitest moves them. Vitest 5 fails such a
|
|
36
|
+
* file while it loads it. As in Vitest, `const value = vi.hoisted(...)` and `await vi.hoisted(...)` count as top
|
|
37
|
+
* level, and a file with in-source tests is never checked.
|
|
38
|
+
*/
|
|
39
|
+
function nestedHoistedCalls(program) {
|
|
40
|
+
if (hasInSourceTests(program))
|
|
41
|
+
return [];
|
|
42
|
+
const topLevel = new Set(program.body);
|
|
43
|
+
for (const statement of program.body) {
|
|
44
|
+
if (statement.type === 'ExpressionStatement')
|
|
45
|
+
topLevel.add(statement.expression);
|
|
46
|
+
}
|
|
47
|
+
const found = [];
|
|
48
|
+
const parents = [];
|
|
49
|
+
const visit = (node) => {
|
|
50
|
+
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
|
|
51
|
+
const { object, property } = node.callee;
|
|
52
|
+
const method = !node.callee.computed && property.type === 'Identifier' ? property.name : '';
|
|
53
|
+
if (object.type === 'Identifier' && HOISTED_OBJECTS.has(object.name) && HOISTED_METHODS.has(method)) {
|
|
54
|
+
if (!topLevel.has(method === 'hoisted' ? hoistedValue(node, parents) : node))
|
|
55
|
+
found.push(node);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
parents.push(node);
|
|
59
|
+
for (const child of childNodes(node))
|
|
60
|
+
visit(child);
|
|
61
|
+
parents.pop();
|
|
62
|
+
};
|
|
63
|
+
visit(program);
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
/** Whether the file has in-source tests, which read `import.meta.vitest`. */
|
|
67
|
+
function hasInSourceTests(node) {
|
|
68
|
+
return (node.type === 'MemberExpression' && isImportMetaVitest(node)) || childNodes(node).some(hasInSourceTests);
|
|
69
|
+
}
|
|
70
|
+
/** What Vitest moves for a `vi.hoisted` call: the declaration it initializes, the `await` around it, or the call. */
|
|
71
|
+
function hoistedValue(call, parents) {
|
|
72
|
+
const declaration = parents.findLast((node) => node.type === 'VariableDeclaration');
|
|
73
|
+
const init = declaration?.type === 'VariableDeclaration' ? declaration.declarations[0]?.init : null;
|
|
74
|
+
const value = init ? unwrap(init) : null;
|
|
75
|
+
if (declaration && (value === call || (value?.type === 'AwaitExpression' && unwrap(value.argument) === call))) {
|
|
76
|
+
return declaration;
|
|
77
|
+
}
|
|
78
|
+
const parent = parents.at(-1);
|
|
79
|
+
return parent?.type === 'AwaitExpression' ? parent : call;
|
|
80
|
+
}
|
|
31
81
|
/** The names a file binds to Vitest's `test`, `describe`, hooks and namespace. */
|
|
32
82
|
class VitestBindings {
|
|
33
83
|
kinds = new Map();
|
|
@@ -158,6 +208,12 @@ export class VitestAdapter {
|
|
|
158
208
|
this.readModule = readModule;
|
|
159
209
|
this.bindings = new VitestBindings(parsed.program, file.version);
|
|
160
210
|
this.problems = [...this.bindings.problems];
|
|
211
|
+
if (atLeast(file.version, 5, 0)) {
|
|
212
|
+
for (const call of nestedHoistedCalls(parsed.program)) {
|
|
213
|
+
const callee = snippet(text(this.file, call.callee));
|
|
214
|
+
this.problems.push(problem('removed-api', call, `Vitest 5 fails a file that calls ${callee}(...) anywhere but its top level, because the call is hoisted there.`, callee));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
161
217
|
this.imports = moduleImports(parsed.program, (source) => source === 'vitest' || MISMATCHED_SOURCES.has(source));
|
|
162
218
|
if (atLeast(file.version, 4, 1)) {
|
|
163
219
|
// Vitest's own pattern, over the whole text as Vitest reads it, so the same tags come out.
|
package/dist/model.d.ts
CHANGED
|
@@ -77,9 +77,13 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
77
77
|
reason: string | null;
|
|
78
78
|
hasDynamicReason: boolean;
|
|
79
79
|
problems: Problem[];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
}
|
|
81
|
+
/** `test.describe.configure()`: a mode, locks (Playwright 1.64), or both. */
|
|
82
|
+
| {
|
|
83
|
+
kind: 'configure';
|
|
84
|
+
mode: Mode | null;
|
|
85
|
+
locks: string[];
|
|
86
|
+
problems: Problem[];
|
|
83
87
|
} | {
|
|
84
88
|
kind: 'hook';
|
|
85
89
|
fn: FunctionNode | null;
|
package/dist/options.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommentsMode } from './records.ts';
|
|
2
2
|
import type { Framework } from './types.ts';
|
|
3
3
|
export type { CommentsMode };
|
|
4
|
+
/** @inline */
|
|
4
5
|
export interface ScanOptions {
|
|
5
6
|
/** Glob patterns for the test files, relative to `root`. */
|
|
6
7
|
patterns: string[];
|
|
@@ -14,6 +15,7 @@ export interface ScanOptions {
|
|
|
14
15
|
/** Which tests get the comments above them. Defaults to `non-active`. */
|
|
15
16
|
comments?: CommentsMode;
|
|
16
17
|
}
|
|
18
|
+
/** @inline */
|
|
17
19
|
export interface ScanSourceOptions {
|
|
18
20
|
code: string;
|
|
19
21
|
/** Path of the file from the project root, used in the output and in ids. */
|
package/dist/records.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Extraction } from './extract.ts';
|
|
2
2
|
import type { ParsedFile } from './parse.ts';
|
|
3
3
|
import type { Diagnostic, Framework, SuiteRecord, TestRecord } from './types.ts';
|
|
4
|
+
/** @inline */
|
|
4
5
|
export type CommentsMode = 'all' | 'non-active' | 'none';
|
|
5
6
|
export interface FileRecords {
|
|
6
7
|
suites: SuiteRecord[];
|
package/dist/records.js
CHANGED
|
@@ -203,7 +203,11 @@ class RecordBuilder {
|
|
|
203
203
|
]),
|
|
204
204
|
annotations: [...describes.flatMap((scope) => scope.details.annotations), ...pending.annotations],
|
|
205
205
|
comments,
|
|
206
|
-
locks: unique([
|
|
206
|
+
locks: unique([
|
|
207
|
+
...fileScope.details.locks,
|
|
208
|
+
...describes.flatMap((scope) => scope.details.locks),
|
|
209
|
+
...declaration.details.locks,
|
|
210
|
+
]),
|
|
207
211
|
meta: Object.keys(meta).length > 0 ? meta : null,
|
|
208
212
|
isParameterized: tables.length > 0,
|
|
209
213
|
caseCount: tables.length > 0 ? repetitions(tables.map((item) => item.caseCount)) : null,
|
package/dist/types.d.ts
CHANGED
|
@@ -10,15 +10,26 @@ type Count = number;
|
|
|
10
10
|
type Position = number;
|
|
11
11
|
/** @pattern ^[0-9a-f]{16}$ */
|
|
12
12
|
type Hash = string;
|
|
13
|
+
/** @inline */
|
|
13
14
|
export type Framework = 'playwright' | 'vitest';
|
|
14
15
|
/**
|
|
15
16
|
* The state the runner applies to a test, after describes and file-level calls are taken into account. `notLoaded`:
|
|
16
17
|
* the runner refuses to load the file, so none of its tests run.
|
|
18
|
+
*
|
|
19
|
+
* @inline
|
|
17
20
|
*/
|
|
18
21
|
export type TestState = 'active' | 'skip' | 'fixme' | 'todo' | 'expectedToFail' | 'notLoaded';
|
|
19
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Where a test's state is set: on the test, on a describe around it, or at file level.
|
|
24
|
+
*
|
|
25
|
+
* @inline
|
|
26
|
+
*/
|
|
20
27
|
export type StateSource = 'test' | 'suite' | 'file';
|
|
21
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* `error`: tests may be missing from the output. `warning`: a value is missing or approximate.
|
|
30
|
+
*
|
|
31
|
+
* @inline
|
|
32
|
+
*/
|
|
22
33
|
export type DiagnosticLevel = 'error' | 'warning';
|
|
23
34
|
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
35
|
export type MetaValue = string | number | boolean | null | MetaValue[] | {
|
|
@@ -38,6 +49,7 @@ export interface Inventory {
|
|
|
38
49
|
tests: TestRecord[];
|
|
39
50
|
diagnostics: Diagnostic[];
|
|
40
51
|
}
|
|
52
|
+
/** @inline */
|
|
41
53
|
export interface Tool {
|
|
42
54
|
name: 'test-inventory';
|
|
43
55
|
/** Version of test-inventory that produced the output. */
|
|
@@ -184,7 +196,7 @@ export interface TestRecord {
|
|
|
184
196
|
annotations: Annotation[];
|
|
185
197
|
/** Comment lines directly above the test. By default only filled for tests that are not active. */
|
|
186
198
|
comments: string[];
|
|
187
|
-
/** From the test and
|
|
199
|
+
/** From the test, its describes and the `test.describe.configure()` calls around it. Playwright only. */
|
|
188
200
|
locks: string[];
|
|
189
201
|
/** Merged from the describes and the test. Vitest only; literal values only. */
|
|
190
202
|
meta: Record<string, MetaValue> | null;
|
|
@@ -202,6 +214,7 @@ export interface TestRecord {
|
|
|
202
214
|
/** Declared inside a plain function. */
|
|
203
215
|
isInFunction: boolean;
|
|
204
216
|
}
|
|
217
|
+
/** @inline */
|
|
205
218
|
export interface Annotation {
|
|
206
219
|
type: string;
|
|
207
220
|
description: string | null;
|
package/package.json
CHANGED
package/schema/v1.json
CHANGED
|
@@ -646,7 +646,7 @@
|
|
|
646
646
|
"items": {
|
|
647
647
|
"type": "string"
|
|
648
648
|
},
|
|
649
|
-
"description": "From the test and
|
|
649
|
+
"description": "From the test, its describes and the `test.describe.configure()` calls around it. Playwright only."
|
|
650
650
|
},
|
|
651
651
|
"meta": {
|
|
652
652
|
"anyOf": [
|