test-inventory 0.2.1 → 0.4.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.
- package/CHANGELOG.md +28 -0
- package/README.md +9 -1
- package/dist/ast.d.ts +11 -0
- package/dist/ast.js +30 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +59 -12
- package/dist/diagnostics.js +2 -0
- package/dist/extract.d.ts +26 -2
- package/dist/extract.js +204 -48
- package/dist/filter.d.ts +38 -0
- package/dist/filter.js +169 -0
- package/dist/frameworks/bindings.d.ts +37 -6
- package/dist/frameworks/bindings.js +167 -33
- package/dist/frameworks/common.d.ts +10 -1
- package/dist/frameworks/common.js +47 -4
- package/dist/frameworks/playwright.d.ts +6 -1
- package/dist/frameworks/playwright.js +111 -32
- package/dist/frameworks/vitest.d.ts +8 -1
- package/dist/frameworks/vitest.js +245 -85
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/inventory.js +1 -0
- package/dist/json.d.ts +7 -1
- package/dist/json.js +32 -9
- package/dist/model.d.ts +62 -5
- package/dist/records.d.ts +7 -2
- package/dist/records.js +74 -4
- package/dist/scan-file.d.ts +2 -0
- package/dist/scan-file.js +12 -6
- package/dist/scan.js +2 -3
- package/dist/summary.js +1 -0
- package/dist/types.d.ts +109 -1
- package/package.json +2 -2
- package/schema/v1.json +326 -2
package/dist/model.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CallExpression, Expression, Node } from 'oxc-parser';
|
|
2
2
|
import type { FunctionNode } from './ast.ts';
|
|
3
|
-
import type { Annotation, DiagnosticCode, DiagnosticLevel, MetaValue } from './types.ts';
|
|
3
|
+
import type { Annotation, DiagnosticCode, DiagnosticLevel, HookCounts, MetaValue } from './types.ts';
|
|
4
4
|
export type State = 'skip' | 'fixme' | 'todo' | 'expectedToFail';
|
|
5
5
|
export type Mode = 'parallel' | 'serial' | 'default';
|
|
6
6
|
/** A state set by a declaration, a describe or a call such as `test.skip(condition)`. */
|
|
@@ -12,6 +12,11 @@ export interface StateSetting {
|
|
|
12
12
|
/** The reason could not be worked out to a string, so `reason` holds its source text. */
|
|
13
13
|
hasDynamicReason: boolean;
|
|
14
14
|
line: number;
|
|
15
|
+
/**
|
|
16
|
+
* Set by code in a test or hook body, which runs only when the test does: a state set while the file loads, on the
|
|
17
|
+
* test or a describe around it, keeps the test from getting there.
|
|
18
|
+
*/
|
|
19
|
+
afterLoad?: boolean;
|
|
15
20
|
}
|
|
16
21
|
export interface Span {
|
|
17
22
|
start: number;
|
|
@@ -40,6 +45,22 @@ export interface Details {
|
|
|
40
45
|
annotations: Annotation[];
|
|
41
46
|
locks: string[];
|
|
42
47
|
meta: Record<string, MetaValue> | null;
|
|
48
|
+
run: RunSettings;
|
|
49
|
+
}
|
|
50
|
+
/** Run settings written in one place; a key that is missing is not set there. */
|
|
51
|
+
export interface RunSettings {
|
|
52
|
+
timeout?: number;
|
|
53
|
+
/** A retry setting replaces the whole `retry` of the describes around, delay and condition included. */
|
|
54
|
+
retry?: {
|
|
55
|
+
count: number;
|
|
56
|
+
delay: number | null;
|
|
57
|
+
condition: string | null;
|
|
58
|
+
};
|
|
59
|
+
repeats?: number;
|
|
60
|
+
/** Playwright `test.slow()`: the conditions of the calls, any of which makes the test slow; null when one always does. */
|
|
61
|
+
slow?: {
|
|
62
|
+
conditions: string[] | null;
|
|
63
|
+
};
|
|
43
64
|
}
|
|
44
65
|
interface Declaration {
|
|
45
66
|
title: Title | null;
|
|
@@ -60,10 +81,16 @@ export interface TestDeclaration extends Declaration {
|
|
|
60
81
|
title: Title;
|
|
61
82
|
/** Framework-specific names through which the test body can change its own state, such as Vitest's `context.skip`. */
|
|
62
83
|
testContext: unknown;
|
|
84
|
+
/** Names the test function takes from its context, without the framework's own keys. */
|
|
85
|
+
fixtures: string[];
|
|
86
|
+
/** Uses Vitest 5's `bench` fixture. */
|
|
87
|
+
usesBench: boolean;
|
|
63
88
|
}
|
|
64
89
|
export interface SuiteDeclaration extends Declaration {
|
|
65
90
|
kind: 'describe';
|
|
66
91
|
isShuffled: boolean;
|
|
92
|
+
/** What the describe passes down to the calls inside it, such as Vitest's `skip` option; null for nothing. */
|
|
93
|
+
passesDown: unknown;
|
|
67
94
|
}
|
|
68
95
|
export interface Condition {
|
|
69
96
|
/** `always` and `never` are conditions that could be worked out from the file. */
|
|
@@ -78,23 +105,46 @@ export type ApiCall = TestDeclaration | SuiteDeclaration | {
|
|
|
78
105
|
hasDynamicReason: boolean;
|
|
79
106
|
problems: Problem[];
|
|
80
107
|
}
|
|
81
|
-
/** `test.describe.configure()`: a mode, locks (Playwright 1.64),
|
|
108
|
+
/** `test.describe.configure()`: a mode, locks (Playwright 1.64), a timeout and retries. */
|
|
82
109
|
| {
|
|
83
110
|
kind: 'configure';
|
|
84
111
|
mode: Mode | null;
|
|
85
112
|
locks: string[];
|
|
113
|
+
run: RunSettings;
|
|
114
|
+
problems: Problem[];
|
|
115
|
+
}
|
|
116
|
+
/** Playwright `test.setTimeout()` or `test.slow()`, which apply where they are called. */
|
|
117
|
+
| {
|
|
118
|
+
kind: 'run';
|
|
119
|
+
settings: RunSettings;
|
|
120
|
+
/** Source text of the first argument. */ argument: string | null;
|
|
86
121
|
problems: Problem[];
|
|
87
122
|
} | {
|
|
88
123
|
kind: 'hook';
|
|
124
|
+
name: keyof HookCounts;
|
|
89
125
|
fn: FunctionNode | null;
|
|
90
126
|
testContext: unknown;
|
|
127
|
+
fixtures: string[];
|
|
128
|
+
}
|
|
129
|
+
/** Playwright `test.step()` or `test.step.skip()`. */
|
|
130
|
+
| {
|
|
131
|
+
kind: 'step';
|
|
132
|
+
title: Title;
|
|
133
|
+
isSkipped: boolean;
|
|
134
|
+
fn: FunctionNode | null;
|
|
91
135
|
} | {
|
|
92
136
|
kind: 'annotations';
|
|
93
137
|
items: Annotation[];
|
|
94
138
|
problems: Problem[];
|
|
95
|
-
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A call the runner refuses. When it is shaped like a declaration, as `describe.sequential('title', fn)` is in Vitest
|
|
142
|
+
* 5, `declaration` is what it would declare, so that its tests are listed as not loaded rather than missing.
|
|
143
|
+
*/
|
|
144
|
+
| {
|
|
96
145
|
kind: 'invalid';
|
|
97
146
|
problem: Problem;
|
|
147
|
+
declaration?: TestDeclaration | SuiteDeclaration;
|
|
98
148
|
} | {
|
|
99
149
|
kind: 'other';
|
|
100
150
|
};
|
|
@@ -105,6 +155,11 @@ export interface CallContext {
|
|
|
105
155
|
inTest: boolean;
|
|
106
156
|
/** Inside a test or hook body, where calls don't declare tests. */
|
|
107
157
|
inBody: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* What the describe the call is registered in passes down, as the framework returned it: the describe around a
|
|
160
|
+
* helper's call, not where the helper is written. Null outside any describe.
|
|
161
|
+
*/
|
|
162
|
+
passedDown: unknown;
|
|
108
163
|
/**
|
|
109
164
|
* What a function declared in the file, or imported from a local module, returns, for a call on its result such as
|
|
110
165
|
* `conditionalTest({ min: 22 })('title', fn)`; null when the name is not a function the scan can find.
|
|
@@ -119,6 +174,8 @@ export interface Returns {
|
|
|
119
174
|
expression: Expression;
|
|
120
175
|
scopes: readonly Node[];
|
|
121
176
|
}[] | null;
|
|
177
|
+
/** What the functions the values call return, looked up in the file that writes the function. */
|
|
178
|
+
returnsOf: CallContext['returnsOf'];
|
|
122
179
|
}
|
|
123
180
|
/** A module read to see whether it exports a custom `test` object. */
|
|
124
181
|
export interface ModuleSource {
|
|
@@ -140,8 +197,8 @@ export interface Adapter {
|
|
|
140
197
|
classify(call: CallExpression, context: CallContext): ApiCall | null;
|
|
141
198
|
/**
|
|
142
199
|
* The test or describe function a value holds, in the adapter's own terms, as `it.skip` in `return it.skip`; null
|
|
143
|
-
* when it holds none. A function the value calls is
|
|
200
|
+
* when it holds none. A function the value calls is followed to what it returns through `returnsOf`.
|
|
144
201
|
*/
|
|
145
|
-
testFunction(expression: Expression, scopes: readonly Node[]): unknown;
|
|
202
|
+
testFunction(expression: Expression, scopes: readonly Node[], returnsOf: CallContext['returnsOf']): unknown;
|
|
146
203
|
}
|
|
147
204
|
export {};
|
package/dist/records.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Extraction } from './extract.ts';
|
|
2
2
|
import type { ParsedFile } from './parse.ts';
|
|
3
|
-
import type { Diagnostic, Framework, SuiteRecord, TestRecord } from './types.ts';
|
|
3
|
+
import type { Diagnostic, FileRecord, Framework, HookCounts, SuiteRecord, TestRecord } from './types.ts';
|
|
4
4
|
/** @inline */
|
|
5
5
|
export type CommentsMode = 'all' | 'non-active' | 'none';
|
|
6
6
|
export interface FileRecords {
|
|
7
|
+
file: FileRecord;
|
|
7
8
|
suites: SuiteRecord[];
|
|
8
9
|
tests: TestRecord[];
|
|
9
10
|
diagnostics: Diagnostic[];
|
|
@@ -13,7 +14,11 @@ interface RecordOptions {
|
|
|
13
14
|
framework: Framework;
|
|
14
15
|
comments: CommentsMode;
|
|
15
16
|
isTypeTest: boolean;
|
|
17
|
+
/** A Vitest 5 benchmark file, such as `*.bench.ts`. */
|
|
18
|
+
isBenchmarkFile: boolean;
|
|
16
19
|
}
|
|
20
|
+
/** The record of a scanned file, from its records and the hooks at its top level. */
|
|
21
|
+
export declare function fileRecord(relativeFilePath: string, hooks: HookCounts, records: Pick<FileRecords, 'suites' | 'tests'>): FileRecord;
|
|
17
22
|
/** An error for each id used by more than one record. Ordinals keep ids apart, so only a hash collision causes one. */
|
|
18
23
|
export declare function duplicateIdDiagnostics(records: readonly {
|
|
19
24
|
id: string;
|
package/dist/records.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
|
|
2
|
+
import { anySlow, eitherCondition, noHooks, } from "./extract.js";
|
|
2
3
|
import { problem } from "./frameworks/common.js";
|
|
3
4
|
import { hashNodes, shortHash } from "./hash.js";
|
|
4
5
|
/**
|
|
5
|
-
* The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo
|
|
6
|
-
*
|
|
6
|
+
* The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo wins
|
|
7
|
+
* over an unconditional expected failure, since the test doesn't run, and one set while the file loads wins over one
|
|
8
|
+
* in a test or hook body, which then never runs; then the nearest conditional state.
|
|
7
9
|
*/
|
|
8
10
|
function chooseState(candidates) {
|
|
9
11
|
const always = candidates.filter((candidate) => candidate.setting.condition === null);
|
|
10
|
-
|
|
12
|
+
const stops = always.filter((candidate) => candidate.setting.state !== 'expectedToFail');
|
|
13
|
+
return (stops.find((candidate) => !candidate.setting.afterLoad) ?? stops.at(0) ?? always.at(0) ?? candidates.at(0) ?? null);
|
|
11
14
|
}
|
|
12
15
|
/** The scope and the scopes around it, innermost first. */
|
|
13
16
|
function outward(scope) {
|
|
@@ -27,6 +30,47 @@ function bodyNodes(fn) {
|
|
|
27
30
|
const body = fn.body;
|
|
28
31
|
return body.type === 'BlockStatement' ? body.body : [body];
|
|
29
32
|
}
|
|
33
|
+
function addHooks(scopes) {
|
|
34
|
+
const total = noHooks();
|
|
35
|
+
for (const scope of scopes) {
|
|
36
|
+
for (const name of Object.keys(total))
|
|
37
|
+
total[name] += scope.hooks[name];
|
|
38
|
+
}
|
|
39
|
+
return total;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The run settings that apply, from the places that set them, nearest first: the first that sets a key wins, and any
|
|
43
|
+
* `test.slow()` makes the test slow.
|
|
44
|
+
*/
|
|
45
|
+
function runKeys(places) {
|
|
46
|
+
const first = (key) => places.find((place) => place[key] !== undefined)?.[key];
|
|
47
|
+
const retry = first('retry');
|
|
48
|
+
const slows = places.flatMap((place) => place.slow ?? []);
|
|
49
|
+
return {
|
|
50
|
+
timeout: first('timeout') ?? null,
|
|
51
|
+
isSlow: slows.length > 0,
|
|
52
|
+
slowCondition: eitherCondition(anySlow(slows).conditions ?? []) || null,
|
|
53
|
+
retries: retry?.count ?? null,
|
|
54
|
+
retryDelay: retry?.delay ?? null,
|
|
55
|
+
retryCondition: retry?.condition ?? null,
|
|
56
|
+
repeats: first('repeats') ?? null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The record of a scanned file, from its records and the hooks at its top level. */
|
|
60
|
+
export function fileRecord(relativeFilePath, hooks, records) {
|
|
61
|
+
return {
|
|
62
|
+
relativeFilePath,
|
|
63
|
+
fileName: relativeFilePath.slice(relativeFilePath.lastIndexOf('/') + 1),
|
|
64
|
+
hooks,
|
|
65
|
+
suiteCount: records.suites.length,
|
|
66
|
+
testCount: records.tests.length,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Whether a function's block holds no statements; comments are not statements. */
|
|
70
|
+
function hasNoStatements(fn) {
|
|
71
|
+
const body = fn?.body;
|
|
72
|
+
return body?.type === 'BlockStatement' && body.body.length === 0;
|
|
73
|
+
}
|
|
30
74
|
/** The number of times the runner repeats a test: its own cases times those of each describe table around it. */
|
|
31
75
|
function repetitions(counts) {
|
|
32
76
|
let product = 1;
|
|
@@ -69,7 +113,12 @@ class RecordBuilder {
|
|
|
69
113
|
this.diagnostics.push(...duplicateIdDiagnostics([...suites, ...tests], this.options.relativeFilePath));
|
|
70
114
|
// Diagnostics for the whole file have no line, which sorts as 0, before the others.
|
|
71
115
|
const diagnostics = this.diagnostics.sort((a, b) => Number(a.lineStart) - Number(b.lineStart) || Number(a.columnStart) - Number(b.columnStart));
|
|
72
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
file: fileRecord(this.options.relativeFilePath, extraction.hooks, { suites, tests }),
|
|
118
|
+
suites,
|
|
119
|
+
tests,
|
|
120
|
+
diagnostics,
|
|
121
|
+
};
|
|
73
122
|
}
|
|
74
123
|
diagnostic(item, testId = null) {
|
|
75
124
|
return toDiagnostic(item, this.options.relativeFilePath, this.parsed.source, testId);
|
|
@@ -134,6 +183,8 @@ class RecordBuilder {
|
|
|
134
183
|
annotations: scope.details.annotations,
|
|
135
184
|
locks: unique(scope.details.locks),
|
|
136
185
|
meta: scope.details.meta,
|
|
186
|
+
hooks: { ...scope.hooks },
|
|
187
|
+
...runKeys([scope.details.run]),
|
|
137
188
|
};
|
|
138
189
|
}
|
|
139
190
|
test(pending) {
|
|
@@ -170,6 +221,10 @@ class RecordBuilder {
|
|
|
170
221
|
const tables = [declaration, ...describes.map((scope) => suiteOf(scope).declaration)].filter((item) => item.isParameterized);
|
|
171
222
|
for (const item of pending.problems)
|
|
172
223
|
this.diagnostics.push(this.diagnostic(item, id));
|
|
224
|
+
if (declaration.usesBench && !this.options.isBenchmarkFile) {
|
|
225
|
+
const message = 'The test uses the bench fixture, which Vitest only provides in benchmark files such as *.bench.ts; elsewhere the test fails.';
|
|
226
|
+
this.diagnostics.push(this.diagnostic(problem('bench-outside-benchmark-file', located, message), id));
|
|
227
|
+
}
|
|
173
228
|
if (pending.isInLoop) {
|
|
174
229
|
this.diagnostics.push(this.diagnostic(problem('test-in-loop', located, 'The test is declared in a loop; it is listed once, while the runner creates one test per iteration.'), id));
|
|
175
230
|
}
|
|
@@ -219,6 +274,21 @@ class RecordBuilder {
|
|
|
219
274
|
...declaration.details.locks,
|
|
220
275
|
]),
|
|
221
276
|
meta: Object.keys(meta).length > 0 ? meta : null,
|
|
277
|
+
hooks: addHooks(scopes),
|
|
278
|
+
// Calls in the body and in `beforeEach` hooks run last, so they win over what the describes set.
|
|
279
|
+
...runKeys([
|
|
280
|
+
pending.run,
|
|
281
|
+
...scopes.map((scope) => scope.hookRun),
|
|
282
|
+
declaration.details.run,
|
|
283
|
+
...scopes.map((scope) => scope.details.run),
|
|
284
|
+
]),
|
|
285
|
+
fixtures: unique([...declaration.fixtures, ...[...scopes].reverse().flatMap((scope) => scope.hookFixtures)]),
|
|
286
|
+
expectCount: pending.expectCount,
|
|
287
|
+
expectedAssertions: pending.expectedAssertions,
|
|
288
|
+
expectsAssertions: pending.expectsAssertions,
|
|
289
|
+
hasEmptyBody: hasNoStatements(declaration.hashed),
|
|
290
|
+
steps: pending.steps,
|
|
291
|
+
isBenchmark: this.options.isBenchmarkFile || declaration.usesBench,
|
|
222
292
|
isParameterized: tables.length > 0,
|
|
223
293
|
caseCount: tables.length > 0 ? repetitions(tables.map((item) => item.caseCount)) : null,
|
|
224
294
|
isInLoop: pending.isInLoop,
|
package/dist/scan-file.d.ts
CHANGED
|
@@ -13,5 +13,7 @@ export interface FileSettings {
|
|
|
13
13
|
export interface FileResult extends FileRecords {
|
|
14
14
|
usesGlobals: boolean;
|
|
15
15
|
}
|
|
16
|
+
/** The records of a file the scan read no tests from. */
|
|
17
|
+
export declare function noRecords(relativeFilePath: string): Omit<FileRecords, 'diagnostics'>;
|
|
16
18
|
/** Reads one file: parse, extract, build records, and drop the AST. Never throws. */
|
|
17
19
|
export declare function scanFile(code: string, relativeFilePath: string, settings: FileSettings): FileResult;
|
package/dist/scan-file.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { fileDiagnostic, toDiagnostic } from "./diagnostics.js";
|
|
2
|
-
import { extract } from "./extract.js";
|
|
2
|
+
import { extract, noHooks } from "./extract.js";
|
|
3
3
|
import { parseModule } from "./frameworks/bindings.js";
|
|
4
|
-
import { problem } from "./frameworks/common.js";
|
|
4
|
+
import { atLeast, problem } from "./frameworks/common.js";
|
|
5
5
|
import { PlaywrightAdapter } from "./frameworks/playwright.js";
|
|
6
6
|
import { VitestAdapter } from "./frameworks/vitest.js";
|
|
7
7
|
import { parseFile } from "./parse.js";
|
|
8
|
-
import { buildRecords } from "./records.js";
|
|
8
|
+
import { buildRecords, fileRecord } from "./records.js";
|
|
9
9
|
import { snippet } from "./source.js";
|
|
10
10
|
import { Values } from "./values.js";
|
|
11
11
|
const TYPE_TEST = /\.(test|spec)-d\.[cm]?[jt]sx?$/;
|
|
12
|
+
const BENCHMARK = /\.(bench|benchmark)\.[cm]?[jt]sx?$/;
|
|
12
13
|
/** A file read for the scan: the scanned file, or a module a helper function is imported from. */
|
|
13
14
|
function frame(parsed, relativeFilePath, settings, readModule) {
|
|
14
15
|
const file = { source: parsed.source, values: new Values(), version: settings.version };
|
|
@@ -17,6 +18,10 @@ function frame(parsed, relativeFilePath, settings, readModule) {
|
|
|
17
18
|
: new VitestAdapter(parsed, file, readModule);
|
|
18
19
|
return { parsed, adapter, values: file.values, readModule, relativeFilePath };
|
|
19
20
|
}
|
|
21
|
+
/** The records of a file the scan read no tests from. */
|
|
22
|
+
export function noRecords(relativeFilePath) {
|
|
23
|
+
return { file: fileRecord(relativeFilePath, noHooks(), { suites: [], tests: [] }), suites: [], tests: [] };
|
|
24
|
+
}
|
|
20
25
|
/** Reads one file: parse, extract, build records, and drop the AST. Never throws. */
|
|
21
26
|
export function scanFile(code, relativeFilePath, settings) {
|
|
22
27
|
try {
|
|
@@ -27,7 +32,7 @@ export function scanFile(code, relativeFilePath, settings) {
|
|
|
27
32
|
diagnostics.push(toDiagnostic(problem('parse-error', { start, end }, `The file could not be parsed: ${message}`, snippet(parsed.source.slice(start, end)) || null), relativeFilePath, parsed.source));
|
|
28
33
|
// The parser keeps what it could read after some errors; after others it returns nothing.
|
|
29
34
|
if (parsed.program.body.length === 0)
|
|
30
|
-
return {
|
|
35
|
+
return { ...noRecords(relativeFilePath), diagnostics, usesGlobals: false };
|
|
31
36
|
}
|
|
32
37
|
const main = frame(parsed, relativeFilePath, settings, settings.readModule);
|
|
33
38
|
const extraction = extract(main, (module) => {
|
|
@@ -39,6 +44,8 @@ export function scanFile(code, relativeFilePath, settings) {
|
|
|
39
44
|
framework: settings.framework,
|
|
40
45
|
comments: settings.comments,
|
|
41
46
|
isTypeTest: settings.framework === 'vitest' && TYPE_TEST.test(relativeFilePath),
|
|
47
|
+
// Vitest 5's default `benchmark.include`.
|
|
48
|
+
isBenchmarkFile: settings.framework === 'vitest' && atLeast(settings.version, 5, 0) && BENCHMARK.test(relativeFilePath),
|
|
42
49
|
});
|
|
43
50
|
return {
|
|
44
51
|
...records,
|
|
@@ -50,8 +57,7 @@ export function scanFile(code, relativeFilePath, settings) {
|
|
|
50
57
|
const raw = error instanceof Error ? error.message : String(error);
|
|
51
58
|
const message = settings.root === null ? raw : raw.replaceAll(settings.root, '.');
|
|
52
59
|
return {
|
|
53
|
-
|
|
54
|
-
tests: [],
|
|
60
|
+
...noRecords(relativeFilePath),
|
|
55
61
|
diagnostics: [
|
|
56
62
|
fileDiagnostic('internal-error', relativeFilePath, `Reading the file failed unexpectedly: ${message}`),
|
|
57
63
|
],
|
package/dist/scan.js
CHANGED
|
@@ -7,7 +7,7 @@ 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";
|
|
9
9
|
import { moduleResolver } from "./resolve.js";
|
|
10
|
-
import { scanFile } from "./scan-file.js";
|
|
10
|
+
import { noRecords, scanFile } from "./scan-file.js";
|
|
11
11
|
import { globalsCheck } from "./vitest-config.js";
|
|
12
12
|
function versionDiagnostics(framework, version, source, range) {
|
|
13
13
|
const name = framework === 'playwright' ? 'Playwright' : 'Vitest';
|
|
@@ -86,8 +86,7 @@ export async function scan(options) {
|
|
|
86
86
|
}
|
|
87
87
|
catch (error) {
|
|
88
88
|
results.push({
|
|
89
|
-
|
|
90
|
-
tests: [],
|
|
89
|
+
...noRecords(relativeFilePath),
|
|
91
90
|
diagnostics: [
|
|
92
91
|
fileDiagnostic('read-error', relativeFilePath, `The file could not be read: ${readFailure(error)}.`),
|
|
93
92
|
],
|
package/dist/summary.js
CHANGED
|
@@ -27,6 +27,7 @@ export function summarize(fileCount, suites, tests, diagnostics) {
|
|
|
27
27
|
inConditionCount: count('isInCondition'),
|
|
28
28
|
inFunctionCount: count('isInFunction'),
|
|
29
29
|
typeTestCount: count('isTypeTest'),
|
|
30
|
+
benchmarkCount: count('isBenchmark'),
|
|
30
31
|
diagnosticCount: diagnostics.length,
|
|
31
32
|
tagCounts: countBy(tests, (test) => test.tags),
|
|
32
33
|
annotationCounts: countBy(tests, (test) => test.annotations.map((annotation) => annotation.type)),
|
package/dist/types.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type StateSource = 'test' | 'suite' | 'file';
|
|
|
31
31
|
* @inline
|
|
32
32
|
*/
|
|
33
33
|
export type DiagnosticLevel = 'error' | 'warning';
|
|
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';
|
|
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' | 'empty-title' | 'bench-outside-benchmark-file';
|
|
35
35
|
export type MetaValue = string | number | boolean | null | MetaValue[] | {
|
|
36
36
|
[key: string]: MetaValue;
|
|
37
37
|
};
|
|
@@ -45,10 +45,30 @@ export interface Inventory {
|
|
|
45
45
|
/** Read from the project, or given with `--framework-version`; null when unknown. */
|
|
46
46
|
frameworkVersion: string | null;
|
|
47
47
|
summary: Summary;
|
|
48
|
+
/** One per scanned file, in the order the files were scanned. */
|
|
49
|
+
files: FileRecord[];
|
|
48
50
|
suites: SuiteRecord[];
|
|
49
51
|
tests: TestRecord[];
|
|
50
52
|
diagnostics: Diagnostic[];
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* One line of the JSON Lines output (`--format jsonl`): the inventory with each record on its own line, marked by
|
|
56
|
+
* `type`. The `inventory` line comes first, then the files, suites, tests and diagnostics in the order of the JSON
|
|
57
|
+
* output, and the `summary` line last.
|
|
58
|
+
*/
|
|
59
|
+
export type InventoryLine = ({
|
|
60
|
+
type: 'inventory';
|
|
61
|
+
} & Pick<Inventory, 'schemaVersion' | 'tool' | 'framework' | 'frameworkVersion'>) | ({
|
|
62
|
+
type: 'file';
|
|
63
|
+
} & FileRecord) | ({
|
|
64
|
+
type: 'suite';
|
|
65
|
+
} & SuiteRecord) | ({
|
|
66
|
+
type: 'test';
|
|
67
|
+
} & TestRecord) | ({
|
|
68
|
+
type: 'diagnostic';
|
|
69
|
+
} & Diagnostic) | ({
|
|
70
|
+
type: 'summary';
|
|
71
|
+
} & Summary);
|
|
52
72
|
/** @inline */
|
|
53
73
|
export interface Tool {
|
|
54
74
|
name: 'test-inventory';
|
|
@@ -76,12 +96,38 @@ export interface Summary {
|
|
|
76
96
|
inConditionCount: Count;
|
|
77
97
|
inFunctionCount: Count;
|
|
78
98
|
typeTestCount: Count;
|
|
99
|
+
benchmarkCount: Count;
|
|
79
100
|
diagnosticCount: Count;
|
|
80
101
|
/** Tests per tag. */
|
|
81
102
|
tagCounts: Record<string, Count>;
|
|
82
103
|
/** Tests per annotation type. */
|
|
83
104
|
annotationCounts: Record<string, Count>;
|
|
84
105
|
}
|
|
106
|
+
/** A scanned file, also one that has no tests or could not be read. */
|
|
107
|
+
export interface FileRecord {
|
|
108
|
+
/** Path from the project root, with `/` separators. */
|
|
109
|
+
relativeFilePath: string;
|
|
110
|
+
fileName: string;
|
|
111
|
+
/** Hooks at the top level of the file, which run around every test in it. */
|
|
112
|
+
hooks: HookCounts;
|
|
113
|
+
suiteCount: Count;
|
|
114
|
+
testCount: Count;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* How many hooks of each kind are registered, including by helper functions called there.
|
|
118
|
+
*
|
|
119
|
+
* @inline
|
|
120
|
+
*/
|
|
121
|
+
export interface HookCounts {
|
|
122
|
+
beforeAll: Count;
|
|
123
|
+
beforeEach: Count;
|
|
124
|
+
afterEach: Count;
|
|
125
|
+
afterAll: Count;
|
|
126
|
+
/** Vitest only. */
|
|
127
|
+
aroundEach: Count;
|
|
128
|
+
/** Vitest only. */
|
|
129
|
+
aroundAll: Count;
|
|
130
|
+
}
|
|
85
131
|
/** A describe block, with what is written on the describe itself. Inherited values are on the tests. */
|
|
86
132
|
export interface SuiteRecord {
|
|
87
133
|
id: Hash;
|
|
@@ -142,6 +188,21 @@ export interface SuiteRecord {
|
|
|
142
188
|
locks: string[];
|
|
143
189
|
/** Vitest only; literal values only. */
|
|
144
190
|
meta: Record<string, MetaValue> | null;
|
|
191
|
+
/** Hooks registered directly in the describe. */
|
|
192
|
+
hooks: HookCounts;
|
|
193
|
+
/** In milliseconds; null when nothing here sets it. */
|
|
194
|
+
timeout: Count | null;
|
|
195
|
+
/** Playwright `test.slow()` written in the describe, which triples the timeout of its tests. */
|
|
196
|
+
isSlow: boolean;
|
|
197
|
+
/** Source text of the `test.slow()` condition; null when unconditional or not slow. */
|
|
198
|
+
slowCondition: string | null;
|
|
199
|
+
retries: Count | null;
|
|
200
|
+
/** Vitest only: milliseconds between retries, from the `retry` object (4.1). */
|
|
201
|
+
retryDelay: Count | null;
|
|
202
|
+
/** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
|
|
203
|
+
retryCondition: string | null;
|
|
204
|
+
/** Vitest only: how many times the runner runs each test again after the first run. */
|
|
205
|
+
repeats: Count | null;
|
|
145
206
|
}
|
|
146
207
|
/** A test, with the effective values the runner applies, including those inherited from its describes. */
|
|
147
208
|
export interface TestRecord {
|
|
@@ -210,6 +271,42 @@ export interface TestRecord {
|
|
|
210
271
|
locks: string[];
|
|
211
272
|
/** Merged from the describes and the test. Vitest only; literal values only. */
|
|
212
273
|
meta: Record<string, MetaValue> | null;
|
|
274
|
+
/** Every hook that runs around the test: those of the file and of each describe around it. */
|
|
275
|
+
hooks: HookCounts;
|
|
276
|
+
/**
|
|
277
|
+
* In milliseconds: the last `test.setTimeout()` in the body or a `beforeEach`, otherwise the nearest describe or file
|
|
278
|
+
* that sets one. Null when the file sets none, so the config's timeout applies.
|
|
279
|
+
*/
|
|
280
|
+
timeout: Count | null;
|
|
281
|
+
/** Playwright `test.slow()` applies: the runner triples the timeout. */
|
|
282
|
+
isSlow: boolean;
|
|
283
|
+
/** Source text of the condition of the `test.slow()` that applies; null when unconditional or not slow. */
|
|
284
|
+
slowCondition: string | null;
|
|
285
|
+
/** How many times the runner retries the test when it fails; null when the file sets none. */
|
|
286
|
+
retries: Count | null;
|
|
287
|
+
/** Vitest only: milliseconds between retries, from the `retry` object (4.1). */
|
|
288
|
+
retryDelay: Count | null;
|
|
289
|
+
/** Vitest only: source text of the `retry` object's `condition`, a RegExp or a function. */
|
|
290
|
+
retryCondition: string | null;
|
|
291
|
+
/** Vitest only: how many times the runner runs the test again after the first run. */
|
|
292
|
+
repeats: Count | null;
|
|
293
|
+
/**
|
|
294
|
+
* Names the test function takes from its context, then those of the `beforeEach`, `afterEach` and `aroundEach` hooks
|
|
295
|
+
* around it, once each. Vitest's own context keys, such as `expect` and `task`, are left out.
|
|
296
|
+
*/
|
|
297
|
+
fixtures: string[];
|
|
298
|
+
/** Calls of `expect()`, `expect.soft()` and `expect.poll()` written in the body, each counted once. */
|
|
299
|
+
expectCount: Count;
|
|
300
|
+
/** Vitest `expect.assertions(n)` in the body. */
|
|
301
|
+
expectedAssertions: Count | null;
|
|
302
|
+
/** Vitest `expect.hasAssertions()` in the body. */
|
|
303
|
+
expectsAssertions: boolean;
|
|
304
|
+
/** The test function has no statements, comments aside. */
|
|
305
|
+
hasEmptyBody: boolean;
|
|
306
|
+
/** Playwright `test.step()` and `test.step.skip()` calls in the body, in source order. */
|
|
307
|
+
steps: Step[];
|
|
308
|
+
/** Vitest 5 benchmark: in a benchmark file, or using the `bench` fixture. Plain `vitest` doesn't run it. */
|
|
309
|
+
isBenchmark: boolean;
|
|
213
310
|
/** Declared with `.each` or `.for`, or inside a describe declared with them, so the runner repeats it. */
|
|
214
311
|
isParameterized: boolean;
|
|
215
312
|
/**
|
|
@@ -225,6 +322,17 @@ export interface TestRecord {
|
|
|
225
322
|
isInFunction: boolean;
|
|
226
323
|
}
|
|
227
324
|
/** @inline */
|
|
325
|
+
export interface Step {
|
|
326
|
+
/** The title, or its source text when it can't be worked out. */
|
|
327
|
+
title: string;
|
|
328
|
+
hasDynamicTitle: boolean;
|
|
329
|
+
lineStart: Position;
|
|
330
|
+
/** 0 for a step directly in the test, 1 for a step inside it, and so on. */
|
|
331
|
+
depth: Count;
|
|
332
|
+
/** Declared with `test.step.skip()`. */
|
|
333
|
+
isSkipped: boolean;
|
|
334
|
+
}
|
|
335
|
+
/** @inline */
|
|
228
336
|
export interface DeclaredAt {
|
|
229
337
|
/** The helper's file, from the project root. */
|
|
230
338
|
relativeFilePath: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "test-inventory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Test metadata from source code, not from test results.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"lint": "eslint .",
|
|
72
72
|
"lint:fix": "eslint . --fix",
|
|
73
73
|
"format": "prettier --write . --ignore-path .gitignore --ignore-path .prettierignore --ignore-path .git/info/exclude",
|
|
74
|
-
"format:check": "
|
|
74
|
+
"format:check": "node scripts/format-check.ts",
|
|
75
75
|
"docs:api": "typedoc",
|
|
76
76
|
"docs:dev": "npm run docs:api && vitepress dev docs",
|
|
77
77
|
"docs:build": "npm run docs:api && vitepress build docs",
|