@vitest/runner 4.1.0-beta.2 → 4.1.0-beta.4
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/dist/chunk-tasks.js +29 -19
- package/dist/index.d.ts +5 -4
- package/dist/index.js +940 -411
- package/dist/{tasks.d-CLPU8HE4.d.ts → tasks.d-C-iOiT8j.d.ts} +466 -58
- package/dist/types.d.ts +2 -2
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { processError } from '@vitest/utils/error';
|
|
|
2
2
|
import { isObject, filterOutComments, createDefer, assertTypes, toArray, isNegativeNaN, unique, objectAttr, shuffle } from '@vitest/utils/helpers';
|
|
3
3
|
import { getSafeTimers } from '@vitest/utils/timers';
|
|
4
4
|
import { format, formatRegExp, objDisplay } from '@vitest/utils/display';
|
|
5
|
-
import {
|
|
5
|
+
import { w as getChainableContext, a as createChainable, v as validateTags, e as createTaskName, x as createNoTagsError, f as findTestFileStackTrace, d as createTagsFilter, b as createFileTask, c as calculateSuiteHash, u as someTasksAreOnly, q as interpretTaskModes, s as limitConcurrency, t as partitionSuiteChildren, p as hasTests, o as hasFailed } from './chunk-tasks.js';
|
|
6
6
|
import '@vitest/utils/source-map';
|
|
7
7
|
import 'pathe';
|
|
8
8
|
|
|
@@ -24,6 +24,18 @@ class TestRunAbortError extends Error {
|
|
|
24
24
|
this.reason = reason;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
class FixtureDependencyError extends Error {
|
|
28
|
+
name = "FixtureDependencyError";
|
|
29
|
+
}
|
|
30
|
+
class AroundHookSetupError extends Error {
|
|
31
|
+
name = "AroundHookSetupError";
|
|
32
|
+
}
|
|
33
|
+
class AroundHookTeardownError extends Error {
|
|
34
|
+
name = "AroundHookTeardownError";
|
|
35
|
+
}
|
|
36
|
+
class AroundHookMultipleCallsError extends Error {
|
|
37
|
+
name = "AroundHookMultipleCallsError";
|
|
38
|
+
}
|
|
27
39
|
|
|
28
40
|
// use WeakMap here to make the Test and Suite object serializable
|
|
29
41
|
const fnMap = new WeakMap();
|
|
@@ -38,7 +50,7 @@ function getFn(key) {
|
|
|
38
50
|
function setTestFixture(key, fixture) {
|
|
39
51
|
testFixtureMap.set(key, fixture);
|
|
40
52
|
}
|
|
41
|
-
function
|
|
53
|
+
function getTestFixtures(key) {
|
|
42
54
|
return testFixtureMap.get(key);
|
|
43
55
|
}
|
|
44
56
|
function setHooks(key, hooks) {
|
|
@@ -48,85 +60,182 @@ function getHooks(key) {
|
|
|
48
60
|
return hooksMap.get(key);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// will reference the original fixture instead of the scope
|
|
66
|
-
fixture.deps = (_fixture$deps = fixture.deps) === null || _fixture$deps === void 0 ? void 0 : _fixture$deps.map((dep) => newFixtures[dep.prop]);
|
|
67
|
-
}
|
|
68
|
-
return Object.values(newFixtures);
|
|
69
|
-
}
|
|
70
|
-
function mergeContextFixtures(fixtures, context, runner) {
|
|
71
|
-
const fixtureOptionKeys = [
|
|
63
|
+
class TestFixtures {
|
|
64
|
+
_suiteContexts;
|
|
65
|
+
_overrides = new WeakMap();
|
|
66
|
+
_registrations;
|
|
67
|
+
static _definitions = [];
|
|
68
|
+
static _builtinFixtures = [
|
|
69
|
+
"task",
|
|
70
|
+
"signal",
|
|
71
|
+
"onTestFailed",
|
|
72
|
+
"onTestFinished",
|
|
73
|
+
"skip",
|
|
74
|
+
"annotate"
|
|
75
|
+
];
|
|
76
|
+
static _fixtureOptionKeys = [
|
|
72
77
|
"auto",
|
|
73
78
|
"injected",
|
|
74
79
|
"scope"
|
|
75
80
|
];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
81
|
+
static _fixtureScopes = [
|
|
82
|
+
"test",
|
|
83
|
+
"file",
|
|
84
|
+
"worker"
|
|
85
|
+
];
|
|
86
|
+
static _workerContextSymbol = Symbol("workerContext");
|
|
87
|
+
static clearDefinitions() {
|
|
88
|
+
TestFixtures._definitions.length = 0;
|
|
89
|
+
}
|
|
90
|
+
static getWorkerContexts() {
|
|
91
|
+
return TestFixtures._definitions.map((f) => f.getWorkerContext());
|
|
92
|
+
}
|
|
93
|
+
static getFileContexts(file) {
|
|
94
|
+
return TestFixtures._definitions.map((f) => f.getFileContext(file));
|
|
95
|
+
}
|
|
96
|
+
constructor(registrations) {
|
|
97
|
+
this._registrations = registrations ?? new Map();
|
|
98
|
+
this._suiteContexts = new WeakMap();
|
|
99
|
+
TestFixtures._definitions.push(this);
|
|
100
|
+
}
|
|
101
|
+
extend(runner, userFixtures) {
|
|
102
|
+
const { suite } = getCurrentSuite();
|
|
103
|
+
const isTopLevel = !suite || suite.file === suite;
|
|
104
|
+
const registrations = this.parseUserFixtures(runner, userFixtures, isTopLevel);
|
|
105
|
+
return new TestFixtures(registrations);
|
|
106
|
+
}
|
|
107
|
+
get(suite) {
|
|
108
|
+
let currentSuite = suite;
|
|
109
|
+
while (currentSuite) {
|
|
110
|
+
const overrides = this._overrides.get(currentSuite);
|
|
111
|
+
// return the closest override
|
|
112
|
+
if (overrides) {
|
|
113
|
+
return overrides;
|
|
104
114
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
var _fixture$deps2;
|
|
108
|
-
(_fixture$deps2 = fixture.deps) === null || _fixture$deps2 === void 0 ? void 0 : _fixture$deps2.forEach((dep) => {
|
|
109
|
-
if (!dep.isFn) {
|
|
110
|
-
// non fn fixtures are always resolved and available to anyone
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
// worker scope can only import from worker scope
|
|
114
|
-
if (fixture.scope === "worker" && dep.scope === "worker") {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
// file scope an import from file and worker scopes
|
|
118
|
-
if (fixture.scope === "file" && dep.scope !== "test") {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
throw new SyntaxError(`cannot use the ${dep.scope} fixture "${dep.prop}" inside the ${fixture.scope} fixture "${fixture.prop}"`);
|
|
122
|
-
});
|
|
115
|
+
if (currentSuite === currentSuite.file) {
|
|
116
|
+
break;
|
|
123
117
|
}
|
|
118
|
+
currentSuite = currentSuite.suite || currentSuite.file;
|
|
119
|
+
}
|
|
120
|
+
return this._registrations;
|
|
121
|
+
}
|
|
122
|
+
override(runner, userFixtures) {
|
|
123
|
+
const { suite: currentSuite, file } = getCurrentSuite();
|
|
124
|
+
const suite = currentSuite || file;
|
|
125
|
+
const isTopLevel = !currentSuite || currentSuite.file === currentSuite;
|
|
126
|
+
// Create a copy of the closest parent's registrations to avoid modifying them
|
|
127
|
+
// For chained calls, this.get(suite) returns this suite's overrides; for first call, returns parent's
|
|
128
|
+
const suiteRegistrations = new Map(this.get(suite));
|
|
129
|
+
const registrations = this.parseUserFixtures(runner, userFixtures, isTopLevel, suiteRegistrations);
|
|
130
|
+
// If defined in top-level, just override all registrations
|
|
131
|
+
// We don't support overriding suite-level fixtures anyway (it will throw an error)
|
|
132
|
+
if (isTopLevel) {
|
|
133
|
+
this._registrations = registrations;
|
|
134
|
+
} else {
|
|
135
|
+
this._overrides.set(suite, registrations);
|
|
124
136
|
}
|
|
125
|
-
}
|
|
126
|
-
|
|
137
|
+
}
|
|
138
|
+
getFileContext(file) {
|
|
139
|
+
if (!this._suiteContexts.has(file)) {
|
|
140
|
+
this._suiteContexts.set(file, Object.create(null));
|
|
141
|
+
}
|
|
142
|
+
return this._suiteContexts.get(file);
|
|
143
|
+
}
|
|
144
|
+
getWorkerContext() {
|
|
145
|
+
if (!this._suiteContexts.has(TestFixtures._workerContextSymbol)) {
|
|
146
|
+
this._suiteContexts.set(TestFixtures._workerContextSymbol, Object.create(null));
|
|
147
|
+
}
|
|
148
|
+
return this._suiteContexts.get(TestFixtures._workerContextSymbol);
|
|
149
|
+
}
|
|
150
|
+
parseUserFixtures(runner, userFixtures, supportNonTest, registrations = new Map(this._registrations)) {
|
|
151
|
+
const errors = [];
|
|
152
|
+
Object.entries(userFixtures).forEach(([name, fn]) => {
|
|
153
|
+
let options;
|
|
154
|
+
let value;
|
|
155
|
+
let _options;
|
|
156
|
+
if (Array.isArray(fn) && fn.length >= 2 && isObject(fn[1]) && Object.keys(fn[1]).some((key) => TestFixtures._fixtureOptionKeys.includes(key))) {
|
|
157
|
+
_options = fn[1];
|
|
158
|
+
options = {
|
|
159
|
+
auto: _options.auto ?? false,
|
|
160
|
+
scope: _options.scope ?? "test",
|
|
161
|
+
injected: _options.injected ?? false
|
|
162
|
+
};
|
|
163
|
+
value = options.injected ? runner.injectValue?.(name) ?? fn[0] : fn[0];
|
|
164
|
+
} else {
|
|
165
|
+
value = fn;
|
|
166
|
+
}
|
|
167
|
+
const parent = registrations.get(name);
|
|
168
|
+
if (parent && options) {
|
|
169
|
+
if (parent.scope !== options.scope) {
|
|
170
|
+
errors.push(new FixtureDependencyError(`The "${name}" fixture was already registered with a "${options.scope}" scope.`));
|
|
171
|
+
}
|
|
172
|
+
if (parent.auto !== options.auto) {
|
|
173
|
+
errors.push(new FixtureDependencyError(`The "${name}" fixture was already registered as { auto: ${options.auto} }.`));
|
|
174
|
+
}
|
|
175
|
+
} else if (parent) {
|
|
176
|
+
options = {
|
|
177
|
+
auto: parent.auto,
|
|
178
|
+
scope: parent.scope,
|
|
179
|
+
injected: parent.injected
|
|
180
|
+
};
|
|
181
|
+
} else if (!options) {
|
|
182
|
+
options = {
|
|
183
|
+
auto: false,
|
|
184
|
+
injected: false,
|
|
185
|
+
scope: "test"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (options.scope && !TestFixtures._fixtureScopes.includes(options.scope)) {
|
|
189
|
+
errors.push(new FixtureDependencyError(`The "${name}" fixture has unknown scope "${options.scope}".`));
|
|
190
|
+
}
|
|
191
|
+
if (!supportNonTest && options.scope !== "test") {
|
|
192
|
+
errors.push(new FixtureDependencyError(`The "${name}" fixture cannot be defined with a ${options.scope} scope${!_options?.scope && parent?.scope ? " (inherited from the base fixture)" : ""} inside the describe block. Define it at the top level of the file instead.`));
|
|
193
|
+
}
|
|
194
|
+
const deps = isFixtureFunction(value) ? getUsedProps(value) : new Set();
|
|
195
|
+
const item = {
|
|
196
|
+
name,
|
|
197
|
+
value,
|
|
198
|
+
auto: options.auto ?? false,
|
|
199
|
+
injected: options.injected ?? false,
|
|
200
|
+
scope: options.scope ?? "test",
|
|
201
|
+
deps,
|
|
202
|
+
parent
|
|
203
|
+
};
|
|
204
|
+
registrations.set(name, item);
|
|
205
|
+
if (item.scope === "worker" && (runner.pool === "vmThreads" || runner.pool === "vmForks")) {
|
|
206
|
+
item.scope = "file";
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// validate fixture dependency scopes
|
|
210
|
+
for (const fixture of registrations.values()) {
|
|
211
|
+
for (const depName of fixture.deps) {
|
|
212
|
+
if (TestFixtures._builtinFixtures.includes(depName)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const dep = registrations.get(depName);
|
|
216
|
+
if (!dep) {
|
|
217
|
+
errors.push(new FixtureDependencyError(`The "${fixture.name}" fixture depends on unknown fixture "${depName}".`));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (depName === fixture.name && !fixture.parent) {
|
|
221
|
+
errors.push(new FixtureDependencyError(`The "${fixture.name}" fixture depends on itself, but does not have a base implementation.`));
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (TestFixtures._fixtureScopes.indexOf(fixture.scope) > TestFixtures._fixtureScopes.indexOf(dep.scope)) {
|
|
225
|
+
errors.push(new FixtureDependencyError(`The ${fixture.scope} "${fixture.name}" fixture cannot depend on a ${dep.scope} fixture "${dep.name}".`));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (errors.length === 1) {
|
|
231
|
+
throw errors[0];
|
|
232
|
+
} else if (errors.length > 1) {
|
|
233
|
+
throw new AggregateError(errors, "Cannot resolve user fixtures. See errors for more information.");
|
|
234
|
+
}
|
|
235
|
+
return registrations;
|
|
236
|
+
}
|
|
127
237
|
}
|
|
128
|
-
const
|
|
129
|
-
const cleanupFnArrayMap = new Map();
|
|
238
|
+
const cleanupFnArrayMap = new WeakMap();
|
|
130
239
|
async function callFixtureCleanup(context) {
|
|
131
240
|
const cleanupFnArray = cleanupFnArrayMap.get(context) ?? [];
|
|
132
241
|
for (const cleanup of cleanupFnArray.reverse()) {
|
|
@@ -134,96 +243,152 @@ async function callFixtureCleanup(context) {
|
|
|
134
243
|
}
|
|
135
244
|
cleanupFnArrayMap.delete(context);
|
|
136
245
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Returns the current number of cleanup functions registered for the context.
|
|
248
|
+
* This can be used as a checkpoint to later clean up only fixtures added after this point.
|
|
249
|
+
*/
|
|
250
|
+
function getFixtureCleanupCount(context) {
|
|
251
|
+
return cleanupFnArrayMap.get(context)?.length ?? 0;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Cleans up only fixtures that were added after the given checkpoint index.
|
|
255
|
+
* This is used by aroundEach to clean up fixtures created inside runTest()
|
|
256
|
+
* while preserving fixtures that were created for aroundEach itself.
|
|
257
|
+
*/
|
|
258
|
+
async function callFixtureCleanupFrom(context, fromIndex) {
|
|
259
|
+
const cleanupFnArray = cleanupFnArrayMap.get(context);
|
|
260
|
+
if (!cleanupFnArray || cleanupFnArray.length <= fromIndex) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Get items added after the checkpoint
|
|
264
|
+
const toCleanup = cleanupFnArray.slice(fromIndex);
|
|
265
|
+
// Clean up in reverse order
|
|
266
|
+
for (const cleanup of toCleanup.reverse()) {
|
|
267
|
+
await cleanup();
|
|
268
|
+
}
|
|
269
|
+
// Remove cleaned up items from the array, keeping items before checkpoint
|
|
270
|
+
cleanupFnArray.length = fromIndex;
|
|
271
|
+
}
|
|
272
|
+
const contextHasFixturesCache = new WeakMap();
|
|
273
|
+
function withFixtures(fn, options) {
|
|
274
|
+
const collector = getCurrentSuite();
|
|
275
|
+
const suite = collector.suite || collector.file;
|
|
276
|
+
return async (hookContext) => {
|
|
277
|
+
const context = hookContext || options?.context;
|
|
140
278
|
if (!context) {
|
|
279
|
+
if (options?.suiteHook) {
|
|
280
|
+
validateSuiteHook(fn, options.suiteHook, options.stackTraceError);
|
|
281
|
+
}
|
|
141
282
|
return fn({});
|
|
142
283
|
}
|
|
143
|
-
const fixtures =
|
|
144
|
-
if (!
|
|
284
|
+
const fixtures = options?.fixtures || getTestFixtures(context);
|
|
285
|
+
if (!fixtures) {
|
|
145
286
|
return fn(context);
|
|
146
287
|
}
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
if (!usedProps.length && !hasAutoFixture) {
|
|
288
|
+
const registrations = fixtures.get(suite);
|
|
289
|
+
if (!registrations.size) {
|
|
150
290
|
return fn(context);
|
|
151
291
|
}
|
|
152
|
-
|
|
153
|
-
|
|
292
|
+
const usedFixtures = [];
|
|
293
|
+
const usedProps = getUsedProps(fn);
|
|
294
|
+
for (const fixture of registrations.values()) {
|
|
295
|
+
if (fixture.auto || usedProps.has(fixture.name)) {
|
|
296
|
+
usedFixtures.push(fixture);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!usedFixtures.length) {
|
|
300
|
+
return fn(context);
|
|
154
301
|
}
|
|
155
|
-
const fixtureValueMap = fixtureValueMaps.get(context);
|
|
156
302
|
if (!cleanupFnArrayMap.has(context)) {
|
|
157
303
|
cleanupFnArrayMap.set(context, []);
|
|
158
304
|
}
|
|
159
305
|
const cleanupFnArray = cleanupFnArrayMap.get(context);
|
|
160
|
-
const
|
|
161
|
-
const pendingFixtures = resolveDeps(usedFixtures);
|
|
306
|
+
const pendingFixtures = resolveDeps(usedFixtures, registrations);
|
|
162
307
|
if (!pendingFixtures.length) {
|
|
163
308
|
return fn(context);
|
|
164
309
|
}
|
|
165
|
-
|
|
166
|
-
|
|
310
|
+
// Check if suite-level hook is trying to access test-scoped fixtures
|
|
311
|
+
// Suite hooks (beforeAll/afterAll/aroundAll) can only access file/worker scoped fixtures
|
|
312
|
+
if (options?.suiteHook) {
|
|
313
|
+
const testScopedFixtures = pendingFixtures.filter((f) => f.scope === "test");
|
|
314
|
+
if (testScopedFixtures.length > 0) {
|
|
315
|
+
const fixtureNames = testScopedFixtures.map((f) => `"${f.name}"`).join(", ");
|
|
316
|
+
const alternativeHook = {
|
|
317
|
+
aroundAll: "aroundEach",
|
|
318
|
+
beforeAll: "beforeEach",
|
|
319
|
+
afterAll: "afterEach"
|
|
320
|
+
};
|
|
321
|
+
const error = new FixtureDependencyError(`Test-scoped fixtures cannot be used inside ${options.suiteHook} hook. ` + `The following fixtures are test-scoped: ${fixtureNames}. ` + `Use { scope: 'file' } or { scope: 'worker' } fixtures instead, or move the logic to ${alternativeHook[options.suiteHook]} hook.`);
|
|
322
|
+
// Use stack trace from hook registration for better error location
|
|
323
|
+
if (options.stackTraceError?.stack) {
|
|
324
|
+
error.stack = error.message + options.stackTraceError.stack.replace(options.stackTraceError.message, "");
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (!contextHasFixturesCache.has(context)) {
|
|
330
|
+
contextHasFixturesCache.set(context, new WeakSet());
|
|
331
|
+
}
|
|
332
|
+
const cachedFixtures = contextHasFixturesCache.get(context);
|
|
333
|
+
for (const fixture of pendingFixtures) {
|
|
334
|
+
if (fixture.scope === "test") {
|
|
167
335
|
// fixture could be already initialized during "before" hook
|
|
168
|
-
|
|
336
|
+
// we can't check "fixture.name" in context because context may
|
|
337
|
+
// access the parent fixture ({ a: ({ a }) => {} })
|
|
338
|
+
if (cachedFixtures.has(fixture)) {
|
|
169
339
|
continue;
|
|
170
340
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
341
|
+
cachedFixtures.add(fixture);
|
|
342
|
+
const resolvedValue = await resolveTestFixtureValue(fixture, context, cleanupFnArray);
|
|
343
|
+
context[fixture.name] = resolvedValue;
|
|
344
|
+
cleanupFnArray.push(() => {
|
|
345
|
+
cachedFixtures.delete(fixture);
|
|
346
|
+
});
|
|
347
|
+
} else {
|
|
348
|
+
const resolvedValue = await resolveScopeFixtureValue(fixtures, suite, fixture);
|
|
349
|
+
context[fixture.name] = resolvedValue;
|
|
179
350
|
}
|
|
180
351
|
}
|
|
181
|
-
return
|
|
352
|
+
return fn(context);
|
|
182
353
|
};
|
|
183
354
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (!fixture.isFn) {
|
|
190
|
-
var _fixture$prop;
|
|
191
|
-
fileContext[_fixture$prop = fixture.prop] ?? (fileContext[_fixture$prop] = fixture.value);
|
|
192
|
-
if (workerContext) {
|
|
193
|
-
var _fixture$prop2;
|
|
194
|
-
workerContext[_fixture$prop2 = fixture.prop] ?? (workerContext[_fixture$prop2] = fixture.value);
|
|
195
|
-
}
|
|
355
|
+
function isFixtureFunction(value) {
|
|
356
|
+
return typeof value === "function";
|
|
357
|
+
}
|
|
358
|
+
function resolveTestFixtureValue(fixture, context, cleanupFnArray) {
|
|
359
|
+
if (!isFixtureFunction(fixture.value)) {
|
|
196
360
|
return fixture.value;
|
|
197
361
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
362
|
+
return resolveFixtureFunction(fixture.value, context, cleanupFnArray);
|
|
363
|
+
}
|
|
364
|
+
const scopedFixturePromiseCache = new WeakMap();
|
|
365
|
+
async function resolveScopeFixtureValue(fixtures, suite, fixture) {
|
|
366
|
+
const workerContext = fixtures.getWorkerContext();
|
|
367
|
+
const fileContext = fixtures.getFileContext(suite.file);
|
|
368
|
+
const fixtureContext = fixture.scope === "worker" ? workerContext : fileContext;
|
|
369
|
+
if (!isFixtureFunction(fixture.value)) {
|
|
370
|
+
fixtureContext[fixture.name] = fixture.value;
|
|
371
|
+
return fixture.value;
|
|
204
372
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (!workerContext) {
|
|
208
|
-
throw new TypeError("[@vitest/runner] The worker context is not available in the current test runner. Please, provide the `getWorkerContext` method when initiating the runner.");
|
|
209
|
-
}
|
|
210
|
-
fixtureContext = workerContext;
|
|
211
|
-
} else {
|
|
212
|
-
fixtureContext = fileContext;
|
|
373
|
+
if (fixture.name in fixtureContext) {
|
|
374
|
+
return fixtureContext[fixture.name];
|
|
213
375
|
}
|
|
214
|
-
if (fixture
|
|
215
|
-
return
|
|
376
|
+
if (scopedFixturePromiseCache.has(fixture)) {
|
|
377
|
+
return scopedFixturePromiseCache.get(fixture);
|
|
216
378
|
}
|
|
217
379
|
if (!cleanupFnArrayMap.has(fixtureContext)) {
|
|
218
380
|
cleanupFnArrayMap.set(fixtureContext, []);
|
|
219
381
|
}
|
|
220
382
|
const cleanupFnFileArray = cleanupFnArrayMap.get(fixtureContext);
|
|
221
|
-
const promise = resolveFixtureFunction(fixture.value,
|
|
222
|
-
|
|
223
|
-
|
|
383
|
+
const promise = resolveFixtureFunction(fixture.value, fixture.scope === "file" ? {
|
|
384
|
+
...workerContext,
|
|
385
|
+
...fileContext
|
|
386
|
+
} : fixtureContext, cleanupFnFileArray).then((value) => {
|
|
387
|
+
fixtureContext[fixture.name] = value;
|
|
388
|
+
scopedFixturePromiseCache.delete(fixture);
|
|
224
389
|
return value;
|
|
225
390
|
});
|
|
226
|
-
|
|
391
|
+
scopedFixturePromiseCache.set(fixture, promise);
|
|
227
392
|
return promise;
|
|
228
393
|
}
|
|
229
394
|
async function resolveFixtureFunction(fixtureFn, context, cleanupFnArray) {
|
|
@@ -254,27 +419,50 @@ async function resolveFixtureFunction(fixtureFn, context, cleanupFnArray) {
|
|
|
254
419
|
});
|
|
255
420
|
return useFnArgPromise;
|
|
256
421
|
}
|
|
257
|
-
function resolveDeps(
|
|
258
|
-
|
|
422
|
+
function resolveDeps(usedFixtures, registrations, depSet = new Set(), pendingFixtures = []) {
|
|
423
|
+
usedFixtures.forEach((fixture) => {
|
|
259
424
|
if (pendingFixtures.includes(fixture)) {
|
|
260
425
|
return;
|
|
261
426
|
}
|
|
262
|
-
if (!fixture.
|
|
427
|
+
if (!isFixtureFunction(fixture.value) || !fixture.deps) {
|
|
263
428
|
pendingFixtures.push(fixture);
|
|
264
429
|
return;
|
|
265
430
|
}
|
|
266
431
|
if (depSet.has(fixture)) {
|
|
267
|
-
|
|
432
|
+
if (fixture.parent) {
|
|
433
|
+
fixture = fixture.parent;
|
|
434
|
+
} else {
|
|
435
|
+
throw new Error(`Circular fixture dependency detected: ${fixture.name} <- ${[...depSet].reverse().map((d) => d.name).join(" <- ")}`);
|
|
436
|
+
}
|
|
268
437
|
}
|
|
269
438
|
depSet.add(fixture);
|
|
270
|
-
resolveDeps(fixture.deps, depSet, pendingFixtures);
|
|
439
|
+
resolveDeps([...fixture.deps].map((n) => n === fixture.name ? fixture.parent : registrations.get(n)).filter((n) => !!n), registrations, depSet, pendingFixtures);
|
|
271
440
|
pendingFixtures.push(fixture);
|
|
272
441
|
depSet.clear();
|
|
273
442
|
});
|
|
274
443
|
return pendingFixtures;
|
|
275
444
|
}
|
|
445
|
+
function validateSuiteHook(fn, hook, error) {
|
|
446
|
+
const usedProps = getUsedProps(fn);
|
|
447
|
+
if (usedProps.size) {
|
|
448
|
+
console.warn(`The ${hook} hook uses fixtures "${[...usedProps].join("\", \"")}", but has no access to context. Did you forget to call it as "test.${hook}()" instead of "${hook}()"? This will throw an error in a future major. See https://vitest.dev/guide/test-context#suite-level-hooks`);
|
|
449
|
+
if (error) {
|
|
450
|
+
const processor = globalThis.__vitest_worker__?.onFilterStackTrace || ((s) => s || "");
|
|
451
|
+
const stack = processor(error.stack || "");
|
|
452
|
+
console.warn(stack);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const kPropsSymbol = Symbol("$vitest:fixture-props");
|
|
457
|
+
function configureProps(fn, options) {
|
|
458
|
+
Object.defineProperty(fn, kPropsSymbol, {
|
|
459
|
+
value: options,
|
|
460
|
+
enumerable: false
|
|
461
|
+
});
|
|
462
|
+
}
|
|
276
463
|
function getUsedProps(fn) {
|
|
277
|
-
|
|
464
|
+
const { index: fixturesIndex = 0, original: implementation = fn } = kPropsSymbol in fn ? fn[kPropsSymbol] : {};
|
|
465
|
+
let fnString = filterOutComments(implementation.toString());
|
|
278
466
|
// match lowered async function and strip it off
|
|
279
467
|
// example code on esbuild-try https://esbuild.github.io/try/#YgAwLjI0LjAALS1zdXBwb3J0ZWQ6YXN5bmMtYXdhaXQ9ZmFsc2UAZQBlbnRyeS50cwBjb25zdCBvID0gewogIGYxOiBhc3luYyAoKSA9PiB7fSwKICBmMjogYXN5bmMgKGEpID0+IHt9LAogIGYzOiBhc3luYyAoYSwgYikgPT4ge30sCiAgZjQ6IGFzeW5jIGZ1bmN0aW9uKGEpIHt9LAogIGY1OiBhc3luYyBmdW5jdGlvbiBmZihhKSB7fSwKICBhc3luYyBmNihhKSB7fSwKCiAgZzE6IGFzeW5jICgpID0+IHt9LAogIGcyOiBhc3luYyAoeyBhIH0pID0+IHt9LAogIGczOiBhc3luYyAoeyBhIH0sIGIpID0+IHt9LAogIGc0OiBhc3luYyBmdW5jdGlvbiAoeyBhIH0pIHt9LAogIGc1OiBhc3luYyBmdW5jdGlvbiBnZyh7IGEgfSkge30sCiAgYXN5bmMgZzYoeyBhIH0pIHt9LAoKICBoMTogYXN5bmMgKCkgPT4ge30sCiAgLy8gY29tbWVudCBiZXR3ZWVuCiAgaDI6IGFzeW5jIChhKSA9PiB7fSwKfQ
|
|
280
468
|
// __async(this, null, function*
|
|
@@ -285,23 +473,20 @@ function getUsedProps(fn) {
|
|
|
285
473
|
}
|
|
286
474
|
const match = fnString.match(/[^(]*\(([^)]*)/);
|
|
287
475
|
if (!match) {
|
|
288
|
-
return
|
|
476
|
+
return new Set();
|
|
289
477
|
}
|
|
290
478
|
const args = splitByComma(match[1]);
|
|
291
479
|
if (!args.length) {
|
|
292
|
-
return
|
|
480
|
+
return new Set();
|
|
293
481
|
}
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
if (!first) {
|
|
298
|
-
return [];
|
|
299
|
-
}
|
|
482
|
+
const fixturesArgument = args[fixturesIndex];
|
|
483
|
+
if (!fixturesArgument) {
|
|
484
|
+
return new Set();
|
|
300
485
|
}
|
|
301
|
-
if (!(
|
|
302
|
-
throw new Error(`The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${
|
|
486
|
+
if (!(fixturesArgument[0] === "{" && fixturesArgument.endsWith("}"))) {
|
|
487
|
+
throw new Error(`The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${fixturesArgument}".`);
|
|
303
488
|
}
|
|
304
|
-
const _first =
|
|
489
|
+
const _first = fixturesArgument.slice(1, -1).replace(/\s/g, "");
|
|
305
490
|
const props = splitByComma(_first).map((prop) => {
|
|
306
491
|
return prop.replace(/:.*|=.*/g, "");
|
|
307
492
|
});
|
|
@@ -309,7 +494,7 @@ function getUsedProps(fn) {
|
|
|
309
494
|
if (last && last.startsWith("...")) {
|
|
310
495
|
throw new Error(`Rest parameters are not supported in fixtures, received "${last}".`);
|
|
311
496
|
}
|
|
312
|
-
return props;
|
|
497
|
+
return new Set(props);
|
|
313
498
|
}
|
|
314
499
|
function splitByComma(s) {
|
|
315
500
|
const result = [];
|
|
@@ -358,6 +543,8 @@ function getDefaultHookTimeout() {
|
|
|
358
543
|
}
|
|
359
544
|
const CLEANUP_TIMEOUT_KEY = Symbol.for("VITEST_CLEANUP_TIMEOUT");
|
|
360
545
|
const CLEANUP_STACK_TRACE_KEY = Symbol.for("VITEST_CLEANUP_STACK_TRACE");
|
|
546
|
+
const AROUND_TIMEOUT_KEY = Symbol.for("VITEST_AROUND_TIMEOUT");
|
|
547
|
+
const AROUND_STACK_TRACE_KEY = Symbol.for("VITEST_AROUND_STACK_TRACE");
|
|
361
548
|
function getBeforeHookCleanupCallback(hook, result, context) {
|
|
362
549
|
if (typeof result === "function") {
|
|
363
550
|
const timeout = CLEANUP_TIMEOUT_KEY in hook && typeof hook[CLEANUP_TIMEOUT_KEY] === "number" ? hook[CLEANUP_TIMEOUT_KEY] : getDefaultHookTimeout();
|
|
@@ -389,7 +576,8 @@ function getBeforeHookCleanupCallback(hook, result, context) {
|
|
|
389
576
|
function beforeAll(fn, timeout = getDefaultHookTimeout()) {
|
|
390
577
|
assertTypes(fn, "\"beforeAll\" callback", ["function"]);
|
|
391
578
|
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
392
|
-
|
|
579
|
+
const context = getChainableContext(this);
|
|
580
|
+
return getCurrentSuite().on("beforeAll", Object.assign(withTimeout(withSuiteFixtures("beforeAll", fn, context, stackTraceError), timeout, true, stackTraceError), {
|
|
393
581
|
[CLEANUP_TIMEOUT_KEY]: timeout,
|
|
394
582
|
[CLEANUP_STACK_TRACE_KEY]: stackTraceError
|
|
395
583
|
}));
|
|
@@ -413,7 +601,9 @@ function beforeAll(fn, timeout = getDefaultHookTimeout()) {
|
|
|
413
601
|
*/
|
|
414
602
|
function afterAll(fn, timeout) {
|
|
415
603
|
assertTypes(fn, "\"afterAll\" callback", ["function"]);
|
|
416
|
-
|
|
604
|
+
const context = getChainableContext(this);
|
|
605
|
+
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
606
|
+
return getCurrentSuite().on("afterAll", withTimeout(withSuiteFixtures("afterAll", fn, context, stackTraceError), timeout ?? getDefaultHookTimeout(), true, stackTraceError));
|
|
417
607
|
}
|
|
418
608
|
/**
|
|
419
609
|
* Registers a callback function to be executed before each test within the current suite.
|
|
@@ -435,8 +625,7 @@ function afterAll(fn, timeout) {
|
|
|
435
625
|
function beforeEach(fn, timeout = getDefaultHookTimeout()) {
|
|
436
626
|
assertTypes(fn, "\"beforeEach\" callback", ["function"]);
|
|
437
627
|
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
438
|
-
|
|
439
|
-
return getCurrentSuite().on("beforeEach", Object.assign(withTimeout(withFixtures(runner, fn), timeout ?? getDefaultHookTimeout(), true, stackTraceError, abortIfTimeout), {
|
|
628
|
+
return getCurrentSuite().on("beforeEach", Object.assign(withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true, stackTraceError, abortIfTimeout), {
|
|
440
629
|
[CLEANUP_TIMEOUT_KEY]: timeout,
|
|
441
630
|
[CLEANUP_STACK_TRACE_KEY]: stackTraceError
|
|
442
631
|
}));
|
|
@@ -460,8 +649,7 @@ function beforeEach(fn, timeout = getDefaultHookTimeout()) {
|
|
|
460
649
|
*/
|
|
461
650
|
function afterEach(fn, timeout) {
|
|
462
651
|
assertTypes(fn, "\"afterEach\" callback", ["function"]);
|
|
463
|
-
|
|
464
|
-
return getCurrentSuite().on("afterEach", withTimeout(withFixtures(runner, fn), timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
|
|
652
|
+
return getCurrentSuite().on("afterEach", withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
|
|
465
653
|
}
|
|
466
654
|
/**
|
|
467
655
|
* Registers a callback function to be executed when a test fails within the current suite.
|
|
@@ -482,7 +670,7 @@ function afterEach(fn, timeout) {
|
|
|
482
670
|
* ```
|
|
483
671
|
*/
|
|
484
672
|
const onTestFailed = createTestHook("onTestFailed", (test, handler, timeout) => {
|
|
485
|
-
test.onFailed
|
|
673
|
+
test.onFailed ||= [];
|
|
486
674
|
test.onFailed.push(withTimeout(handler, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
|
|
487
675
|
});
|
|
488
676
|
/**
|
|
@@ -509,9 +697,116 @@ const onTestFailed = createTestHook("onTestFailed", (test, handler, timeout) =>
|
|
|
509
697
|
* ```
|
|
510
698
|
*/
|
|
511
699
|
const onTestFinished = createTestHook("onTestFinished", (test, handler, timeout) => {
|
|
512
|
-
test.onFinished
|
|
700
|
+
test.onFinished ||= [];
|
|
513
701
|
test.onFinished.push(withTimeout(handler, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
|
|
514
702
|
});
|
|
703
|
+
/**
|
|
704
|
+
* Registers a callback function that wraps around all tests within the current suite.
|
|
705
|
+
* The callback receives a `runSuite` function that must be called to run the suite's tests.
|
|
706
|
+
* This hook is useful for scenarios where you need to wrap an entire suite in a context
|
|
707
|
+
* (e.g., starting a server, opening a database connection that all tests share).
|
|
708
|
+
*
|
|
709
|
+
* **Note:** When multiple `aroundAll` hooks are registered, they are nested inside each other.
|
|
710
|
+
* The first registered hook is the outermost wrapper.
|
|
711
|
+
*
|
|
712
|
+
* @param {Function} fn - The callback function that wraps the suite. Must call `runSuite()` to run the tests.
|
|
713
|
+
* @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
|
|
714
|
+
* @returns {void}
|
|
715
|
+
* @example
|
|
716
|
+
* ```ts
|
|
717
|
+
* // Example of using aroundAll to wrap suite in a tracing span
|
|
718
|
+
* aroundAll(async (runSuite) => {
|
|
719
|
+
* await tracer.trace('test-suite', runSuite);
|
|
720
|
+
* });
|
|
721
|
+
* ```
|
|
722
|
+
* @example
|
|
723
|
+
* ```ts
|
|
724
|
+
* // Example of using aroundAll with fixtures
|
|
725
|
+
* aroundAll(async (runSuite, { db }) => {
|
|
726
|
+
* await db.transaction(() => runSuite());
|
|
727
|
+
* });
|
|
728
|
+
* ```
|
|
729
|
+
*/
|
|
730
|
+
function aroundAll(fn, timeout) {
|
|
731
|
+
assertTypes(fn, "\"aroundAll\" callback", ["function"]);
|
|
732
|
+
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
733
|
+
const resolvedTimeout = timeout ?? getDefaultHookTimeout();
|
|
734
|
+
const context = getChainableContext(this);
|
|
735
|
+
return getCurrentSuite().on("aroundAll", Object.assign(withSuiteFixtures("aroundAll", fn, context, stackTraceError, 1), {
|
|
736
|
+
[AROUND_TIMEOUT_KEY]: resolvedTimeout,
|
|
737
|
+
[AROUND_STACK_TRACE_KEY]: stackTraceError
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Registers a callback function that wraps around each test within the current suite.
|
|
742
|
+
* The callback receives a `runTest` function that must be called to run the test.
|
|
743
|
+
* This hook is useful for scenarios where you need to wrap tests in a context (e.g., database transactions).
|
|
744
|
+
*
|
|
745
|
+
* **Note:** When multiple `aroundEach` hooks are registered, they are nested inside each other.
|
|
746
|
+
* The first registered hook is the outermost wrapper.
|
|
747
|
+
*
|
|
748
|
+
* @param {Function} fn - The callback function that wraps the test. Must call `runTest()` to run the test.
|
|
749
|
+
* @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
|
|
750
|
+
* @returns {void}
|
|
751
|
+
* @example
|
|
752
|
+
* ```ts
|
|
753
|
+
* // Example of using aroundEach to wrap tests in a database transaction
|
|
754
|
+
* aroundEach(async (runTest) => {
|
|
755
|
+
* await database.transaction(() => runTest());
|
|
756
|
+
* });
|
|
757
|
+
* ```
|
|
758
|
+
* @example
|
|
759
|
+
* ```ts
|
|
760
|
+
* // Example of using aroundEach with fixtures
|
|
761
|
+
* aroundEach(async (runTest, { db }) => {
|
|
762
|
+
* await db.transaction(() => runTest());
|
|
763
|
+
* });
|
|
764
|
+
* ```
|
|
765
|
+
*/
|
|
766
|
+
function aroundEach(fn, timeout) {
|
|
767
|
+
assertTypes(fn, "\"aroundEach\" callback", ["function"]);
|
|
768
|
+
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
769
|
+
const resolvedTimeout = timeout ?? getDefaultHookTimeout();
|
|
770
|
+
const wrapper = (runTest, context, suite) => {
|
|
771
|
+
const innerFn = (ctx) => fn(runTest, ctx, suite);
|
|
772
|
+
configureProps(innerFn, {
|
|
773
|
+
index: 1,
|
|
774
|
+
original: fn
|
|
775
|
+
});
|
|
776
|
+
const fixtureResolver = withFixtures(innerFn);
|
|
777
|
+
return fixtureResolver(context);
|
|
778
|
+
};
|
|
779
|
+
return getCurrentSuite().on("aroundEach", Object.assign(wrapper, {
|
|
780
|
+
[AROUND_TIMEOUT_KEY]: resolvedTimeout,
|
|
781
|
+
[AROUND_STACK_TRACE_KEY]: stackTraceError
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
function withSuiteFixtures(suiteHook, fn, context, stackTraceError, contextIndex = 0) {
|
|
785
|
+
return (...args) => {
|
|
786
|
+
const suite = args.at(-1);
|
|
787
|
+
const prefix = args.slice(0, -1);
|
|
788
|
+
const wrapper = (ctx) => fn(...prefix, ctx, suite);
|
|
789
|
+
configureProps(wrapper, {
|
|
790
|
+
index: contextIndex,
|
|
791
|
+
original: fn
|
|
792
|
+
});
|
|
793
|
+
const fixtures = context?.getFixtures();
|
|
794
|
+
const fileContext = fixtures?.getFileContext(suite.file);
|
|
795
|
+
const fixtured = withFixtures(wrapper, {
|
|
796
|
+
suiteHook,
|
|
797
|
+
fixtures,
|
|
798
|
+
context: fileContext,
|
|
799
|
+
stackTraceError
|
|
800
|
+
});
|
|
801
|
+
return fixtured();
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function getAroundHookTimeout(hook) {
|
|
805
|
+
return AROUND_TIMEOUT_KEY in hook && typeof hook[AROUND_TIMEOUT_KEY] === "number" ? hook[AROUND_TIMEOUT_KEY] : getDefaultHookTimeout();
|
|
806
|
+
}
|
|
807
|
+
function getAroundHookStackTrace(hook) {
|
|
808
|
+
return AROUND_STACK_TRACE_KEY in hook && hook[AROUND_STACK_TRACE_KEY] instanceof Error ? hook[AROUND_STACK_TRACE_KEY] : undefined;
|
|
809
|
+
}
|
|
515
810
|
function createTestHook(name, handler) {
|
|
516
811
|
return (fn, timeout) => {
|
|
517
812
|
assertTypes(fn, `"${name}" callback`, ["function"]);
|
|
@@ -698,7 +993,9 @@ function createSuiteHooks() {
|
|
|
698
993
|
beforeAll: [],
|
|
699
994
|
afterAll: [],
|
|
700
995
|
beforeEach: [],
|
|
701
|
-
afterEach: []
|
|
996
|
+
afterEach: [],
|
|
997
|
+
aroundEach: [],
|
|
998
|
+
aroundAll: []
|
|
702
999
|
};
|
|
703
1000
|
}
|
|
704
1001
|
const POSITIVE_INFINITY = Number.POSITIVE_INFINITY;
|
|
@@ -728,49 +1025,63 @@ function parseArguments(optionsOrFn, timeoutOrTest) {
|
|
|
728
1025
|
};
|
|
729
1026
|
}
|
|
730
1027
|
// implementations
|
|
731
|
-
function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions
|
|
1028
|
+
function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions) {
|
|
732
1029
|
const tasks = [];
|
|
733
1030
|
let suite;
|
|
734
1031
|
initSuite(true);
|
|
735
1032
|
const task = function(name = "", options = {}) {
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
const parentTags = (parentTask === null || parentTask === void 0 ? void 0 : parentTask.tags) || [];
|
|
1033
|
+
const currentSuite = collectorContext.currentSuite?.suite;
|
|
1034
|
+
const parentTask = currentSuite ?? collectorContext.currentSuite?.file;
|
|
1035
|
+
const parentTags = parentTask?.tags || [];
|
|
740
1036
|
const testTags = unique([...parentTags, ...toArray(options.tags)]);
|
|
741
1037
|
const tagsOptions = testTags.map((tag) => {
|
|
742
|
-
|
|
743
|
-
const tagDefinition = (_runner$config$tags = runner.config.tags) === null || _runner$config$tags === void 0 ? void 0 : _runner$config$tags.find((t) => t.name === tag);
|
|
1038
|
+
const tagDefinition = runner.config.tags?.find((t) => t.name === tag);
|
|
744
1039
|
if (!tagDefinition && runner.config.strictTags) {
|
|
745
1040
|
throw createNoTagsError(runner.config.tags, tag);
|
|
746
1041
|
}
|
|
747
1042
|
return tagDefinition;
|
|
748
1043
|
}).filter((r) => r != null).sort((tag1, tag2) => (tag2.priority ?? POSITIVE_INFINITY) - (tag1.priority ?? POSITIVE_INFINITY)).reduce((acc, tag) => {
|
|
749
|
-
const { name, description, priority, ...options } = tag;
|
|
1044
|
+
const { name, description, priority, meta, ...options } = tag;
|
|
750
1045
|
Object.assign(acc, options);
|
|
1046
|
+
if (meta) {
|
|
1047
|
+
acc.meta = Object.assign(acc.meta ?? Object.create(null), meta);
|
|
1048
|
+
}
|
|
751
1049
|
return acc;
|
|
752
1050
|
}, {});
|
|
1051
|
+
const testOwnMeta = options.meta;
|
|
753
1052
|
options = {
|
|
754
1053
|
...tagsOptions,
|
|
755
1054
|
...options
|
|
756
1055
|
};
|
|
757
1056
|
const timeout = options.timeout ?? runner.config.testTimeout;
|
|
1057
|
+
const parentMeta = currentSuite?.meta;
|
|
1058
|
+
const tagMeta = tagsOptions.meta;
|
|
1059
|
+
const testMeta = Object.create(null);
|
|
1060
|
+
if (tagMeta) {
|
|
1061
|
+
Object.assign(testMeta, tagMeta);
|
|
1062
|
+
}
|
|
1063
|
+
if (parentMeta) {
|
|
1064
|
+
Object.assign(testMeta, parentMeta);
|
|
1065
|
+
}
|
|
1066
|
+
if (testOwnMeta) {
|
|
1067
|
+
Object.assign(testMeta, testOwnMeta);
|
|
1068
|
+
}
|
|
758
1069
|
const task = {
|
|
759
1070
|
id: "",
|
|
760
1071
|
name,
|
|
761
|
-
fullName: createTaskName([
|
|
762
|
-
fullTestName: createTaskName([currentSuite
|
|
1072
|
+
fullName: createTaskName([currentSuite?.fullName ?? collectorContext.currentSuite?.file?.fullName, name]),
|
|
1073
|
+
fullTestName: createTaskName([currentSuite?.fullTestName, name]),
|
|
763
1074
|
suite: currentSuite,
|
|
764
1075
|
each: options.each,
|
|
765
1076
|
fails: options.fails,
|
|
766
1077
|
context: undefined,
|
|
767
1078
|
type: "test",
|
|
768
|
-
file:
|
|
1079
|
+
file: currentSuite?.file ?? collectorContext.currentSuite?.file,
|
|
769
1080
|
timeout,
|
|
770
1081
|
retry: options.retry ?? runner.config.retry,
|
|
771
1082
|
repeats: options.repeats,
|
|
772
1083
|
mode: options.only ? "only" : options.skip ? "skip" : options.todo ? "todo" : "run",
|
|
773
|
-
meta:
|
|
1084
|
+
meta: testMeta,
|
|
774
1085
|
annotations: [],
|
|
775
1086
|
artifacts: [],
|
|
776
1087
|
tags: testTags
|
|
@@ -782,21 +1093,20 @@ function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions
|
|
|
782
1093
|
if (options.concurrent || !options.sequential && runner.config.sequence.concurrent) {
|
|
783
1094
|
task.concurrent = true;
|
|
784
1095
|
}
|
|
785
|
-
task.shuffle = suiteOptions
|
|
1096
|
+
task.shuffle = suiteOptions?.shuffle;
|
|
786
1097
|
const context = createTestContext(task, runner);
|
|
787
1098
|
// create test context
|
|
788
1099
|
Object.defineProperty(task, "context", {
|
|
789
1100
|
value: context,
|
|
790
1101
|
enumerable: false
|
|
791
1102
|
});
|
|
792
|
-
setTestFixture(context, options.fixtures);
|
|
793
|
-
// custom can be called from any place, let's assume the limit is 15 stacks
|
|
1103
|
+
setTestFixture(context, options.fixtures ?? new TestFixtures());
|
|
794
1104
|
const limit = Error.stackTraceLimit;
|
|
795
|
-
Error.stackTraceLimit =
|
|
1105
|
+
Error.stackTraceLimit = 10;
|
|
796
1106
|
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
797
1107
|
Error.stackTraceLimit = limit;
|
|
798
1108
|
if (handler) {
|
|
799
|
-
setFn(task, withTimeout(withAwaitAsyncAssertions(withFixtures(
|
|
1109
|
+
setFn(task, withTimeout(withAwaitAsyncAssertions(withFixtures(handler, { context }), task), timeout, false, stackTraceError, (_, error) => abortIfTimeout([context], error)));
|
|
800
1110
|
}
|
|
801
1111
|
if (runner.config.includeTaskLocation) {
|
|
802
1112
|
const error = stackTraceError.stack;
|
|
@@ -818,11 +1128,11 @@ function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions
|
|
|
818
1128
|
options = Object.assign({}, suiteOptions, options);
|
|
819
1129
|
}
|
|
820
1130
|
// inherit concurrent / sequential from suite
|
|
821
|
-
const concurrent = this.concurrent ?? (!this.sequential &&
|
|
1131
|
+
const concurrent = this.concurrent ?? (!this.sequential && options?.concurrent);
|
|
822
1132
|
if (options.concurrent != null && concurrent != null) {
|
|
823
1133
|
options.concurrent = concurrent;
|
|
824
1134
|
}
|
|
825
|
-
const sequential = this.sequential ?? (!this.concurrent &&
|
|
1135
|
+
const sequential = this.sequential ?? (!this.concurrent && options?.sequential);
|
|
826
1136
|
if (options.sequential != null && sequential != null) {
|
|
827
1137
|
options.sequential = sequential;
|
|
828
1138
|
}
|
|
@@ -833,7 +1143,6 @@ function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions
|
|
|
833
1143
|
});
|
|
834
1144
|
test.type = "test";
|
|
835
1145
|
});
|
|
836
|
-
let collectorFixtures = parentCollectorFixtures;
|
|
837
1146
|
const collector = {
|
|
838
1147
|
type: "collector",
|
|
839
1148
|
name,
|
|
@@ -841,48 +1150,39 @@ function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions
|
|
|
841
1150
|
suite,
|
|
842
1151
|
options: suiteOptions,
|
|
843
1152
|
test,
|
|
1153
|
+
file: suite.file,
|
|
844
1154
|
tasks,
|
|
845
1155
|
collect,
|
|
846
1156
|
task,
|
|
847
1157
|
clear,
|
|
848
|
-
on: addHook
|
|
849
|
-
fixtures() {
|
|
850
|
-
return collectorFixtures;
|
|
851
|
-
},
|
|
852
|
-
scoped(fixtures) {
|
|
853
|
-
const parsed = mergeContextFixtures(fixtures, { fixtures: collectorFixtures }, runner);
|
|
854
|
-
if (parsed.fixtures) {
|
|
855
|
-
collectorFixtures = parsed.fixtures;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
1158
|
+
on: addHook
|
|
858
1159
|
};
|
|
859
1160
|
function addHook(name, ...fn) {
|
|
860
1161
|
getHooks(suite)[name].push(...fn);
|
|
861
1162
|
}
|
|
862
1163
|
function initSuite(includeLocation) {
|
|
863
|
-
var _collectorContext$cur5, _collectorContext$cur6, _collectorContext$cur7, _collectorContext$cur8;
|
|
864
1164
|
if (typeof suiteOptions === "number") {
|
|
865
1165
|
suiteOptions = { timeout: suiteOptions };
|
|
866
1166
|
}
|
|
867
|
-
const currentSuite =
|
|
868
|
-
const parentTask = currentSuite ??
|
|
869
|
-
const suiteTags = toArray(suiteOptions
|
|
1167
|
+
const currentSuite = collectorContext.currentSuite?.suite;
|
|
1168
|
+
const parentTask = currentSuite ?? collectorContext.currentSuite?.file;
|
|
1169
|
+
const suiteTags = toArray(suiteOptions?.tags);
|
|
870
1170
|
validateTags(runner.config, suiteTags);
|
|
871
1171
|
suite = {
|
|
872
1172
|
id: "",
|
|
873
1173
|
type: "suite",
|
|
874
1174
|
name,
|
|
875
|
-
fullName: createTaskName([
|
|
876
|
-
fullTestName: createTaskName([currentSuite
|
|
1175
|
+
fullName: createTaskName([currentSuite?.fullName ?? collectorContext.currentSuite?.file?.fullName, name]),
|
|
1176
|
+
fullTestName: createTaskName([currentSuite?.fullTestName, name]),
|
|
877
1177
|
suite: currentSuite,
|
|
878
1178
|
mode,
|
|
879
1179
|
each,
|
|
880
|
-
file:
|
|
881
|
-
shuffle: suiteOptions
|
|
1180
|
+
file: currentSuite?.file ?? collectorContext.currentSuite?.file,
|
|
1181
|
+
shuffle: suiteOptions?.shuffle,
|
|
882
1182
|
tasks: [],
|
|
883
|
-
meta: Object.create(null),
|
|
884
|
-
concurrent: suiteOptions
|
|
885
|
-
tags: unique([...
|
|
1183
|
+
meta: suiteOptions?.meta ?? Object.create(null),
|
|
1184
|
+
concurrent: suiteOptions?.concurrent,
|
|
1185
|
+
tags: unique([...parentTask?.tags || [], ...suiteTags])
|
|
886
1186
|
};
|
|
887
1187
|
if (runner && includeLocation && runner.config.includeTaskLocation) {
|
|
888
1188
|
const limit = Error.stackTraceLimit;
|
|
@@ -936,7 +1236,6 @@ function withAwaitAsyncAssertions(fn, task) {
|
|
|
936
1236
|
}
|
|
937
1237
|
function createSuite() {
|
|
938
1238
|
function suiteFn(name, factoryOrOptions, optionsOrFactory) {
|
|
939
|
-
var _currentSuite$options;
|
|
940
1239
|
if (getCurrentTest()) {
|
|
941
1240
|
throw new Error("Calling the suite function inside test function is not allowed. It can be only called at the top level or inside another suite function.");
|
|
942
1241
|
}
|
|
@@ -944,12 +1243,13 @@ function createSuite() {
|
|
|
944
1243
|
let { options, handler: factory } = parseArguments(factoryOrOptions, optionsOrFactory);
|
|
945
1244
|
const isConcurrentSpecified = options.concurrent || this.concurrent || options.sequential === false;
|
|
946
1245
|
const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false;
|
|
1246
|
+
const { meta: parentMeta, ...parentOptions } = currentSuite?.options || {};
|
|
947
1247
|
// inherit options from current suite
|
|
948
1248
|
options = {
|
|
949
|
-
...
|
|
1249
|
+
...parentOptions,
|
|
950
1250
|
...options
|
|
951
1251
|
};
|
|
952
|
-
const shuffle = this.shuffle ?? options.shuffle ??
|
|
1252
|
+
const shuffle = this.shuffle ?? options.shuffle ?? currentSuite?.options?.shuffle ?? runner?.config.sequence.shuffle;
|
|
953
1253
|
if (shuffle != null) {
|
|
954
1254
|
options.shuffle = shuffle;
|
|
955
1255
|
}
|
|
@@ -967,11 +1267,15 @@ function createSuite() {
|
|
|
967
1267
|
if (isSequential != null) {
|
|
968
1268
|
options.sequential = isSequential && !isConcurrent;
|
|
969
1269
|
}
|
|
970
|
-
|
|
1270
|
+
if (parentMeta) {
|
|
1271
|
+
options.meta = Object.assign(Object.create(null), parentMeta, options.meta);
|
|
1272
|
+
}
|
|
1273
|
+
return createSuiteCollector(formatName(name), factory, mode, this.each, options);
|
|
971
1274
|
}
|
|
972
1275
|
suiteFn.each = function(cases, ...args) {
|
|
973
|
-
const
|
|
974
|
-
|
|
1276
|
+
const context = getChainableContext(this);
|
|
1277
|
+
const suite = context.withContext();
|
|
1278
|
+
context.setContext("each", true);
|
|
975
1279
|
if (Array.isArray(cases) && args.length) {
|
|
976
1280
|
cases = formatTemplateString(cases, args);
|
|
977
1281
|
}
|
|
@@ -996,7 +1300,7 @@ function createSuite() {
|
|
|
996
1300
|
}
|
|
997
1301
|
}
|
|
998
1302
|
});
|
|
999
|
-
|
|
1303
|
+
context.setContext("each", undefined);
|
|
1000
1304
|
};
|
|
1001
1305
|
};
|
|
1002
1306
|
suiteFn.for = function(cases, ...args) {
|
|
@@ -1022,11 +1326,12 @@ function createSuite() {
|
|
|
1022
1326
|
"todo"
|
|
1023
1327
|
], suiteFn);
|
|
1024
1328
|
}
|
|
1025
|
-
function createTaskCollector(fn
|
|
1329
|
+
function createTaskCollector(fn) {
|
|
1026
1330
|
const taskFn = fn;
|
|
1027
1331
|
taskFn.each = function(cases, ...args) {
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1332
|
+
const context = getChainableContext(this);
|
|
1333
|
+
const test = context.withContext();
|
|
1334
|
+
context.setContext("each", true);
|
|
1030
1335
|
if (Array.isArray(cases) && args.length) {
|
|
1031
1336
|
cases = formatTemplateString(cases, args);
|
|
1032
1337
|
}
|
|
@@ -1051,11 +1356,12 @@ function createTaskCollector(fn, context) {
|
|
|
1051
1356
|
}
|
|
1052
1357
|
}
|
|
1053
1358
|
});
|
|
1054
|
-
|
|
1359
|
+
context.setContext("each", undefined);
|
|
1055
1360
|
};
|
|
1056
1361
|
};
|
|
1057
1362
|
taskFn.for = function(cases, ...args) {
|
|
1058
|
-
const
|
|
1363
|
+
const context = getChainableContext(this);
|
|
1364
|
+
const test = context.withContext();
|
|
1059
1365
|
if (Array.isArray(cases) && args.length) {
|
|
1060
1366
|
cases = formatTemplateString(cases, args);
|
|
1061
1367
|
}
|
|
@@ -1066,8 +1372,10 @@ function createTaskCollector(fn, context) {
|
|
|
1066
1372
|
// monkey-patch handler to allow parsing fixture
|
|
1067
1373
|
const handlerWrapper = handler ? (ctx) => handler(item, ctx) : undefined;
|
|
1068
1374
|
if (handlerWrapper) {
|
|
1069
|
-
handlerWrapper
|
|
1070
|
-
|
|
1375
|
+
configureProps(handlerWrapper, {
|
|
1376
|
+
index: 1,
|
|
1377
|
+
original: handler
|
|
1378
|
+
});
|
|
1071
1379
|
}
|
|
1072
1380
|
test(formatTitle(_name, toArray(item), idx), options, handlerWrapper);
|
|
1073
1381
|
});
|
|
@@ -1079,27 +1387,91 @@ function createTaskCollector(fn, context) {
|
|
|
1079
1387
|
taskFn.runIf = function(condition) {
|
|
1080
1388
|
return condition ? this : this.skip;
|
|
1081
1389
|
};
|
|
1390
|
+
/**
|
|
1391
|
+
* Parse builder pattern arguments into a fixtures object.
|
|
1392
|
+
* Handles both builder pattern (name, options?, value) and object syntax.
|
|
1393
|
+
*/
|
|
1394
|
+
function parseBuilderFixtures(fixturesOrName, optionsOrFn, maybeFn) {
|
|
1395
|
+
// Object syntax: just return as-is
|
|
1396
|
+
if (typeof fixturesOrName !== "string") {
|
|
1397
|
+
return fixturesOrName;
|
|
1398
|
+
}
|
|
1399
|
+
const fixtureName = fixturesOrName;
|
|
1400
|
+
let fixtureOptions;
|
|
1401
|
+
let fixtureValue;
|
|
1402
|
+
if (maybeFn !== undefined) {
|
|
1403
|
+
// (name, options, value) or (name, options, fn)
|
|
1404
|
+
fixtureOptions = optionsOrFn;
|
|
1405
|
+
fixtureValue = maybeFn;
|
|
1406
|
+
} else {
|
|
1407
|
+
// (name, value) or (name, fn)
|
|
1408
|
+
// Check if optionsOrFn looks like fixture options (has scope or auto)
|
|
1409
|
+
if (optionsOrFn !== null && typeof optionsOrFn === "object" && !Array.isArray(optionsOrFn) && ("scope" in optionsOrFn || "auto" in optionsOrFn)) {
|
|
1410
|
+
// (name, options) with no value - treat as empty object fixture
|
|
1411
|
+
fixtureOptions = optionsOrFn;
|
|
1412
|
+
fixtureValue = {};
|
|
1413
|
+
} else {
|
|
1414
|
+
// (name, value) or (name, fn)
|
|
1415
|
+
fixtureOptions = undefined;
|
|
1416
|
+
fixtureValue = optionsOrFn;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// Function value: wrap with onCleanup pattern
|
|
1420
|
+
if (typeof fixtureValue === "function") {
|
|
1421
|
+
const builderFn = fixtureValue;
|
|
1422
|
+
// Wrap builder pattern function (returns value) to use() pattern
|
|
1423
|
+
const fixture = async (ctx, use) => {
|
|
1424
|
+
let cleanup;
|
|
1425
|
+
const onCleanup = (fn) => {
|
|
1426
|
+
if (cleanup !== undefined) {
|
|
1427
|
+
throw new Error(`onCleanup can only be called once per fixture. ` + `Define separate fixtures if you need multiple cleanup functions.`);
|
|
1428
|
+
}
|
|
1429
|
+
cleanup = fn;
|
|
1430
|
+
};
|
|
1431
|
+
const value = await builderFn(ctx, { onCleanup });
|
|
1432
|
+
await use(value);
|
|
1433
|
+
if (cleanup) {
|
|
1434
|
+
await cleanup();
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
configureProps(fixture, { original: builderFn });
|
|
1438
|
+
if (fixtureOptions) {
|
|
1439
|
+
return { [fixtureName]: [fixture, fixtureOptions] };
|
|
1440
|
+
}
|
|
1441
|
+
return { [fixtureName]: fixture };
|
|
1442
|
+
}
|
|
1443
|
+
// Non-function value: use directly
|
|
1444
|
+
if (fixtureOptions) {
|
|
1445
|
+
return { [fixtureName]: [fixtureValue, fixtureOptions] };
|
|
1446
|
+
}
|
|
1447
|
+
return { [fixtureName]: fixtureValue };
|
|
1448
|
+
}
|
|
1449
|
+
taskFn.override = function(fixturesOrName, optionsOrFn, maybeFn) {
|
|
1450
|
+
const userFixtures = parseBuilderFixtures(fixturesOrName, optionsOrFn, maybeFn);
|
|
1451
|
+
getChainableContext(this).getFixtures().override(runner, userFixtures);
|
|
1452
|
+
return this;
|
|
1453
|
+
};
|
|
1082
1454
|
taskFn.scoped = function(fixtures) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1455
|
+
console.warn(`test.scoped() is deprecated and will be removed in future versions. Please use test.override() instead.`);
|
|
1456
|
+
return this.override(fixtures);
|
|
1085
1457
|
};
|
|
1086
|
-
taskFn.extend = function(
|
|
1087
|
-
const
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
context.fixtures = mergeScopedFixtures(context.fixtures || [], scopedFixtures);
|
|
1095
|
-
}
|
|
1096
|
-
originalWrapper.call(context, formatName(name), optionsOrFn, optionsOrTest);
|
|
1097
|
-
}, _context);
|
|
1458
|
+
taskFn.extend = function(fixturesOrName, optionsOrFn, maybeFn) {
|
|
1459
|
+
const userFixtures = parseBuilderFixtures(fixturesOrName, optionsOrFn, maybeFn);
|
|
1460
|
+
const fixtures = getChainableContext(this).getFixtures().extend(runner, userFixtures);
|
|
1461
|
+
const _test = createTest(function(name, optionsOrFn, optionsOrTest) {
|
|
1462
|
+
fn.call(this, formatName(name), optionsOrFn, optionsOrTest);
|
|
1463
|
+
});
|
|
1464
|
+
getChainableContext(_test).mergeContext({ fixtures });
|
|
1465
|
+
return _test;
|
|
1098
1466
|
};
|
|
1467
|
+
taskFn.describe = suite;
|
|
1468
|
+
taskFn.suite = suite;
|
|
1099
1469
|
taskFn.beforeEach = beforeEach;
|
|
1100
1470
|
taskFn.afterEach = afterEach;
|
|
1101
1471
|
taskFn.beforeAll = beforeAll;
|
|
1102
1472
|
taskFn.afterAll = afterAll;
|
|
1473
|
+
taskFn.aroundEach = aroundEach;
|
|
1474
|
+
taskFn.aroundAll = aroundAll;
|
|
1103
1475
|
const _test = createChainable([
|
|
1104
1476
|
"concurrent",
|
|
1105
1477
|
"sequential",
|
|
@@ -1107,14 +1479,11 @@ function createTaskCollector(fn, context) {
|
|
|
1107
1479
|
"only",
|
|
1108
1480
|
"todo",
|
|
1109
1481
|
"fails"
|
|
1110
|
-
], taskFn);
|
|
1111
|
-
if (context) {
|
|
1112
|
-
_test.mergeContext(context);
|
|
1113
|
-
}
|
|
1482
|
+
], taskFn, { fixtures: new TestFixtures() });
|
|
1114
1483
|
return _test;
|
|
1115
1484
|
}
|
|
1116
|
-
function createTest(fn
|
|
1117
|
-
return createTaskCollector(fn
|
|
1485
|
+
function createTest(fn) {
|
|
1486
|
+
return createTaskCollector(fn);
|
|
1118
1487
|
}
|
|
1119
1488
|
function formatName(name) {
|
|
1120
1489
|
return typeof name === "string" ? name : typeof name === "function" ? name.name || "<anonymous>" : String(name);
|
|
@@ -1141,14 +1510,13 @@ function formatTitle(template, items, idx) {
|
|
|
1141
1510
|
const isObjectItem = isObject(items[0]);
|
|
1142
1511
|
function formatAttribute(s) {
|
|
1143
1512
|
return s.replace(/\$([$\w.]+)/g, (_, key) => {
|
|
1144
|
-
var _runner$config;
|
|
1145
1513
|
const isArrayKey = /^\d+$/.test(key);
|
|
1146
1514
|
if (!isObjectItem && !isArrayKey) {
|
|
1147
1515
|
return `$${key}`;
|
|
1148
1516
|
}
|
|
1149
1517
|
const arrayElement = isArrayKey ? objectAttr(items, key) : undefined;
|
|
1150
1518
|
const value = isObjectItem ? objectAttr(items[0], key, arrayElement) : arrayElement;
|
|
1151
|
-
return objDisplay(value, { truncate: runner
|
|
1519
|
+
return objDisplay(value, { truncate: runner?.config?.chaiConfig?.truncateThreshold });
|
|
1152
1520
|
});
|
|
1153
1521
|
}
|
|
1154
1522
|
let output = "";
|
|
@@ -1204,8 +1572,7 @@ const collectorContext = {
|
|
|
1204
1572
|
currentSuite: null
|
|
1205
1573
|
};
|
|
1206
1574
|
function collectTask(task) {
|
|
1207
|
-
|
|
1208
|
-
(_collectorContext$cur = collectorContext.currentSuite) === null || _collectorContext$cur === void 0 ? void 0 : _collectorContext$cur.tasks.push(task);
|
|
1575
|
+
collectorContext.currentSuite?.tasks.push(task);
|
|
1209
1576
|
}
|
|
1210
1577
|
async function runWithSuite(suite, fn) {
|
|
1211
1578
|
const prev = collectorContext.currentSuite;
|
|
@@ -1225,16 +1592,15 @@ function withTimeout(fn, timeout, isHook = false, stackTraceError, onTimeout) {
|
|
|
1225
1592
|
runner._currentTaskStartTime = startTime;
|
|
1226
1593
|
runner._currentTaskTimeout = timeout;
|
|
1227
1594
|
return new Promise((resolve_, reject_) => {
|
|
1228
|
-
var _timer$unref;
|
|
1229
1595
|
const timer = setTimeout(() => {
|
|
1230
1596
|
clearTimeout(timer);
|
|
1231
1597
|
rejectTimeoutError();
|
|
1232
1598
|
}, timeout);
|
|
1233
1599
|
// `unref` might not exist in browser
|
|
1234
|
-
|
|
1600
|
+
timer.unref?.();
|
|
1235
1601
|
function rejectTimeoutError() {
|
|
1236
1602
|
const error = makeTimeoutError(isHook, timeout, stackTraceError);
|
|
1237
|
-
onTimeout
|
|
1603
|
+
onTimeout?.(args, error);
|
|
1238
1604
|
reject_(error);
|
|
1239
1605
|
}
|
|
1240
1606
|
function resolve(result) {
|
|
@@ -1282,10 +1648,9 @@ function abortIfTimeout([context], error) {
|
|
|
1282
1648
|
}
|
|
1283
1649
|
function abortContextSignal(context, error) {
|
|
1284
1650
|
const abortController = abortControllers.get(context);
|
|
1285
|
-
abortController
|
|
1651
|
+
abortController?.abort(error);
|
|
1286
1652
|
}
|
|
1287
1653
|
function createTestContext(test, runner) {
|
|
1288
|
-
var _runner$extendTaskCon;
|
|
1289
1654
|
const context = function() {
|
|
1290
1655
|
throw new Error("done() callback is deprecated, use promise instead");
|
|
1291
1656
|
};
|
|
@@ -1301,7 +1666,7 @@ function createTestContext(test, runner) {
|
|
|
1301
1666
|
// do nothing
|
|
1302
1667
|
return undefined;
|
|
1303
1668
|
}
|
|
1304
|
-
test.result
|
|
1669
|
+
test.result ??= { state: "skip" };
|
|
1305
1670
|
test.result.pending = true;
|
|
1306
1671
|
throw new PendingError("test is skipped; abort execution", test, typeof condition === "string" ? condition : note);
|
|
1307
1672
|
};
|
|
@@ -1332,34 +1697,23 @@ function createTestContext(test, runner) {
|
|
|
1332
1697
|
}));
|
|
1333
1698
|
});
|
|
1334
1699
|
context.onTestFailed = (handler, timeout) => {
|
|
1335
|
-
test.onFailed
|
|
1700
|
+
test.onFailed ||= [];
|
|
1336
1701
|
test.onFailed.push(withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error("STACK_TRACE_ERROR"), (_, error) => abortController.abort(error)));
|
|
1337
1702
|
};
|
|
1338
1703
|
context.onTestFinished = (handler, timeout) => {
|
|
1339
|
-
test.onFinished
|
|
1704
|
+
test.onFinished ||= [];
|
|
1340
1705
|
test.onFinished.push(withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error("STACK_TRACE_ERROR"), (_, error) => abortController.abort(error)));
|
|
1341
1706
|
};
|
|
1342
|
-
return
|
|
1707
|
+
return runner.extendTaskContext?.(context) || context;
|
|
1343
1708
|
}
|
|
1344
1709
|
function makeTimeoutError(isHook, timeout, stackTraceError) {
|
|
1345
1710
|
const message = `${isHook ? "Hook" : "Test"} timed out in ${timeout}ms.\nIf this is a long-running ${isHook ? "hook" : "test"}, pass a timeout value as the last argument or configure it globally with "${isHook ? "hookTimeout" : "testTimeout"}".`;
|
|
1346
1711
|
const error = new Error(message);
|
|
1347
|
-
if (stackTraceError
|
|
1712
|
+
if (stackTraceError?.stack) {
|
|
1348
1713
|
error.stack = stackTraceError.stack.replace(error.message, stackTraceError.message);
|
|
1349
1714
|
}
|
|
1350
1715
|
return error;
|
|
1351
1716
|
}
|
|
1352
|
-
const fileContexts = new WeakMap();
|
|
1353
|
-
function getFileContext(file) {
|
|
1354
|
-
const context = fileContexts.get(file);
|
|
1355
|
-
if (!context) {
|
|
1356
|
-
throw new Error(`Cannot find file context for ${file.name}`);
|
|
1357
|
-
}
|
|
1358
|
-
return context;
|
|
1359
|
-
}
|
|
1360
|
-
function setFileContext(file, context) {
|
|
1361
|
-
fileContexts.set(file, context);
|
|
1362
|
-
}
|
|
1363
1717
|
|
|
1364
1718
|
async function runSetupFiles(config, files, runner) {
|
|
1365
1719
|
if (config.sequence.setupFiles === "parallel") {
|
|
@@ -1388,13 +1742,11 @@ async function collectTests(specs, runner) {
|
|
|
1388
1742
|
const testTagsFilter = typeof spec === "object" && spec.testTagsFilter ? createTagsFilter(spec.testTagsFilter, config.tags) : undefined;
|
|
1389
1743
|
const fileTags = typeof spec === "string" ? [] : spec.fileTags || [];
|
|
1390
1744
|
const file = createFileTask(filepath, config.root, config.name, runner.pool, runner.viteEnvironment);
|
|
1391
|
-
setFileContext(file, Object.create(null));
|
|
1392
1745
|
file.tags = fileTags;
|
|
1393
1746
|
file.shuffle = config.sequence.shuffle;
|
|
1394
1747
|
try {
|
|
1395
|
-
var _runner$onCollectStar, _runner$getImportDura;
|
|
1396
1748
|
validateTags(runner.config, fileTags);
|
|
1397
|
-
|
|
1749
|
+
runner.onCollectStart?.(file);
|
|
1398
1750
|
clearCollectorContext(file, runner);
|
|
1399
1751
|
const setupFiles = toArray(config.setupFiles);
|
|
1400
1752
|
if (setupFiles.length) {
|
|
@@ -1407,7 +1759,7 @@ async function collectTests(specs, runner) {
|
|
|
1407
1759
|
}
|
|
1408
1760
|
const collectStart = now$1();
|
|
1409
1761
|
await runner.importFile(filepath, "collect");
|
|
1410
|
-
const durations =
|
|
1762
|
+
const durations = runner.getImportDurations?.();
|
|
1411
1763
|
if (durations) {
|
|
1412
1764
|
file.importDurations = durations;
|
|
1413
1765
|
}
|
|
@@ -1431,13 +1783,12 @@ async function collectTests(specs, runner) {
|
|
|
1431
1783
|
setHooks(file, fileHooks);
|
|
1432
1784
|
file.collectDuration = now$1() - collectStart;
|
|
1433
1785
|
} catch (e) {
|
|
1434
|
-
|
|
1435
|
-
const error = processError(e);
|
|
1786
|
+
const errors = e instanceof AggregateError ? e.errors.map((e) => processError(e, runner.config.diffOptions)) : [processError(e, runner.config.diffOptions)];
|
|
1436
1787
|
file.result = {
|
|
1437
1788
|
state: "fail",
|
|
1438
|
-
errors
|
|
1789
|
+
errors
|
|
1439
1790
|
};
|
|
1440
|
-
const durations =
|
|
1791
|
+
const durations = runner.getImportDurations?.();
|
|
1441
1792
|
if (durations) {
|
|
1442
1793
|
file.importDurations = durations;
|
|
1443
1794
|
}
|
|
@@ -1586,12 +1937,179 @@ async function callSuiteHook(suite, currentTask, name, runner, args) {
|
|
|
1586
1937
|
}
|
|
1587
1938
|
return callbacks;
|
|
1588
1939
|
}
|
|
1940
|
+
function getAroundEachHooks(suite) {
|
|
1941
|
+
const hooks = [];
|
|
1942
|
+
const parentSuite = "filepath" in suite ? null : suite.suite || suite.file;
|
|
1943
|
+
if (parentSuite) {
|
|
1944
|
+
hooks.push(...getAroundEachHooks(parentSuite));
|
|
1945
|
+
}
|
|
1946
|
+
hooks.push(...getHooks(suite).aroundEach);
|
|
1947
|
+
return hooks;
|
|
1948
|
+
}
|
|
1949
|
+
function getAroundAllHooks(suite) {
|
|
1950
|
+
return getHooks(suite).aroundAll;
|
|
1951
|
+
}
|
|
1952
|
+
function makeAroundHookTimeoutError(hookName, phase, timeout, stackTraceError) {
|
|
1953
|
+
const message = `The ${phase} phase of "${hookName}" hook timed out after ${timeout}ms.`;
|
|
1954
|
+
const ErrorClass = phase === "setup" ? AroundHookSetupError : AroundHookTeardownError;
|
|
1955
|
+
const error = new ErrorClass(message);
|
|
1956
|
+
if (stackTraceError?.stack) {
|
|
1957
|
+
error.stack = stackTraceError.stack.replace(stackTraceError.message, error.message);
|
|
1958
|
+
}
|
|
1959
|
+
return error;
|
|
1960
|
+
}
|
|
1961
|
+
async function callAroundHooks(runInner, options) {
|
|
1962
|
+
const { hooks, hookName, callbackName, onTimeout, invokeHook } = options;
|
|
1963
|
+
if (!hooks.length) {
|
|
1964
|
+
await runInner();
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
const createTimeoutPromise = (timeout, phase, stackTraceError) => {
|
|
1968
|
+
let timer;
|
|
1969
|
+
let timedout = false;
|
|
1970
|
+
const promise = new Promise((_, reject) => {
|
|
1971
|
+
if (timeout > 0 && timeout !== Number.POSITIVE_INFINITY) {
|
|
1972
|
+
timer = setTimeout(() => {
|
|
1973
|
+
timedout = true;
|
|
1974
|
+
const error = makeAroundHookTimeoutError(hookName, phase, timeout, stackTraceError);
|
|
1975
|
+
onTimeout?.(error);
|
|
1976
|
+
reject(error);
|
|
1977
|
+
}, timeout);
|
|
1978
|
+
timer.unref?.();
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
const clear = () => {
|
|
1982
|
+
if (timer) {
|
|
1983
|
+
clearTimeout(timer);
|
|
1984
|
+
timer = undefined;
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
return {
|
|
1988
|
+
promise,
|
|
1989
|
+
clear,
|
|
1990
|
+
isTimedOut: () => timedout
|
|
1991
|
+
};
|
|
1992
|
+
};
|
|
1993
|
+
const runNextHook = async (index) => {
|
|
1994
|
+
if (index >= hooks.length) {
|
|
1995
|
+
return runInner();
|
|
1996
|
+
}
|
|
1997
|
+
const hook = hooks[index];
|
|
1998
|
+
const timeout = getAroundHookTimeout(hook);
|
|
1999
|
+
const stackTraceError = getAroundHookStackTrace(hook);
|
|
2000
|
+
let useCalled = false;
|
|
2001
|
+
let setupTimeout;
|
|
2002
|
+
let teardownTimeout;
|
|
2003
|
+
// Promise that resolves when use() is called (setup phase complete)
|
|
2004
|
+
let resolveUseCalled;
|
|
2005
|
+
const useCalledPromise = new Promise((resolve) => {
|
|
2006
|
+
resolveUseCalled = resolve;
|
|
2007
|
+
});
|
|
2008
|
+
// Promise that resolves when use() returns (inner hooks complete, teardown phase starts)
|
|
2009
|
+
let resolveUseReturned;
|
|
2010
|
+
const useReturnedPromise = new Promise((resolve) => {
|
|
2011
|
+
resolveUseReturned = resolve;
|
|
2012
|
+
});
|
|
2013
|
+
// Promise that resolves when hook completes
|
|
2014
|
+
let resolveHookComplete;
|
|
2015
|
+
let rejectHookComplete;
|
|
2016
|
+
const hookCompletePromise = new Promise((resolve, reject) => {
|
|
2017
|
+
resolveHookComplete = resolve;
|
|
2018
|
+
rejectHookComplete = reject;
|
|
2019
|
+
});
|
|
2020
|
+
const use = async () => {
|
|
2021
|
+
// shouldn't continue to next (runTest/Suite or inner aroundEach/All) when aroundEach/All setup timed out.
|
|
2022
|
+
if (setupTimeout.isTimedOut()) {
|
|
2023
|
+
// we can throw any error to bail out.
|
|
2024
|
+
// this error is not seen by end users since `runNextHook` already rejected with timeout error
|
|
2025
|
+
// and this error is caught by `rejectHookComplete`.
|
|
2026
|
+
throw new Error("__VITEST_INTERNAL_AROUND_HOOK_ABORT__");
|
|
2027
|
+
}
|
|
2028
|
+
if (useCalled) {
|
|
2029
|
+
throw new AroundHookMultipleCallsError(`The \`${callbackName}\` callback was called multiple times in the \`${hookName}\` hook. ` + `The callback can only be called once per hook.`);
|
|
2030
|
+
}
|
|
2031
|
+
useCalled = true;
|
|
2032
|
+
resolveUseCalled();
|
|
2033
|
+
// Setup phase completed - clear setup timer
|
|
2034
|
+
setupTimeout.clear();
|
|
2035
|
+
// Run inner hooks - don't time this against our teardown timeout
|
|
2036
|
+
let nextError;
|
|
2037
|
+
try {
|
|
2038
|
+
await runNextHook(index + 1);
|
|
2039
|
+
} catch (value) {
|
|
2040
|
+
nextError = { value };
|
|
2041
|
+
}
|
|
2042
|
+
// Start teardown timer after inner hooks complete - only times this hook's teardown code
|
|
2043
|
+
teardownTimeout = createTimeoutPromise(timeout, "teardown", stackTraceError);
|
|
2044
|
+
// Signal that use() is returning (teardown phase starting)
|
|
2045
|
+
resolveUseReturned();
|
|
2046
|
+
if (nextError) {
|
|
2047
|
+
throw nextError.value;
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
// Start setup timeout
|
|
2051
|
+
setupTimeout = createTimeoutPromise(timeout, "setup", stackTraceError);
|
|
2052
|
+
(async () => {
|
|
2053
|
+
try {
|
|
2054
|
+
await invokeHook(hook, use);
|
|
2055
|
+
if (!useCalled) {
|
|
2056
|
+
throw new AroundHookSetupError(`The \`${callbackName}\` callback was not called in the \`${hookName}\` hook. ` + `Make sure to call \`${callbackName}\` to run the ${hookName === "aroundEach" ? "test" : "suite"}.`);
|
|
2057
|
+
}
|
|
2058
|
+
resolveHookComplete();
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
rejectHookComplete(error);
|
|
2061
|
+
}
|
|
2062
|
+
})();
|
|
2063
|
+
// Wait for either: use() to be called OR hook to complete (error) OR setup timeout
|
|
2064
|
+
try {
|
|
2065
|
+
await Promise.race([
|
|
2066
|
+
useCalledPromise,
|
|
2067
|
+
hookCompletePromise,
|
|
2068
|
+
setupTimeout.promise
|
|
2069
|
+
]);
|
|
2070
|
+
} finally {
|
|
2071
|
+
setupTimeout.clear();
|
|
2072
|
+
}
|
|
2073
|
+
// Wait for use() to return (inner hooks complete) OR hook to complete (error during inner hooks)
|
|
2074
|
+
await Promise.race([useReturnedPromise, hookCompletePromise]);
|
|
2075
|
+
// Now teardownTimeout is guaranteed to be set
|
|
2076
|
+
// Wait for hook to complete (teardown) OR teardown timeout
|
|
2077
|
+
try {
|
|
2078
|
+
await Promise.race([hookCompletePromise, teardownTimeout?.promise]);
|
|
2079
|
+
} finally {
|
|
2080
|
+
teardownTimeout?.clear();
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
await runNextHook(0);
|
|
2084
|
+
}
|
|
2085
|
+
async function callAroundAllHooks(suite, runSuiteInner) {
|
|
2086
|
+
await callAroundHooks(runSuiteInner, {
|
|
2087
|
+
hooks: getAroundAllHooks(suite),
|
|
2088
|
+
hookName: "aroundAll",
|
|
2089
|
+
callbackName: "runSuite()",
|
|
2090
|
+
invokeHook: (hook, use) => hook(use, suite)
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
async function callAroundEachHooks(suite, test, runTest) {
|
|
2094
|
+
await callAroundHooks(
|
|
2095
|
+
// Take checkpoint right before runTest - at this point all aroundEach fixtures
|
|
2096
|
+
// have been resolved, so we can correctly identify which fixtures belong to
|
|
2097
|
+
// aroundEach (before checkpoint) vs inside runTest (after checkpoint)
|
|
2098
|
+
() => runTest(getFixtureCleanupCount(test.context)),
|
|
2099
|
+
{
|
|
2100
|
+
hooks: getAroundEachHooks(suite),
|
|
2101
|
+
hookName: "aroundEach",
|
|
2102
|
+
callbackName: "runTest()",
|
|
2103
|
+
onTimeout: (error) => abortContextSignal(test.context, error),
|
|
2104
|
+
invokeHook: (hook, use) => hook(use, test.context, suite)
|
|
2105
|
+
}
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
1589
2108
|
const packs = new Map();
|
|
1590
2109
|
const eventsPacks = [];
|
|
1591
2110
|
const pendingTasksUpdates = [];
|
|
1592
2111
|
function sendTasksUpdate(runner) {
|
|
1593
2112
|
if (packs.size) {
|
|
1594
|
-
var _runner$onTaskUpdate;
|
|
1595
2113
|
const taskPacks = Array.from(packs).map(([id, task]) => {
|
|
1596
2114
|
return [
|
|
1597
2115
|
id,
|
|
@@ -1599,7 +2117,7 @@ function sendTasksUpdate(runner) {
|
|
|
1599
2117
|
task[1]
|
|
1600
2118
|
];
|
|
1601
2119
|
});
|
|
1602
|
-
const p =
|
|
2120
|
+
const p = runner.onTaskUpdate?.(taskPacks, eventsPacks);
|
|
1603
2121
|
if (p) {
|
|
1604
2122
|
pendingTasksUpdates.push(p);
|
|
1605
2123
|
// remove successful promise to not grow array indefnitely,
|
|
@@ -1626,7 +2144,7 @@ function throttle(fn, ms) {
|
|
|
1626
2144
|
return fn.apply(this, args);
|
|
1627
2145
|
}
|
|
1628
2146
|
// Make sure fn is still called even if there are no further calls
|
|
1629
|
-
pendingCall
|
|
2147
|
+
pendingCall ??= setTimeout(() => call.bind(this)(...args), ms);
|
|
1630
2148
|
};
|
|
1631
2149
|
}
|
|
1632
2150
|
// throttle based on summary reporter's DURATION_UPDATE_INTERVAL_MS
|
|
@@ -1681,14 +2199,13 @@ function passesRetryCondition(test, errors) {
|
|
|
1681
2199
|
return false;
|
|
1682
2200
|
}
|
|
1683
2201
|
async function runTest(test, runner) {
|
|
1684
|
-
|
|
1685
|
-
await ((_runner$onBeforeRunTa = runner.onBeforeRunTask) === null || _runner$onBeforeRunTa === void 0 ? void 0 : _runner$onBeforeRunTa.call(runner, test));
|
|
2202
|
+
await runner.onBeforeRunTask?.(test);
|
|
1686
2203
|
if (test.mode !== "run" && test.mode !== "queued") {
|
|
1687
2204
|
updateTask("test-prepare", test, runner);
|
|
1688
2205
|
updateTask("test-finished", test, runner);
|
|
1689
2206
|
return;
|
|
1690
2207
|
}
|
|
1691
|
-
if (
|
|
2208
|
+
if (test.result?.state === "fail") {
|
|
1692
2209
|
// should not be possible to get here, I think this is just copy pasted from suite
|
|
1693
2210
|
// TODO: maybe someone fails tests in `beforeAll` hooks?
|
|
1694
2211
|
// https://github.com/vitest-dev/vitest/pull/7069
|
|
@@ -1710,73 +2227,86 @@ async function runTest(test, runner) {
|
|
|
1710
2227
|
for (let repeatCount = 0; repeatCount <= repeats; repeatCount++) {
|
|
1711
2228
|
const retry = getRetryCount(test.retry);
|
|
1712
2229
|
for (let retryCount = 0; retryCount <= retry; retryCount++) {
|
|
1713
|
-
var _test$onFinished, _test$onFailed, _runner$onAfterRetryT, _test$result2, _test$result3;
|
|
1714
2230
|
let beforeEachCleanups = [];
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
if (
|
|
1728
|
-
|
|
2231
|
+
// fixtureCheckpoint is passed by callAroundEachHooks - it represents the count
|
|
2232
|
+
// of fixture cleanup functions AFTER all aroundEach fixtures have been resolved
|
|
2233
|
+
// but BEFORE the test runs. This allows us to clean up only fixtures created
|
|
2234
|
+
// inside runTest while preserving aroundEach fixtures for teardown.
|
|
2235
|
+
await callAroundEachHooks(suite, test, async (fixtureCheckpoint) => {
|
|
2236
|
+
try {
|
|
2237
|
+
await runner.onBeforeTryTask?.(test, {
|
|
2238
|
+
retry: retryCount,
|
|
2239
|
+
repeats: repeatCount
|
|
2240
|
+
});
|
|
2241
|
+
test.result.repeatCount = repeatCount;
|
|
2242
|
+
beforeEachCleanups = await $("test.beforeEach", () => callSuiteHook(suite, test, "beforeEach", runner, [test.context, suite]));
|
|
2243
|
+
if (runner.runTask) {
|
|
2244
|
+
await $("test.callback", () => runner.runTask(test));
|
|
2245
|
+
} else {
|
|
2246
|
+
const fn = getFn(test);
|
|
2247
|
+
if (!fn) {
|
|
2248
|
+
throw new Error("Test function is not found. Did you add it using `setFn`?");
|
|
2249
|
+
}
|
|
2250
|
+
await $("test.callback", () => fn());
|
|
2251
|
+
}
|
|
2252
|
+
await runner.onAfterTryTask?.(test, {
|
|
2253
|
+
retry: retryCount,
|
|
2254
|
+
repeats: repeatCount
|
|
2255
|
+
});
|
|
2256
|
+
if (test.result.state !== "fail") {
|
|
2257
|
+
if (!test.repeats) {
|
|
2258
|
+
test.result.state = "pass";
|
|
2259
|
+
} else if (test.repeats && retry === retryCount) {
|
|
2260
|
+
test.result.state = "pass";
|
|
2261
|
+
}
|
|
1729
2262
|
}
|
|
1730
|
-
|
|
2263
|
+
} catch (e) {
|
|
2264
|
+
failTask(test.result, e, runner.config.diffOptions);
|
|
1731
2265
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
test.
|
|
2266
|
+
try {
|
|
2267
|
+
await runner.onTaskFinished?.(test);
|
|
2268
|
+
} catch (e) {
|
|
2269
|
+
failTask(test.result, e, runner.config.diffOptions);
|
|
2270
|
+
}
|
|
2271
|
+
try {
|
|
2272
|
+
await $("test.afterEach", () => callSuiteHook(suite, test, "afterEach", runner, [test.context, suite]));
|
|
2273
|
+
if (beforeEachCleanups.length) {
|
|
2274
|
+
await $("test.cleanup", () => callCleanupHooks(runner, beforeEachCleanups));
|
|
1741
2275
|
}
|
|
2276
|
+
// Only clean up fixtures created inside runTest (after the checkpoint)
|
|
2277
|
+
// Fixtures created for aroundEach will be cleaned up after aroundEach teardown
|
|
2278
|
+
await callFixtureCleanupFrom(test.context, fixtureCheckpoint);
|
|
2279
|
+
} catch (e) {
|
|
2280
|
+
failTask(test.result, e, runner.config.diffOptions);
|
|
1742
2281
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
}
|
|
1746
|
-
try {
|
|
1747
|
-
var _runner$onTaskFinishe;
|
|
1748
|
-
await ((_runner$onTaskFinishe = runner.onTaskFinished) === null || _runner$onTaskFinishe === void 0 ? void 0 : _runner$onTaskFinishe.call(runner, test));
|
|
1749
|
-
} catch (e) {
|
|
1750
|
-
failTask(test.result, e, runner.config.diffOptions);
|
|
1751
|
-
}
|
|
1752
|
-
try {
|
|
1753
|
-
await $("test.afterEach", () => callSuiteHook(suite, test, "afterEach", runner, [test.context, suite]));
|
|
1754
|
-
if (beforeEachCleanups.length) {
|
|
1755
|
-
await $("test.cleanup", () => callCleanupHooks(runner, beforeEachCleanups));
|
|
2282
|
+
if (test.onFinished?.length) {
|
|
2283
|
+
await $("test.onFinished", () => callTestHooks(runner, test, test.onFinished, "stack"));
|
|
1756
2284
|
}
|
|
2285
|
+
if (test.result.state === "fail" && test.onFailed?.length) {
|
|
2286
|
+
await $("test.onFailed", () => callTestHooks(runner, test, test.onFailed, runner.config.sequence.hooks));
|
|
2287
|
+
}
|
|
2288
|
+
test.onFailed = undefined;
|
|
2289
|
+
test.onFinished = undefined;
|
|
2290
|
+
await runner.onAfterRetryTask?.(test, {
|
|
2291
|
+
retry: retryCount,
|
|
2292
|
+
repeats: repeatCount
|
|
2293
|
+
});
|
|
2294
|
+
}).catch((error) => {
|
|
2295
|
+
failTask(test.result, error, runner.config.diffOptions);
|
|
2296
|
+
});
|
|
2297
|
+
// Clean up fixtures that were created for aroundEach (before the checkpoint)
|
|
2298
|
+
// This runs after aroundEach teardown has completed
|
|
2299
|
+
try {
|
|
1757
2300
|
await callFixtureCleanup(test.context);
|
|
1758
2301
|
} catch (e) {
|
|
1759
2302
|
failTask(test.result, e, runner.config.diffOptions);
|
|
1760
2303
|
}
|
|
1761
|
-
if ((_test$onFinished = test.onFinished) === null || _test$onFinished === void 0 ? void 0 : _test$onFinished.length) {
|
|
1762
|
-
await $("test.onFinished", () => callTestHooks(runner, test, test.onFinished, "stack"));
|
|
1763
|
-
}
|
|
1764
|
-
if (test.result.state === "fail" && ((_test$onFailed = test.onFailed) === null || _test$onFailed === void 0 ? void 0 : _test$onFailed.length)) {
|
|
1765
|
-
await $("test.onFailed", () => callTestHooks(runner, test, test.onFailed, runner.config.sequence.hooks));
|
|
1766
|
-
}
|
|
1767
|
-
test.onFailed = undefined;
|
|
1768
|
-
test.onFinished = undefined;
|
|
1769
|
-
await ((_runner$onAfterRetryT = runner.onAfterRetryTask) === null || _runner$onAfterRetryT === void 0 ? void 0 : _runner$onAfterRetryT.call(runner, test, {
|
|
1770
|
-
retry: retryCount,
|
|
1771
|
-
repeats: repeatCount
|
|
1772
|
-
}));
|
|
1773
2304
|
// skipped with new PendingError
|
|
1774
|
-
if (
|
|
1775
|
-
var _test$result4;
|
|
2305
|
+
if (test.result?.pending || test.result?.state === "skip") {
|
|
1776
2306
|
test.mode = "skip";
|
|
1777
2307
|
test.result = {
|
|
1778
2308
|
state: "skip",
|
|
1779
|
-
note:
|
|
2309
|
+
note: test.result?.note,
|
|
1780
2310
|
pending: true,
|
|
1781
2311
|
duration: now() - start
|
|
1782
2312
|
};
|
|
@@ -1818,7 +2348,7 @@ async function runTest(test, runner) {
|
|
|
1818
2348
|
cleanupRunningTest();
|
|
1819
2349
|
setCurrentTest(undefined);
|
|
1820
2350
|
test.result.duration = now() - start;
|
|
1821
|
-
await
|
|
2351
|
+
await runner.onAfterRunTask?.(test);
|
|
1822
2352
|
updateTask("test-finished", test, runner);
|
|
1823
2353
|
}
|
|
1824
2354
|
function failTask(result, err, diffOptions) {
|
|
@@ -1831,9 +2361,9 @@ function failTask(result, err, diffOptions) {
|
|
|
1831
2361
|
result.state = "fail";
|
|
1832
2362
|
const errors = Array.isArray(err) ? err : [err];
|
|
1833
2363
|
for (const e of errors) {
|
|
1834
|
-
const
|
|
1835
|
-
result.errors
|
|
1836
|
-
result.errors.push(
|
|
2364
|
+
const errors = e instanceof AggregateError ? e.errors.map((e) => processError(e, diffOptions)) : [processError(e, diffOptions)];
|
|
2365
|
+
result.errors ??= [];
|
|
2366
|
+
result.errors.push(...errors);
|
|
1837
2367
|
}
|
|
1838
2368
|
}
|
|
1839
2369
|
function markTasksAsSkipped(suite, runner) {
|
|
@@ -1850,9 +2380,8 @@ function markTasksAsSkipped(suite, runner) {
|
|
|
1850
2380
|
});
|
|
1851
2381
|
}
|
|
1852
2382
|
async function runSuite(suite, runner) {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
if (((_suite$result = suite.result) === null || _suite$result === void 0 ? void 0 : _suite$result.state) === "fail") {
|
|
2383
|
+
await runner.onBeforeRunSuite?.(suite);
|
|
2384
|
+
if (suite.result?.state === "fail") {
|
|
1856
2385
|
markTasksAsSkipped(suite, runner);
|
|
1857
2386
|
// failed during collection
|
|
1858
2387
|
updateTask("suite-failed-early", suite, runner);
|
|
@@ -1874,55 +2403,68 @@ async function runSuite(suite, runner) {
|
|
|
1874
2403
|
suite.result.state = "todo";
|
|
1875
2404
|
updateTask("suite-finished", suite, runner);
|
|
1876
2405
|
} else {
|
|
1877
|
-
|
|
2406
|
+
let suiteRan = false;
|
|
1878
2407
|
try {
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2408
|
+
await callAroundAllHooks(suite, async () => {
|
|
2409
|
+
suiteRan = true;
|
|
2410
|
+
try {
|
|
2411
|
+
// beforeAll
|
|
2412
|
+
try {
|
|
2413
|
+
beforeAllCleanups = await $("suite.beforeAll", () => callSuiteHook(suite, suite, "beforeAll", runner, [suite]));
|
|
2414
|
+
} catch (e) {
|
|
2415
|
+
failTask(suite.result, e, runner.config.diffOptions);
|
|
2416
|
+
markTasksAsSkipped(suite, runner);
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
// run suite children
|
|
2420
|
+
if (runner.runSuite) {
|
|
2421
|
+
await runner.runSuite(suite);
|
|
1891
2422
|
} else {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2423
|
+
for (let tasksGroup of partitionSuiteChildren(suite)) {
|
|
2424
|
+
if (tasksGroup[0].concurrent === true) {
|
|
2425
|
+
await Promise.all(tasksGroup.map((c) => runSuiteChild(c, runner)));
|
|
2426
|
+
} else {
|
|
2427
|
+
const { sequence } = runner.config;
|
|
2428
|
+
if (suite.shuffle) {
|
|
2429
|
+
// run describe block independently from tests
|
|
2430
|
+
const suites = tasksGroup.filter((group) => group.type === "suite");
|
|
2431
|
+
const tests = tasksGroup.filter((group) => group.type === "test");
|
|
2432
|
+
const groups = shuffle([suites, tests], sequence.seed);
|
|
2433
|
+
tasksGroup = groups.flatMap((group) => shuffle(group, sequence.seed));
|
|
2434
|
+
}
|
|
2435
|
+
for (const c of tasksGroup) {
|
|
2436
|
+
await runSuiteChild(c, runner);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
} finally {
|
|
2442
|
+
// afterAll runs even if beforeAll or suite children fail
|
|
2443
|
+
try {
|
|
2444
|
+
await $("suite.afterAll", () => callSuiteHook(suite, suite, "afterAll", runner, [suite]));
|
|
2445
|
+
if (beforeAllCleanups.length) {
|
|
2446
|
+
await $("suite.cleanup", () => callCleanupHooks(runner, beforeAllCleanups));
|
|
1899
2447
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
2448
|
+
if (suite.file === suite) {
|
|
2449
|
+
const contexts = TestFixtures.getFileContexts(suite.file);
|
|
2450
|
+
await Promise.all(contexts.map((context) => callFixtureCleanup(context)));
|
|
1902
2451
|
}
|
|
2452
|
+
} catch (e) {
|
|
2453
|
+
failTask(suite.result, e, runner.config.diffOptions);
|
|
1903
2454
|
}
|
|
1904
2455
|
}
|
|
1905
|
-
}
|
|
2456
|
+
});
|
|
1906
2457
|
} catch (e) {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
await $("suite.afterAll", () => callSuiteHook(suite, suite, "afterAll", runner, [suite]));
|
|
1911
|
-
if (beforeAllCleanups.length) {
|
|
1912
|
-
await $("suite.cleanup", () => callCleanupHooks(runner, beforeAllCleanups));
|
|
1913
|
-
}
|
|
1914
|
-
if (suite.file === suite) {
|
|
1915
|
-
const context = getFileContext(suite);
|
|
1916
|
-
await callFixtureCleanup(context);
|
|
2458
|
+
// mark tasks as skipped if aroundAll failed before the suite callback was executed
|
|
2459
|
+
if (!suiteRan) {
|
|
2460
|
+
markTasksAsSkipped(suite, runner);
|
|
1917
2461
|
}
|
|
1918
|
-
} catch (e) {
|
|
1919
2462
|
failTask(suite.result, e, runner.config.diffOptions);
|
|
1920
2463
|
}
|
|
1921
2464
|
if (suite.mode === "run" || suite.mode === "queued") {
|
|
1922
2465
|
if (!runner.config.passWithNoTests && !hasTests(suite)) {
|
|
1923
|
-
var _suite$result$errors;
|
|
1924
2466
|
suite.result.state = "fail";
|
|
1925
|
-
if (!
|
|
2467
|
+
if (!suite.result.errors?.length) {
|
|
1926
2468
|
const error = processError(new Error(`No test found in suite ${suite.name}`));
|
|
1927
2469
|
suite.result.errors = [error];
|
|
1928
2470
|
}
|
|
@@ -1933,7 +2475,7 @@ async function runSuite(suite, runner) {
|
|
|
1933
2475
|
}
|
|
1934
2476
|
}
|
|
1935
2477
|
suite.result.duration = now() - start;
|
|
1936
|
-
await
|
|
2478
|
+
await runner.onAfterRunSuite?.(suite);
|
|
1937
2479
|
updateTask("suite-finished", suite, runner);
|
|
1938
2480
|
}
|
|
1939
2481
|
}
|
|
@@ -1941,36 +2483,31 @@ let limitMaxConcurrency;
|
|
|
1941
2483
|
async function runSuiteChild(c, runner) {
|
|
1942
2484
|
const $ = runner.trace;
|
|
1943
2485
|
if (c.type === "test") {
|
|
1944
|
-
return limitMaxConcurrency(() => {
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
"code.column.number": (_c$location2 = c.location) === null || _c$location2 === void 0 ? void 0 : _c$location2.column
|
|
1954
|
-
}, () => runTest(c, runner));
|
|
1955
|
-
});
|
|
2486
|
+
return limitMaxConcurrency(() => $("run.test", {
|
|
2487
|
+
"vitest.test.id": c.id,
|
|
2488
|
+
"vitest.test.name": c.name,
|
|
2489
|
+
"vitest.test.mode": c.mode,
|
|
2490
|
+
"vitest.test.timeout": c.timeout,
|
|
2491
|
+
"code.file.path": c.file.filepath,
|
|
2492
|
+
"code.line.number": c.location?.line,
|
|
2493
|
+
"code.column.number": c.location?.column
|
|
2494
|
+
}, () => runTest(c, runner)));
|
|
1956
2495
|
} else if (c.type === "suite") {
|
|
1957
|
-
var _c$location3, _c$location4;
|
|
1958
2496
|
return $("run.suite", {
|
|
1959
2497
|
"vitest.suite.id": c.id,
|
|
1960
2498
|
"vitest.suite.name": c.name,
|
|
1961
2499
|
"vitest.suite.mode": c.mode,
|
|
1962
2500
|
"code.file.path": c.file.filepath,
|
|
1963
|
-
"code.line.number":
|
|
1964
|
-
"code.column.number":
|
|
2501
|
+
"code.line.number": c.location?.line,
|
|
2502
|
+
"code.column.number": c.location?.column
|
|
1965
2503
|
}, () => runSuite(c, runner));
|
|
1966
2504
|
}
|
|
1967
2505
|
}
|
|
1968
2506
|
async function runFiles(files, runner) {
|
|
1969
|
-
limitMaxConcurrency
|
|
2507
|
+
limitMaxConcurrency ??= limitConcurrency(runner.config.maxConcurrency);
|
|
1970
2508
|
for (const file of files) {
|
|
1971
2509
|
if (!file.tasks.length && !runner.config.passWithNoTests) {
|
|
1972
|
-
|
|
1973
|
-
if (!((_file$result = file.result) === null || _file$result === void 0 || (_file$result = _file$result.errors) === null || _file$result === void 0 ? void 0 : _file$result.length)) {
|
|
2510
|
+
if (!file.result?.errors?.length) {
|
|
1974
2511
|
const error = processError(new Error(`No test suite found in file ${file.filepath}`));
|
|
1975
2512
|
file.result = {
|
|
1976
2513
|
state: "fail",
|
|
@@ -1992,37 +2529,32 @@ function defaultTrace(_, attributes, cb) {
|
|
|
1992
2529
|
return cb();
|
|
1993
2530
|
}
|
|
1994
2531
|
async function startTests(specs, runner) {
|
|
1995
|
-
|
|
1996
|
-
runner.
|
|
1997
|
-
const cancel = (_runner$cancel = runner.cancel) === null || _runner$cancel === void 0 ? void 0 : _runner$cancel.bind(runner);
|
|
2532
|
+
runner.trace ??= defaultTrace;
|
|
2533
|
+
const cancel = runner.cancel?.bind(runner);
|
|
1998
2534
|
// Ideally, we need to have an event listener for this, but only have a runner here.
|
|
1999
2535
|
// Adding another onCancel felt wrong (maybe it needs to be refactored)
|
|
2000
2536
|
runner.cancel = (reason) => {
|
|
2001
2537
|
// We intentionally create only one error since there is only one test run that can be cancelled
|
|
2002
2538
|
const error = new TestRunAbortError("The test run was aborted by the user.", reason);
|
|
2003
2539
|
getRunningTests().forEach((test) => abortContextSignal(test.context, error));
|
|
2004
|
-
return cancel
|
|
2540
|
+
return cancel?.(reason);
|
|
2005
2541
|
};
|
|
2006
2542
|
if (!workerRunners.has(runner)) {
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
if (context) {
|
|
2012
|
-
await callFixtureCleanup(context);
|
|
2013
|
-
}
|
|
2543
|
+
runner.onCleanupWorkerContext?.(async () => {
|
|
2544
|
+
await Promise.all([...TestFixtures.getWorkerContexts()].map((context) => callFixtureCleanup(context))).finally(() => {
|
|
2545
|
+
TestFixtures.clearDefinitions();
|
|
2546
|
+
});
|
|
2014
2547
|
});
|
|
2015
2548
|
workerRunners.add(runner);
|
|
2016
2549
|
}
|
|
2017
2550
|
try {
|
|
2018
|
-
var _runner$onBeforeColle, _runner$onCollected, _runner$onBeforeRunFi, _runner$onAfterRunFil;
|
|
2019
2551
|
const paths = specs.map((f) => typeof f === "string" ? f : f.filepath);
|
|
2020
|
-
await
|
|
2552
|
+
await runner.onBeforeCollect?.(paths);
|
|
2021
2553
|
const files = await collectTests(specs, runner);
|
|
2022
|
-
await
|
|
2023
|
-
await
|
|
2554
|
+
await runner.onCollected?.(files);
|
|
2555
|
+
await runner.onBeforeRunFiles?.(files);
|
|
2024
2556
|
await runFiles(files, runner);
|
|
2025
|
-
await
|
|
2557
|
+
await runner.onAfterRunFiles?.(files);
|
|
2026
2558
|
await finishSendTasksUpdate(runner);
|
|
2027
2559
|
return files;
|
|
2028
2560
|
} finally {
|
|
@@ -2030,12 +2562,11 @@ async function startTests(specs, runner) {
|
|
|
2030
2562
|
}
|
|
2031
2563
|
}
|
|
2032
2564
|
async function publicCollect(specs, runner) {
|
|
2033
|
-
|
|
2034
|
-
runner.trace ?? (runner.trace = defaultTrace);
|
|
2565
|
+
runner.trace ??= defaultTrace;
|
|
2035
2566
|
const paths = specs.map((f) => typeof f === "string" ? f : f.filepath);
|
|
2036
|
-
await
|
|
2567
|
+
await runner.onBeforeCollect?.(paths);
|
|
2037
2568
|
const files = await collectTests(specs, runner);
|
|
2038
|
-
await
|
|
2569
|
+
await runner.onCollected?.(files);
|
|
2039
2570
|
return files;
|
|
2040
2571
|
}
|
|
2041
2572
|
|
|
@@ -2049,12 +2580,13 @@ async function publicCollect(specs, runner) {
|
|
|
2049
2580
|
*
|
|
2050
2581
|
* Vitest automatically injects the source location where the artifact was created and manages any attachments you include.
|
|
2051
2582
|
*
|
|
2583
|
+
* **Note:** artifacts must be recorded before the task is reported. Any artifacts recorded after that will not be included in the task.
|
|
2584
|
+
*
|
|
2052
2585
|
* @param task - The test task context, typically accessed via `this.task` in custom matchers or `context.task` in tests
|
|
2053
2586
|
* @param artifact - The artifact to record. Must extend {@linkcode TestArtifactBase}
|
|
2054
2587
|
*
|
|
2055
2588
|
* @returns A promise that resolves to the recorded artifact with location injected
|
|
2056
2589
|
*
|
|
2057
|
-
* @throws {Error} If called after the test has finished running
|
|
2058
2590
|
* @throws {Error} If the test runner doesn't support artifacts
|
|
2059
2591
|
*
|
|
2060
2592
|
* @example
|
|
@@ -2075,9 +2607,6 @@ async function publicCollect(specs, runner) {
|
|
|
2075
2607
|
*/
|
|
2076
2608
|
async function recordArtifact(task, artifact) {
|
|
2077
2609
|
const runner = getRunner();
|
|
2078
|
-
if (task.result && task.result.state !== "run") {
|
|
2079
|
-
throw new Error(`Cannot record a test artifact outside of the test run. The test "${task.name}" finished running with the "${task.result.state}" state already.`);
|
|
2080
|
-
}
|
|
2081
2610
|
const stack = findTestFileStackTrace(task.file.filepath, new Error("STACK_TRACE").stack);
|
|
2082
2611
|
if (stack) {
|
|
2083
2612
|
artifact.location = {
|
|
@@ -2195,4 +2724,4 @@ function manageArtifactAttachment(attachment) {
|
|
|
2195
2724
|
}
|
|
2196
2725
|
}
|
|
2197
2726
|
|
|
2198
|
-
export { afterAll, afterEach, beforeAll, beforeEach, publicCollect as collectTests, createTaskCollector, describe, getCurrentSuite, getCurrentTest, getFn, getHooks, it, onTestFailed, onTestFinished, recordArtifact, setFn, setHooks, startTests, suite, test, updateTask };
|
|
2727
|
+
export { afterAll, afterEach, aroundAll, aroundEach, beforeAll, beforeEach, publicCollect as collectTests, createTaskCollector, describe, getCurrentSuite, getCurrentTest, getFn, getHooks, it, onTestFailed, onTestFinished, recordArtifact, setFn, setHooks, startTests, suite, test, updateTask };
|