test-inventory 0.4.1 → 0.6.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 +96 -0
- package/README.md +4 -1
- package/dist/ast.d.ts +3 -3
- package/dist/ast.js +4 -4
- package/dist/bin.js +1 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +27 -7
- package/dist/config-dependencies.d.ts +62 -0
- package/dist/config-dependencies.js +231 -0
- package/dist/config-runs.d.ts +113 -0
- package/dist/config-runs.js +807 -0
- package/dist/config.d.ts +90 -0
- package/dist/config.js +652 -0
- package/dist/dependency-hash.d.ts +242 -0
- package/dist/dependency-hash.js +1458 -0
- package/dist/diagnostics.js +6 -1
- package/dist/extract.d.ts +20 -0
- package/dist/extract.js +20 -4
- package/dist/filter.d.ts +6 -0
- package/dist/filter.js +18 -1
- package/dist/frameworks/bindings.d.ts +22 -0
- package/dist/frameworks/bindings.js +19 -9
- package/dist/frameworks/common.d.ts +5 -0
- package/dist/frameworks/common.js +11 -0
- package/dist/frameworks/playwright.d.ts +2 -1
- package/dist/frameworks/playwright.js +11 -2
- package/dist/frameworks/vitest.d.ts +2 -1
- package/dist/frameworks/vitest.js +9 -3
- package/dist/hash.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/inventory.d.ts +2 -2
- package/dist/inventory.js +2 -1
- package/dist/json.js +2 -0
- package/dist/model.d.ts +9 -1
- package/dist/options.d.ts +2 -2
- package/dist/records.d.ts +58 -2
- package/dist/records.js +196 -28
- package/dist/scan-file.d.ts +13 -1
- package/dist/scan-file.js +16 -4
- package/dist/scan-source.js +1 -1
- package/dist/scan.d.ts +2 -1
- package/dist/scan.js +131 -18
- package/dist/types.d.ts +83 -7
- package/dist/values.d.ts +5 -0
- package/dist/values.js +5 -0
- package/dist/vitest-config.d.ts +6 -4
- package/dist/vitest-config.js +32 -28
- package/dist/vitest-defaults.d.ts +6 -0
- package/dist/vitest-defaults.js +16 -0
- package/package.json +10 -8
- package/schema/v1.json +200 -6
package/dist/records.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { type Extraction } from './extract.ts';
|
|
1
|
+
import { type Extraction, type PendingTest } from './extract.ts';
|
|
2
|
+
import type { Dependencies } from './dependency-hash.ts';
|
|
3
|
+
import type { RunSettings } from './model.ts';
|
|
2
4
|
import type { ParsedFile } from './parse.ts';
|
|
3
|
-
import type { Diagnostic, FileRecord, Framework, HookCounts, SuiteRecord, TestRecord } from './types.ts';
|
|
5
|
+
import type { Diagnostic, FileRecord, Framework, HookCounts, MetaValue, SuiteRecord, TestRecord } from './types.ts';
|
|
4
6
|
/** @inline */
|
|
5
7
|
export type CommentsMode = 'all' | 'non-active' | 'none';
|
|
6
8
|
export interface FileRecords {
|
|
@@ -9,6 +11,57 @@ export interface FileRecords {
|
|
|
9
11
|
tests: TestRecord[];
|
|
10
12
|
diagnostics: Diagnostic[];
|
|
11
13
|
}
|
|
14
|
+
/** What the config the file uses sets for its tests, where the file sets nothing. */
|
|
15
|
+
export interface ConfigDefaults {
|
|
16
|
+
/** A run setting the config sets; null when its projects set different values or it depends on a condition. */
|
|
17
|
+
timeout?: number | null;
|
|
18
|
+
retry?: RunSettings['retry'] | null;
|
|
19
|
+
repeats?: number | null;
|
|
20
|
+
/** The source text of a run setting the config sets under a condition; null for any other. */
|
|
21
|
+
timeoutText: string | null;
|
|
22
|
+
retryText: string | null;
|
|
23
|
+
repeatsText: string | null;
|
|
24
|
+
/** Whether tests run in parallel unless the file says otherwise: Playwright's `fullyParallel`, Vitest's `sequence.concurrent`. */
|
|
25
|
+
parallel: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** What a Vitest tag the config defines sets on the tests that carry it. */
|
|
28
|
+
export interface TagOptions {
|
|
29
|
+
skip?: boolean;
|
|
30
|
+
only?: boolean;
|
|
31
|
+
todo?: boolean;
|
|
32
|
+
fails?: boolean;
|
|
33
|
+
concurrent?: boolean;
|
|
34
|
+
run: RunSettings;
|
|
35
|
+
meta?: Record<string, MetaValue>;
|
|
36
|
+
}
|
|
37
|
+
export interface TagDefinition {
|
|
38
|
+
name: string;
|
|
39
|
+
/** Lower numbers win when two tags set the same option; a tag without one loses to every tag with one. */
|
|
40
|
+
priority: number | null;
|
|
41
|
+
options: TagOptions;
|
|
42
|
+
}
|
|
43
|
+
/** The tags the Vitest config defines (Vitest 4.1). */
|
|
44
|
+
export interface VitestTags {
|
|
45
|
+
definitions: TagDefinition[];
|
|
46
|
+
/** `strictTags`: a tag the config doesn't define fails the file while it loads. */
|
|
47
|
+
isStrict: boolean;
|
|
48
|
+
/** From Vitest 5 a tag's options win over the describe's around the test; before, the describe's win. */
|
|
49
|
+
beatsDescribes: boolean;
|
|
50
|
+
/** Vitest 4.1 passes the config's `sequence.concurrent`, when it sets one, to every test, over a tag's `concurrent`. */
|
|
51
|
+
concurrentBeatsTags: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** What the config the file uses gives it. */
|
|
54
|
+
export interface FileConfig {
|
|
55
|
+
/** Whether a `.only` fails the run: Playwright's `forbidOnly`, always, never, or under the condition with this text. */
|
|
56
|
+
forbidOnly: boolean | string;
|
|
57
|
+
/** The names of the projects that run a test, null when unknown, and what they set for it. */
|
|
58
|
+
forTest(test: TestRecord, suites: SuiteRecord[]): {
|
|
59
|
+
projects: string[] | null;
|
|
60
|
+
defaults: ConfigDefaults | null;
|
|
61
|
+
};
|
|
62
|
+
/** Vitest's tags; null when the scan can't work them out, or the projects that run the file define different ones. */
|
|
63
|
+
tags: VitestTags | null;
|
|
64
|
+
}
|
|
12
65
|
interface RecordOptions {
|
|
13
66
|
relativeFilePath: string;
|
|
14
67
|
framework: Framework;
|
|
@@ -16,6 +69,9 @@ interface RecordOptions {
|
|
|
16
69
|
isTypeTest: boolean;
|
|
17
70
|
/** A Vitest 5 benchmark file, such as `*.bench.ts`. */
|
|
18
71
|
isBenchmarkFile: boolean;
|
|
72
|
+
config: FileConfig | null;
|
|
73
|
+
/** What a test runs, for its `dependencyHash`. */
|
|
74
|
+
dependencies: (test: PendingTest) => Dependencies;
|
|
19
75
|
}
|
|
20
76
|
/** The record of a scanned file, from its records and the hooks at its top level. */
|
|
21
77
|
export declare function fileRecord(relativeFilePath: string, hooks: HookCounts, records: Pick<FileRecords, 'suites' | 'tests'>): FileRecord;
|
package/dist/records.js
CHANGED
|
@@ -2,6 +2,7 @@ import { REJECTS_FILE, toDiagnostic } from "./diagnostics.js";
|
|
|
2
2
|
import { anySlow, eitherCondition, noHooks, } from "./extract.js";
|
|
3
3
|
import { problem } from "./frameworks/common.js";
|
|
4
4
|
import { hashNodes, shortHash } from "./hash.js";
|
|
5
|
+
import { snippet } from "./source.js";
|
|
5
6
|
/**
|
|
6
7
|
* The state the runner applies, from the candidates nearest the test first. An unconditional skip, fixme or todo wins
|
|
7
8
|
* over an unconditional expected failure, since the test doesn't run, and one set while the file loads wins over one
|
|
@@ -12,6 +13,65 @@ function chooseState(candidates) {
|
|
|
12
13
|
const stops = always.filter((candidate) => candidate.setting.state !== 'expectedToFail');
|
|
13
14
|
return (stops.find((candidate) => !candidate.setting.afterLoad) ?? stops.at(0) ?? always.at(0) ?? candidates.at(0) ?? null);
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The first use of each tag the Vitest config doesn't define, by `strictTags` an error that fails the file while it
|
|
18
|
+
* loads, in the order Vitest meets them: the `@module-tag` comments, the describes' `tags` as the describes are called,
|
|
19
|
+
* then the tests'. A use in a function or under a condition only fails the file if that code runs.
|
|
20
|
+
*/
|
|
21
|
+
function undefinedTags(extraction, tags) {
|
|
22
|
+
const defined = new Set(tags.definitions.map(({ name }) => name));
|
|
23
|
+
const declared = (items) => items
|
|
24
|
+
.flatMap(({ declaration, site, call, isInCondition, isInFunction }) => declaration.details.tags.map((name) => ({ name, span: site ?? call, isSure: !isInCondition && !isInFunction })))
|
|
25
|
+
.sort((a, b) => a.span.start - b.span.start);
|
|
26
|
+
const uses = [
|
|
27
|
+
...extraction.fileTags.map((tag) => ({ ...tag, isSure: true })),
|
|
28
|
+
...declared(extraction.suites),
|
|
29
|
+
...declared(extraction.tests),
|
|
30
|
+
].filter(({ name }) => !defined.has(name));
|
|
31
|
+
// Each tag is reported once: at its first use that surely runs, else at its first use.
|
|
32
|
+
const first = new Map();
|
|
33
|
+
for (const use of uses) {
|
|
34
|
+
const known = first.get(use.name);
|
|
35
|
+
if (!known || (!known.isSure && use.isSure))
|
|
36
|
+
first.set(use.name, use);
|
|
37
|
+
}
|
|
38
|
+
const reported = [...first.values()].sort((a, b) => uses.indexOf(a) - uses.indexOf(b));
|
|
39
|
+
return reported.map(({ name, span, isSure }) => {
|
|
40
|
+
const consequence = isSure
|
|
41
|
+
? 'Vitest fails the file while it loads, and none of its tests run'
|
|
42
|
+
: 'Vitest fails the file if this code runs while it loads';
|
|
43
|
+
const message = `The Vitest config doesn't define the tag "${name}", so with strictTags ${consequence}.`;
|
|
44
|
+
return { problem: problem('undefined-tag', span, message), isSure };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const FLAG_ORDER = ['skip', 'todo', 'expectedToFail'];
|
|
48
|
+
const ALWAYS = { condition: null, reason: null, hasDynamicReason: false };
|
|
49
|
+
/**
|
|
50
|
+
* The states of a test's declaration with those of its Vitest tags. Vitest merges the options and then takes `only`
|
|
51
|
+
* over `skip` over `todo`, so a `.only` on either side drops the skip and todo of both.
|
|
52
|
+
*/
|
|
53
|
+
function declaredStates(declared, options, { isOnly, skipPassedDown }) {
|
|
54
|
+
const own = declared.map((setting) => ({ setting, source: 'test' }));
|
|
55
|
+
if (options === null)
|
|
56
|
+
return own;
|
|
57
|
+
// A describe's `skip` option is merged in too, where the scan otherwise leaves it to the describe's own state; the
|
|
58
|
+
// test's own skip, when it has one, sorts first.
|
|
59
|
+
const passed = skipPassedDown === undefined
|
|
60
|
+
? []
|
|
61
|
+
: [{ setting: { ...ALWAYS, state: 'skip', line: skipPassedDown }, source: 'suite' }];
|
|
62
|
+
const all = [...own, ...passed, ...tagStates(options).map((setting) => ({ setting, source: 'config' }))];
|
|
63
|
+
const kept = isOnly || options.only ? all.filter(({ setting }) => setting.state === 'expectedToFail') : all;
|
|
64
|
+
return kept.sort((a, b) => FLAG_ORDER.indexOf(a.setting.state) - FLAG_ORDER.indexOf(b.setting.state));
|
|
65
|
+
}
|
|
66
|
+
/** The states the options of a test's Vitest tags set. */
|
|
67
|
+
function tagStates(options) {
|
|
68
|
+
const states = [
|
|
69
|
+
[options.skip, 'skip'],
|
|
70
|
+
[options.todo, 'todo'],
|
|
71
|
+
[options.fails, 'expectedToFail'],
|
|
72
|
+
];
|
|
73
|
+
return states.filter(([isSet]) => isSet === true).map(([, state]) => ({ ...ALWAYS, state, line: null }));
|
|
74
|
+
}
|
|
15
75
|
/** The scope and the scopes around it, innermost first. */
|
|
16
76
|
function outward(scope) {
|
|
17
77
|
const scopes = [];
|
|
@@ -30,6 +90,10 @@ function bodyNodes(fn) {
|
|
|
30
90
|
const body = fn.body;
|
|
31
91
|
return body.type === 'BlockStatement' ? body.body : [body];
|
|
32
92
|
}
|
|
93
|
+
/** Where a run setting of a scope is written, for a test in it: the file, or a describe. */
|
|
94
|
+
function placeOf(scope) {
|
|
95
|
+
return scope.kind === 'file' ? 'file' : 'suite';
|
|
96
|
+
}
|
|
33
97
|
function addHooks(scopes) {
|
|
34
98
|
const total = noHooks();
|
|
35
99
|
for (const scope of scopes) {
|
|
@@ -56,11 +120,47 @@ function runKeys(places) {
|
|
|
56
120
|
repeats: first('repeats') ?? null,
|
|
57
121
|
};
|
|
58
122
|
}
|
|
123
|
+
/** A test's run settings from the file, and where each comes from: the nearest place that sets it. */
|
|
124
|
+
function testRun(places) {
|
|
125
|
+
const from = (key) => places.find((place) => place.run[key] !== undefined)?.source ?? null;
|
|
126
|
+
return {
|
|
127
|
+
...runKeys(places.map((place) => place.run)),
|
|
128
|
+
timeoutSource: from('timeout'),
|
|
129
|
+
retriesSource: from('retry'),
|
|
130
|
+
repeatsSource: from('repeats'),
|
|
131
|
+
timeoutConfigText: null,
|
|
132
|
+
retriesConfigText: null,
|
|
133
|
+
repeatsConfigText: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** A test's run settings, with what the config sets where the file sets nothing. */
|
|
137
|
+
function withConfig(fromFile, config) {
|
|
138
|
+
const run = { ...fromFile };
|
|
139
|
+
if (run.timeoutSource === null && config?.timeout !== undefined) {
|
|
140
|
+
run.timeout = config.timeout;
|
|
141
|
+
run.timeoutSource = 'config';
|
|
142
|
+
run.timeoutConfigText = config.timeoutText;
|
|
143
|
+
}
|
|
144
|
+
if (run.retriesSource === null && config?.retry !== undefined) {
|
|
145
|
+
run.retries = config.retry?.count ?? null;
|
|
146
|
+
run.retryDelay = config.retry?.delay ?? null;
|
|
147
|
+
run.retryCondition = config.retry?.condition ?? null;
|
|
148
|
+
run.retriesSource = 'config';
|
|
149
|
+
run.retriesConfigText = config.retryText;
|
|
150
|
+
}
|
|
151
|
+
if (run.repeatsSource === null && config?.repeats !== undefined) {
|
|
152
|
+
run.repeats = config.repeats;
|
|
153
|
+
run.repeatsSource = 'config';
|
|
154
|
+
run.repeatsConfigText = config.repeatsText;
|
|
155
|
+
}
|
|
156
|
+
return run;
|
|
157
|
+
}
|
|
59
158
|
/** The record of a scanned file, from its records and the hooks at its top level. */
|
|
60
159
|
export function fileRecord(relativeFilePath, hooks, records) {
|
|
61
160
|
return {
|
|
62
161
|
relativeFilePath,
|
|
63
162
|
fileName: relativeFilePath.slice(relativeFilePath.lastIndexOf('/') + 1),
|
|
163
|
+
config: null,
|
|
64
164
|
hooks,
|
|
65
165
|
suiteCount: records.suites.length,
|
|
66
166
|
testCount: records.tests.length,
|
|
@@ -94,19 +194,28 @@ class RecordBuilder {
|
|
|
94
194
|
}
|
|
95
195
|
build(extraction) {
|
|
96
196
|
const duplicates = this.duplicates(extraction.tests);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
197
|
+
const tags = this.options.framework === 'vitest' ? (this.options.config?.tags ?? null) : null;
|
|
198
|
+
const testProblems = extraction.tests.flatMap((test) => test.problems);
|
|
199
|
+
// With the config's tags known, what they do is in the records, unless a tag can't be worked out.
|
|
200
|
+
const isTagMissing = [...extraction.problems, ...testProblems].some((item) => item.code === 'unresolved-tag');
|
|
201
|
+
const undefinedUses = tags?.isStrict ? undefinedTags(extraction, tags) : [];
|
|
202
|
+
const problems = [
|
|
203
|
+
...extraction.problems.filter((item) => tags === null || isTagMissing || item.code !== 'config-may-change-state'),
|
|
204
|
+
...undefinedUses.map((use) => use.problem),
|
|
205
|
+
];
|
|
206
|
+
const rejecting = [...problems, ...testProblems, ...duplicates.map((item) => item.problem)].filter((item) => REJECTS_FILE[this.options.framework].has(item.code) && item.code !== 'undefined-tag' && item.afterLoad !== true);
|
|
207
|
+
// Each of these problems points at the code that causes it. Vitest meets the tags in the order they are listed.
|
|
208
|
+
const firstTag = undefinedUses.find((use) => use.isSure)?.problem;
|
|
209
|
+
const lines = [...rejecting, ...(firstTag ? [firstTag] : [])].map((item) => this.parsed.source.line(item.node.start));
|
|
104
210
|
if (lines.length > 0)
|
|
105
211
|
this.rejectedAt = Math.min(...lines);
|
|
106
212
|
const suites = extraction.suites.map((suite) => this.suite(suite));
|
|
107
|
-
const tests = extraction.tests.map((test) => this.test(test));
|
|
108
|
-
for (const item of
|
|
109
|
-
this.
|
|
213
|
+
const tests = extraction.tests.map((test) => this.test(test, suites));
|
|
214
|
+
for (const item of problems) {
|
|
215
|
+
const found = this.diagnostic(item);
|
|
216
|
+
const isSure = undefinedUses.find((use) => use.problem === item)?.isSure ?? true;
|
|
217
|
+
this.diagnostics.push(isSure ? found : { ...found, level: 'warning' });
|
|
218
|
+
}
|
|
110
219
|
for (const { index, problem: item } of duplicates) {
|
|
111
220
|
this.diagnostics.push(this.diagnostic(item, tests[index].id));
|
|
112
221
|
}
|
|
@@ -144,8 +253,25 @@ class RecordBuilder {
|
|
|
144
253
|
this.ordinals.set(key, ordinal + 1);
|
|
145
254
|
return shortHash(JSON.stringify([this.options.relativeFilePath, ...parts, ordinal]));
|
|
146
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Reports a `.only` that the config's `forbidOnly` makes fail the run, at `located`; one under a condition is a
|
|
258
|
+
* warning. `call` is the `.only` call when it is in this file, whose callee the diagnostic shows.
|
|
259
|
+
*/
|
|
260
|
+
forbidOnly(located, call) {
|
|
261
|
+
const forbid = this.options.config?.forbidOnly ?? false;
|
|
262
|
+
if (forbid === false)
|
|
263
|
+
return;
|
|
264
|
+
const message = forbid === true
|
|
265
|
+
? 'The config sets forbidOnly, so this .only fails the run before any test runs.'
|
|
266
|
+
: `The config sets forbidOnly when ${forbid}, so then this .only fails the run before any test runs.`;
|
|
267
|
+
const callee = call && snippet(this.parsed.source.text.slice(call.callee.start, call.callee.end));
|
|
268
|
+
const found = problem('forbidden-only', located, message, callee);
|
|
269
|
+
this.diagnostics.push(this.diagnostic(forbid === true ? found : { ...found, level: 'warning' }));
|
|
270
|
+
}
|
|
147
271
|
suite(pending) {
|
|
148
272
|
const { scope, declaration, call } = pending;
|
|
273
|
+
if (declaration.isOnly)
|
|
274
|
+
this.forbidOnly(pending.site ?? call, pending.site ? null : call);
|
|
149
275
|
const parents = outward(scope).slice(1, -1).reverse();
|
|
150
276
|
const title = declaration.title;
|
|
151
277
|
// Suite ids are marked so that a describe and a test with the same title in the same place differ.
|
|
@@ -187,16 +313,47 @@ class RecordBuilder {
|
|
|
187
313
|
...runKeys([scope.details.run]),
|
|
188
314
|
};
|
|
189
315
|
}
|
|
190
|
-
test
|
|
316
|
+
/** What the config's Vitest tags set on a test with `tags`, merged as Vitest merges them; null when none applies. */
|
|
317
|
+
tagOptions(tags) {
|
|
318
|
+
const definitions = this.options.config?.tags?.definitions ?? [];
|
|
319
|
+
const applied = tags.flatMap((name) => definitions.filter((definition) => definition.name === name));
|
|
320
|
+
if (applied.length === 0)
|
|
321
|
+
return null;
|
|
322
|
+
// Vitest applies the tags from the highest number to the lowest, so the lowest wins; a tag without one goes first.
|
|
323
|
+
const rank = ({ priority }) => priority ?? Infinity;
|
|
324
|
+
applied.sort((a, b) => rank(b) - rank(a));
|
|
325
|
+
const merged = { run: {} };
|
|
326
|
+
for (const { options } of applied) {
|
|
327
|
+
const { run, meta, ...flags } = options;
|
|
328
|
+
Object.assign(merged, flags);
|
|
329
|
+
Object.assign(merged.run, run);
|
|
330
|
+
if (meta)
|
|
331
|
+
merged.meta = { ...merged.meta, ...meta };
|
|
332
|
+
}
|
|
333
|
+
return merged;
|
|
334
|
+
}
|
|
335
|
+
test(pending, suites) {
|
|
191
336
|
const { declaration } = pending;
|
|
337
|
+
const dependencies = this.options.dependencies(pending);
|
|
192
338
|
// A test from an imported helper is placed at the call in this file that registers it.
|
|
193
339
|
const located = pending.site ?? pending.call;
|
|
194
340
|
const scopes = outward(pending.scope);
|
|
195
341
|
const describes = scopes.filter((scope) => scope.kind === 'describe').reverse();
|
|
196
342
|
const title = declaration.title;
|
|
197
343
|
const id = this.id([this.describeTitles(describes), title.withoutTags]);
|
|
344
|
+
const fileScope = scopes[scopes.length - 1];
|
|
345
|
+
const tags = unique([
|
|
346
|
+
...fileScope.details.tags,
|
|
347
|
+
...describes.flatMap((scope) => scope.details.tags),
|
|
348
|
+
...declaration.details.tags,
|
|
349
|
+
]);
|
|
350
|
+
const fromTags = this.tagOptions(tags);
|
|
351
|
+
// In Vitest 5 the options of a test's tags win over those the describes pass down; before, the describes' win.
|
|
352
|
+
const tagsFirst = this.options.config?.tags?.beatsDescribes ?? true;
|
|
353
|
+
const tagged = (own, tag, around) => tagsFirst ? [...own, ...tag, ...around] : [...own, ...around, ...tag];
|
|
198
354
|
const chosen = chooseState([
|
|
199
|
-
...pending.states.
|
|
355
|
+
...declaredStates(pending.states.slice(0, declaration.states.length), fromTags, declaration),
|
|
356
|
+
...pending.states.slice(declaration.states.length).map((setting) => ({ setting, source: 'test' })),
|
|
200
357
|
...scopes.flatMap((scope) => scope.states.map((setting) => ({
|
|
201
358
|
setting,
|
|
202
359
|
source: scope.kind === 'file' ? 'file' : 'suite',
|
|
@@ -207,13 +364,14 @@ class RecordBuilder {
|
|
|
207
364
|
const state = rejected ? 'notLoaded' : (chosen?.setting.state ?? 'active');
|
|
208
365
|
const setting = rejected ? null : (chosen?.setting ?? null);
|
|
209
366
|
const suitePath = describes.flatMap((scope) => suiteOf(scope).declaration.title?.text ?? []);
|
|
210
|
-
const
|
|
211
|
-
const
|
|
367
|
+
const tagConcurrent = this.options.config?.tags?.concurrentBeatsTags ? undefined : fromTags?.concurrent;
|
|
368
|
+
const tagMode = tagConcurrent === undefined ? null : tagConcurrent ? 'parallel' : 'default';
|
|
369
|
+
const [mode = null] = tagged([declaration.mode], [tagMode], scopes.map((scope) => scope.mode)).filter((item) => item !== null);
|
|
212
370
|
const meta = {};
|
|
213
|
-
for (const values of [...describes.map((scope) => scope.details.meta), declaration.details.meta]) {
|
|
371
|
+
for (const values of [fromTags?.meta, ...describes.map((scope) => scope.details.meta), declaration.details.meta]) {
|
|
214
372
|
Object.assign(meta, values);
|
|
215
373
|
}
|
|
216
|
-
const isOnly = declaration.isOnly || scopes.some((scope) => scope.isOnly);
|
|
374
|
+
const isOnly = declaration.isOnly || scopes.some((scope) => scope.isOnly) || fromTags?.only === true;
|
|
217
375
|
const comments = this.options.comments === 'all' || (this.options.comments === 'non-active' && state !== 'active')
|
|
218
376
|
? pending.comments
|
|
219
377
|
: [];
|
|
@@ -225,15 +383,23 @@ class RecordBuilder {
|
|
|
225
383
|
const message = 'The test uses the bench fixture, which Vitest only provides in benchmark files such as *.bench.ts; elsewhere the test fails.';
|
|
226
384
|
this.diagnostics.push(this.diagnostic(problem('bench-outside-benchmark-file', located, message), id));
|
|
227
385
|
}
|
|
386
|
+
if (declaration.isOnly)
|
|
387
|
+
this.forbidOnly(located, pending.site ? null : pending.call);
|
|
228
388
|
if (pending.isInLoop) {
|
|
229
389
|
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));
|
|
230
390
|
}
|
|
231
|
-
if (declaration.isOnly
|
|
391
|
+
if ((declaration.isOnly || fromTags?.only === true) &&
|
|
392
|
+
chosen &&
|
|
393
|
+
chosen.source !== 'test' &&
|
|
394
|
+
chosen.setting.state === 'skip') {
|
|
232
395
|
this.diagnostics.push(this.diagnostic(problem('only-in-skipped-describe', located, 'The test uses .only, but a describe around it is skipped, so it is skipped too.'), id));
|
|
233
396
|
}
|
|
234
|
-
|
|
397
|
+
const record = {
|
|
235
398
|
id,
|
|
236
399
|
bodyHash: declaration.hashed ? hashNodes([declaration.hashed]) : null,
|
|
400
|
+
dependencyHash: dependencies.hash,
|
|
401
|
+
dependencies: dependencies.files,
|
|
402
|
+
hasUnfollowedDependencies: !dependencies.isComplete,
|
|
237
403
|
suiteId: describes.length > 0 ? this.suiteIds.get(describes[describes.length - 1]) : null,
|
|
238
404
|
suitePath,
|
|
239
405
|
relativeFilePath: this.options.relativeFilePath,
|
|
@@ -261,11 +427,7 @@ class RecordBuilder {
|
|
|
261
427
|
stateLine: rejected ? this.rejectedAt : (setting?.line ?? null),
|
|
262
428
|
isParallel: mode === 'parallel',
|
|
263
429
|
isSerial: mode === 'serial',
|
|
264
|
-
tags
|
|
265
|
-
...fileScope.details.tags,
|
|
266
|
-
...describes.flatMap((scope) => scope.details.tags),
|
|
267
|
-
...declaration.details.tags,
|
|
268
|
-
]),
|
|
430
|
+
tags,
|
|
269
431
|
annotations: [...describes.flatMap((scope) => scope.details.annotations), ...pending.annotations],
|
|
270
432
|
comments,
|
|
271
433
|
locks: unique([
|
|
@@ -276,11 +438,10 @@ class RecordBuilder {
|
|
|
276
438
|
meta: Object.keys(meta).length > 0 ? meta : null,
|
|
277
439
|
hooks: addHooks(scopes),
|
|
278
440
|
// Calls in the body and in `beforeEach` hooks run last, so they win over what the describes set.
|
|
279
|
-
...
|
|
280
|
-
pending.run,
|
|
281
|
-
...scopes.map((scope) => scope.hookRun),
|
|
282
|
-
declaration.details.run,
|
|
283
|
-
...scopes.map((scope) => scope.details.run),
|
|
441
|
+
...testRun([
|
|
442
|
+
{ run: pending.run, source: 'test' },
|
|
443
|
+
...scopes.map((scope) => ({ run: scope.hookRun, source: placeOf(scope) })),
|
|
444
|
+
...tagged([{ run: declaration.details.run, source: 'test' }], [{ run: fromTags?.run ?? {}, source: 'config' }], scopes.map((scope) => ({ run: scope.details.run, source: placeOf(scope) }))),
|
|
284
445
|
]),
|
|
285
446
|
fixtures: unique([...declaration.fixtures, ...[...scopes].reverse().flatMap((scope) => scope.hookFixtures)]),
|
|
286
447
|
expectCount: pending.expectCount,
|
|
@@ -294,6 +455,13 @@ class RecordBuilder {
|
|
|
294
455
|
isInLoop: pending.isInLoop,
|
|
295
456
|
isInCondition: pending.isInCondition,
|
|
296
457
|
isInFunction: pending.isInFunction,
|
|
458
|
+
projects: null,
|
|
459
|
+
};
|
|
460
|
+
const { projects, defaults } = this.options.config?.forTest(record, suites) ?? { projects: null, defaults: null };
|
|
461
|
+
return {
|
|
462
|
+
...withConfig(record, defaults),
|
|
463
|
+
isParallel: record.isParallel || (mode === null && defaults?.parallel === true),
|
|
464
|
+
projects,
|
|
297
465
|
};
|
|
298
466
|
}
|
|
299
467
|
/**
|
package/dist/scan-file.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { RunsWith } from './config-dependencies.ts';
|
|
2
|
+
import { DependencyGraph, type SnapshotFiles } from './dependency-hash.ts';
|
|
1
3
|
import type { ModuleReader } from './model.ts';
|
|
2
|
-
import { type CommentsMode, type FileRecords } from './records.ts';
|
|
4
|
+
import { type CommentsMode, type FileConfig, type FileRecords } from './records.ts';
|
|
3
5
|
import type { Framework } from './types.ts';
|
|
4
6
|
export interface FileSettings {
|
|
5
7
|
framework: Framework;
|
|
@@ -9,6 +11,16 @@ export interface FileSettings {
|
|
|
9
11
|
readModule: ModuleReader | null;
|
|
10
12
|
/** The project root, left out of error messages so that the output has no absolute paths. */
|
|
11
13
|
root: string | null;
|
|
14
|
+
/** Whether the config runs the file as a benchmark; null takes it from the file's name, as Vitest 5's default does. */
|
|
15
|
+
isBenchmarkFile?: boolean | null;
|
|
16
|
+
/** What the config the file uses sets for its tests; null without a config. */
|
|
17
|
+
config?: FileConfig | null;
|
|
18
|
+
/** Shared by the files of one scan, so that code many tests reach is read once. */
|
|
19
|
+
graph?: DependencyGraph;
|
|
20
|
+
/** What the file's config runs with each of its tests: the config, setup files and setup projects. */
|
|
21
|
+
runsWith?: RunsWith;
|
|
22
|
+
/** Reads the snapshot files tests compare against; none when one file's code is scanned. */
|
|
23
|
+
snapshotFiles?: SnapshotFiles;
|
|
12
24
|
}
|
|
13
25
|
export interface FileResult extends FileRecords {
|
|
14
26
|
usesGlobals: boolean;
|
package/dist/scan-file.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DependencyGraph, fileDependencies } from "./dependency-hash.js";
|
|
1
2
|
import { fileDiagnostic, toDiagnostic } from "./diagnostics.js";
|
|
2
3
|
import { extract, noHooks } from "./extract.js";
|
|
3
4
|
import { parseModule } from "./frameworks/bindings.js";
|
|
@@ -39,17 +40,28 @@ export function scanFile(code, relativeFilePath, settings) {
|
|
|
39
40
|
const helper = parseModule(module);
|
|
40
41
|
return helper.error ? null : frame(helper, module.relativeFilePath, settings, module.readModule);
|
|
41
42
|
});
|
|
43
|
+
const isTypeTest = settings.framework === 'vitest' && TYPE_TEST.test(relativeFilePath);
|
|
44
|
+
const unfollowed = [];
|
|
45
|
+
const dependencies = fileDependencies(settings.graph ?? new DependencyGraph(), main, extraction, isTypeTest, (place) => unfollowed.push(place), settings.runsWith, settings.snapshotFiles);
|
|
42
46
|
const records = buildRecords(extraction, parsed, {
|
|
43
47
|
relativeFilePath,
|
|
44
48
|
framework: settings.framework,
|
|
45
49
|
comments: settings.comments,
|
|
46
|
-
isTypeTest
|
|
47
|
-
// Vitest 5's default `benchmark.include
|
|
48
|
-
isBenchmarkFile: settings.framework === 'vitest' &&
|
|
50
|
+
isTypeTest,
|
|
51
|
+
// Vitest 5's default `benchmark.include`, when no config says.
|
|
52
|
+
isBenchmarkFile: settings.framework === 'vitest' &&
|
|
53
|
+
atLeast(settings.version, 5, 0) &&
|
|
54
|
+
(settings.isBenchmarkFile ?? BENCHMARK.test(relativeFilePath)),
|
|
55
|
+
config: settings.config ?? null,
|
|
56
|
+
dependencies,
|
|
49
57
|
});
|
|
50
58
|
return {
|
|
51
59
|
...records,
|
|
52
|
-
diagnostics: [
|
|
60
|
+
diagnostics: [
|
|
61
|
+
...diagnostics,
|
|
62
|
+
...records.diagnostics,
|
|
63
|
+
...unfollowed.map((place) => toDiagnostic(place.problem, place.relativeFilePath, place.source)),
|
|
64
|
+
],
|
|
53
65
|
usesGlobals: extraction.usesGlobals,
|
|
54
66
|
};
|
|
55
67
|
}
|
package/dist/scan-source.js
CHANGED
package/dist/scan.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ScanOptions } from './options.ts';
|
|
2
2
|
import type { Inventory } from './types.ts';
|
|
3
3
|
/**
|
|
4
|
-
* Scans the test files that match `patterns` under `root`, one file at a
|
|
4
|
+
* Scans the test files that match `patterns` under `root`, or without patterns those the configs run, one file at a
|
|
5
|
+
* time.
|
|
5
6
|
*
|
|
6
7
|
* Throws a `TypeError` for invalid options. Problems with files and code are reported as diagnostics.
|
|
7
8
|
*/
|