@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/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 { c as createChainable, v as validateTags, h as createTaskName, w as createNoTagsError, f as findTestFileStackTrace, e as createTagsFilter, b as createFileTask, a as calculateSuiteHash, s as someTasksAreOnly, i as interpretTaskModes, l as limitConcurrency, p as partitionSuiteChildren, t as hasTests, r as hasFailed } from './chunk-tasks.js';
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 getTestFixture(key) {
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
- function mergeScopedFixtures(testFixtures, scopedFixtures) {
52
- const scopedFixturesMap = scopedFixtures.reduce((map, fixture) => {
53
- map[fixture.prop] = fixture;
54
- return map;
55
- }, {});
56
- const newFixtures = {};
57
- testFixtures.forEach((fixture) => {
58
- const useFixture = scopedFixturesMap[fixture.prop] || { ...fixture };
59
- newFixtures[useFixture.prop] = useFixture;
60
- });
61
- for (const fixtureKep in newFixtures) {
62
- var _fixture$deps;
63
- const fixture = newFixtures[fixtureKep];
64
- // if the fixture was define before the scope, then its dep
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
- const fixtureArray = Object.entries(fixtures).map(([prop, value]) => {
77
- const fixtureItem = { value };
78
- if (Array.isArray(value) && value.length >= 2 && isObject(value[1]) && Object.keys(value[1]).some((key) => fixtureOptionKeys.includes(key))) {
79
- var _runner$injectValue;
80
- // fixture with options
81
- Object.assign(fixtureItem, value[1]);
82
- const userValue = value[0];
83
- fixtureItem.value = fixtureItem.injected ? ((_runner$injectValue = runner.injectValue) === null || _runner$injectValue === void 0 ? void 0 : _runner$injectValue.call(runner, prop)) ?? userValue : userValue;
84
- }
85
- fixtureItem.scope = fixtureItem.scope || "test";
86
- if (fixtureItem.scope === "worker" && !runner.getWorkerContext) {
87
- fixtureItem.scope = "file";
88
- }
89
- fixtureItem.prop = prop;
90
- fixtureItem.isFn = typeof fixtureItem.value === "function";
91
- return fixtureItem;
92
- });
93
- if (Array.isArray(context.fixtures)) {
94
- context.fixtures = context.fixtures.concat(fixtureArray);
95
- } else {
96
- context.fixtures = fixtureArray;
97
- }
98
- // Update dependencies of fixture functions
99
- fixtureArray.forEach((fixture) => {
100
- if (fixture.isFn) {
101
- const usedProps = getUsedProps(fixture.value);
102
- if (usedProps.length) {
103
- fixture.deps = context.fixtures.filter(({ prop }) => prop !== fixture.prop && usedProps.includes(prop));
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
- // test can access anything, so we ignore it
106
- if (fixture.scope !== "test") {
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
- return context;
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 fixtureValueMaps = new Map();
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
- function withFixtures(runner, fn, testContext) {
138
- return (hookContext) => {
139
- const context = hookContext || testContext;
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 = getTestFixture(context);
144
- if (!(fixtures === null || fixtures === void 0 ? void 0 : fixtures.length)) {
284
+ const fixtures = options?.fixtures || getTestFixtures(context);
285
+ if (!fixtures) {
145
286
  return fn(context);
146
287
  }
147
- const usedProps = getUsedProps(fn);
148
- const hasAutoFixture = fixtures.some(({ auto }) => auto);
149
- if (!usedProps.length && !hasAutoFixture) {
288
+ const registrations = fixtures.get(suite);
289
+ if (!registrations.size) {
150
290
  return fn(context);
151
291
  }
152
- if (!fixtureValueMaps.get(context)) {
153
- fixtureValueMaps.set(context, new Map());
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 usedFixtures = fixtures.filter(({ prop, auto }) => auto || usedProps.includes(prop));
161
- const pendingFixtures = resolveDeps(usedFixtures);
306
+ const pendingFixtures = resolveDeps(usedFixtures, registrations);
162
307
  if (!pendingFixtures.length) {
163
308
  return fn(context);
164
309
  }
165
- async function resolveFixtures() {
166
- for (const fixture of pendingFixtures) {
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
- if (fixtureValueMap.has(fixture)) {
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
- const resolvedValue = await resolveFixtureValue(runner, fixture, context, cleanupFnArray);
172
- context[fixture.prop] = resolvedValue;
173
- fixtureValueMap.set(fixture, resolvedValue);
174
- if (fixture.scope === "test") {
175
- cleanupFnArray.unshift(() => {
176
- fixtureValueMap.delete(fixture);
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 resolveFixtures().then(() => fn(context));
352
+ return fn(context);
182
353
  };
183
354
  }
184
- const globalFixturePromise = new WeakMap();
185
- function resolveFixtureValue(runner, fixture, context, cleanupFnArray) {
186
- var _runner$getWorkerCont;
187
- const fileContext = getFileContext(context.task.file);
188
- const workerContext = (_runner$getWorkerCont = runner.getWorkerContext) === null || _runner$getWorkerCont === void 0 ? void 0 : _runner$getWorkerCont.call(runner);
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
- if (fixture.scope === "test") {
199
- return resolveFixtureFunction(fixture.value, context, cleanupFnArray);
200
- }
201
- // in case the test runs in parallel
202
- if (globalFixturePromise.has(fixture)) {
203
- return globalFixturePromise.get(fixture);
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
- let fixtureContext;
206
- if (fixture.scope === "worker") {
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.prop in fixtureContext) {
215
- return fixtureContext[fixture.prop];
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, fixtureContext, cleanupFnFileArray).then((value) => {
222
- fixtureContext[fixture.prop] = value;
223
- globalFixturePromise.delete(fixture);
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
- globalFixturePromise.set(fixture, promise);
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(fixtures, depSet = new Set(), pendingFixtures = []) {
258
- fixtures.forEach((fixture) => {
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.isFn || !fixture.deps) {
427
+ if (!isFixtureFunction(fixture.value) || !fixture.deps) {
263
428
  pendingFixtures.push(fixture);
264
429
  return;
265
430
  }
266
431
  if (depSet.has(fixture)) {
267
- throw new Error(`Circular fixture dependency detected: ${fixture.prop} <- ${[...depSet].reverse().map((d) => d.prop).join(" <- ")}`);
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
- let fnString = filterOutComments(fn.toString());
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
- let first = args[0];
295
- if ("__VITEST_FIXTURE_INDEX__" in fn) {
296
- first = args[fn.__VITEST_FIXTURE_INDEX__];
297
- if (!first) {
298
- return [];
299
- }
482
+ const fixturesArgument = args[fixturesIndex];
483
+ if (!fixturesArgument) {
484
+ return new Set();
300
485
  }
301
- if (!(first[0] === "{" && first.endsWith("}"))) {
302
- throw new Error(`The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${first}".`);
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 = first.slice(1, -1).replace(/\s/g, "");
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
- return getCurrentSuite().on("beforeAll", Object.assign(withTimeout(fn, timeout, true, stackTraceError), {
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
- return getCurrentSuite().on("afterAll", withTimeout(fn, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR")));
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
- const runner = getRunner();
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
- const runner = getRunner();
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 || (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 || (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, parentCollectorFixtures) {
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
- var _collectorContext$cur, _collectorContext$cur2, _collectorContext$cur3, _collectorContext$cur4;
737
- const currentSuite = (_collectorContext$cur = collectorContext.currentSuite) === null || _collectorContext$cur === void 0 ? void 0 : _collectorContext$cur.suite;
738
- const parentTask = currentSuite ?? ((_collectorContext$cur2 = collectorContext.currentSuite) === null || _collectorContext$cur2 === void 0 ? void 0 : _collectorContext$cur2.file);
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
- var _runner$config$tags;
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([(currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullName) ?? ((_collectorContext$cur3 = collectorContext.currentSuite) === null || _collectorContext$cur3 === void 0 || (_collectorContext$cur3 = _collectorContext$cur3.file) === null || _collectorContext$cur3 === void 0 ? void 0 : _collectorContext$cur3.fullName), name]),
762
- fullTestName: createTaskName([currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullTestName, name]),
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: (currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.file) ?? ((_collectorContext$cur4 = collectorContext.currentSuite) === null || _collectorContext$cur4 === void 0 ? void 0 : _collectorContext$cur4.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: options.meta ?? Object.create(null),
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 === null || suiteOptions === void 0 ? void 0 : suiteOptions.shuffle;
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 = 15;
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(runner, handler, context), task), timeout, false, stackTraceError, (_, error) => abortIfTimeout([context], error)));
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 && (options === null || options === void 0 ? void 0 : options.concurrent));
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 && (options === null || options === void 0 ? void 0 : options.sequential));
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 = (_collectorContext$cur5 = collectorContext.currentSuite) === null || _collectorContext$cur5 === void 0 ? void 0 : _collectorContext$cur5.suite;
868
- const parentTask = currentSuite ?? ((_collectorContext$cur6 = collectorContext.currentSuite) === null || _collectorContext$cur6 === void 0 ? void 0 : _collectorContext$cur6.file);
869
- const suiteTags = toArray(suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.tags);
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([(currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullName) ?? ((_collectorContext$cur7 = collectorContext.currentSuite) === null || _collectorContext$cur7 === void 0 || (_collectorContext$cur7 = _collectorContext$cur7.file) === null || _collectorContext$cur7 === void 0 ? void 0 : _collectorContext$cur7.fullName), name]),
876
- fullTestName: createTaskName([currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullTestName, name]),
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: (currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.file) ?? ((_collectorContext$cur8 = collectorContext.currentSuite) === null || _collectorContext$cur8 === void 0 ? void 0 : _collectorContext$cur8.file),
881
- shuffle: suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.shuffle,
1180
+ file: currentSuite?.file ?? collectorContext.currentSuite?.file,
1181
+ shuffle: suiteOptions?.shuffle,
882
1182
  tasks: [],
883
- meta: Object.create(null),
884
- concurrent: suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.concurrent,
885
- tags: unique([...(parentTask === null || parentTask === void 0 ? void 0 : parentTask.tags) || [], ...suiteTags])
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
- ...currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.options,
1249
+ ...parentOptions,
950
1250
  ...options
951
1251
  };
952
- const shuffle = this.shuffle ?? options.shuffle ?? (currentSuite === null || currentSuite === void 0 || (_currentSuite$options = currentSuite.options) === null || _currentSuite$options === void 0 ? void 0 : _currentSuite$options.shuffle) ?? (runner === null || runner === void 0 ? void 0 : runner.config.sequence.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
- return createSuiteCollector(formatName(name), factory, mode, this.each, options, currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fixtures());
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 suite = this.withContext();
974
- this.setContext("each", true);
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
- this.setContext("each", undefined);
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, context) {
1329
+ function createTaskCollector(fn) {
1026
1330
  const taskFn = fn;
1027
1331
  taskFn.each = function(cases, ...args) {
1028
- const test = this.withContext();
1029
- this.setContext("each", true);
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
- this.setContext("each", undefined);
1359
+ context.setContext("each", undefined);
1055
1360
  };
1056
1361
  };
1057
1362
  taskFn.for = function(cases, ...args) {
1058
- const test = this.withContext();
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.__VITEST_FIXTURE_INDEX__ = 1;
1070
- handlerWrapper.toString = () => handler.toString();
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
- const collector = getCurrentSuite();
1084
- collector.scoped(fixtures);
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(fixtures) {
1087
- const _context = mergeContextFixtures(fixtures, context || {}, runner);
1088
- const originalWrapper = fn;
1089
- return createTest(function(name, optionsOrFn, optionsOrTest) {
1090
- const collector = getCurrentSuite();
1091
- const scopedFixtures = collector.fixtures();
1092
- const context = { ...this };
1093
- if (scopedFixtures) {
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, context) {
1117
- return createTaskCollector(fn, context);
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 === null || runner === void 0 || (_runner$config = runner.config) === null || _runner$config === void 0 || (_runner$config = _runner$config.chaiConfig) === null || _runner$config === void 0 ? void 0 : _runner$config.truncateThreshold });
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
- var _collectorContext$cur;
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
- (_timer$unref = timer.unref) === null || _timer$unref === void 0 ? void 0 : _timer$unref.call(timer);
1600
+ timer.unref?.();
1235
1601
  function rejectTimeoutError() {
1236
1602
  const error = makeTimeoutError(isHook, timeout, stackTraceError);
1237
- onTimeout === null || onTimeout === void 0 ? void 0 : onTimeout(args, error);
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 === null || abortController === void 0 ? void 0 : abortController.abort(error);
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 ?? (test.result = { state: "skip" });
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 || (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 || (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 ((_runner$extendTaskCon = runner.extendTaskContext) === null || _runner$extendTaskCon === void 0 ? void 0 : _runner$extendTaskCon.call(runner, context)) || context;
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 === null || stackTraceError === void 0 ? void 0 : stackTraceError.stack) {
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
- (_runner$onCollectStar = runner.onCollectStart) === null || _runner$onCollectStar === void 0 ? void 0 : _runner$onCollectStar.call(runner, file);
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 = (_runner$getImportDura = runner.getImportDurations) === null || _runner$getImportDura === void 0 ? void 0 : _runner$getImportDura.call(runner);
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
- var _runner$getImportDura2;
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: [error]
1789
+ errors
1439
1790
  };
1440
- const durations = (_runner$getImportDura2 = runner.getImportDurations) === null || _runner$getImportDura2 === void 0 ? void 0 : _runner$getImportDura2.call(runner);
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 = (_runner$onTaskUpdate = runner.onTaskUpdate) === null || _runner$onTaskUpdate === void 0 ? void 0 : _runner$onTaskUpdate.call(runner, taskPacks, eventsPacks);
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 ?? (pendingCall = setTimeout(() => call.bind(this)(...args), ms));
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
- var _runner$onBeforeRunTa, _test$result, _runner$onAfterRunTas;
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 (((_test$result = test.result) === null || _test$result === void 0 ? void 0 : _test$result.state) === "fail") {
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
- try {
1716
- var _runner$onBeforeTryTa, _runner$onAfterTryTas;
1717
- await ((_runner$onBeforeTryTa = runner.onBeforeTryTask) === null || _runner$onBeforeTryTa === void 0 ? void 0 : _runner$onBeforeTryTa.call(runner, test, {
1718
- retry: retryCount,
1719
- repeats: repeatCount
1720
- }));
1721
- test.result.repeatCount = repeatCount;
1722
- beforeEachCleanups = await $("test.beforeEach", () => callSuiteHook(suite, test, "beforeEach", runner, [test.context, suite]));
1723
- if (runner.runTask) {
1724
- await $("test.callback", () => runner.runTask(test));
1725
- } else {
1726
- const fn = getFn(test);
1727
- if (!fn) {
1728
- throw new Error("Test function is not found. Did you add it using `setFn`?");
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
- await $("test.callback", () => fn());
2263
+ } catch (e) {
2264
+ failTask(test.result, e, runner.config.diffOptions);
1731
2265
  }
1732
- await ((_runner$onAfterTryTas = runner.onAfterTryTask) === null || _runner$onAfterTryTas === void 0 ? void 0 : _runner$onAfterTryTas.call(runner, test, {
1733
- retry: retryCount,
1734
- repeats: repeatCount
1735
- }));
1736
- if (test.result.state !== "fail") {
1737
- if (!test.repeats) {
1738
- test.result.state = "pass";
1739
- } else if (test.repeats && retry === retryCount) {
1740
- test.result.state = "pass";
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
- } catch (e) {
1744
- failTask(test.result, e, runner.config.diffOptions);
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 (((_test$result2 = test.result) === null || _test$result2 === void 0 ? void 0 : _test$result2.pending) || ((_test$result3 = test.result) === null || _test$result3 === void 0 ? void 0 : _test$result3.state) === "skip") {
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: (_test$result4 = test.result) === null || _test$result4 === void 0 ? void 0 : _test$result4.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 ((_runner$onAfterRunTas = runner.onAfterRunTask) === null || _runner$onAfterRunTas === void 0 ? void 0 : _runner$onAfterRunTas.call(runner, test));
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 error = processError(e, diffOptions);
1835
- result.errors ?? (result.errors = []);
1836
- result.errors.push(error);
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
- var _runner$onBeforeRunSu, _suite$result;
1854
- await ((_runner$onBeforeRunSu = runner.onBeforeRunSuite) === null || _runner$onBeforeRunSu === void 0 ? void 0 : _runner$onBeforeRunSu.call(runner, suite));
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
- var _runner$onAfterRunSui;
2406
+ let suiteRan = false;
1878
2407
  try {
1879
- try {
1880
- beforeAllCleanups = await $("suite.beforeAll", () => callSuiteHook(suite, suite, "beforeAll", runner, [suite]));
1881
- } catch (e) {
1882
- markTasksAsSkipped(suite, runner);
1883
- throw e;
1884
- }
1885
- if (runner.runSuite) {
1886
- await runner.runSuite(suite);
1887
- } else {
1888
- for (let tasksGroup of partitionSuiteChildren(suite)) {
1889
- if (tasksGroup[0].concurrent === true) {
1890
- await Promise.all(tasksGroup.map((c) => runSuiteChild(c, runner)));
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
- const { sequence } = runner.config;
1893
- if (suite.shuffle) {
1894
- // run describe block independently from tests
1895
- const suites = tasksGroup.filter((group) => group.type === "suite");
1896
- const tests = tasksGroup.filter((group) => group.type === "test");
1897
- const groups = shuffle([suites, tests], sequence.seed);
1898
- tasksGroup = groups.flatMap((group) => shuffle(group, sequence.seed));
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
- for (const c of tasksGroup) {
1901
- await runSuiteChild(c, runner);
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
- failTask(suite.result, e, runner.config.diffOptions);
1908
- }
1909
- try {
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 (!((_suite$result$errors = suite.result.errors) === null || _suite$result$errors === void 0 ? void 0 : _suite$result$errors.length)) {
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 ((_runner$onAfterRunSui = runner.onAfterRunSuite) === null || _runner$onAfterRunSui === void 0 ? void 0 : _runner$onAfterRunSui.call(runner, suite));
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
- var _c$location, _c$location2;
1946
- return $("run.test", {
1947
- "vitest.test.id": c.id,
1948
- "vitest.test.name": c.name,
1949
- "vitest.test.mode": c.mode,
1950
- "vitest.test.timeout": c.timeout,
1951
- "code.file.path": c.file.filepath,
1952
- "code.line.number": (_c$location = c.location) === null || _c$location === void 0 ? void 0 : _c$location.line,
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": (_c$location3 = c.location) === null || _c$location3 === void 0 ? void 0 : _c$location3.line,
1964
- "code.column.number": (_c$location4 = c.location) === null || _c$location4 === void 0 ? void 0 : _c$location4.column
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 ?? (limitMaxConcurrency = limitConcurrency(runner.config.maxConcurrency));
2507
+ limitMaxConcurrency ??= limitConcurrency(runner.config.maxConcurrency);
1970
2508
  for (const file of files) {
1971
2509
  if (!file.tasks.length && !runner.config.passWithNoTests) {
1972
- var _file$result;
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
- var _runner$cancel;
1996
- runner.trace ?? (runner.trace = defaultTrace);
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 === null || cancel === void 0 ? void 0 : cancel(reason);
2540
+ return cancel?.(reason);
2005
2541
  };
2006
2542
  if (!workerRunners.has(runner)) {
2007
- var _runner$onCleanupWork;
2008
- (_runner$onCleanupWork = runner.onCleanupWorkerContext) === null || _runner$onCleanupWork === void 0 ? void 0 : _runner$onCleanupWork.call(runner, async () => {
2009
- var _runner$getWorkerCont;
2010
- const context = (_runner$getWorkerCont = runner.getWorkerContext) === null || _runner$getWorkerCont === void 0 ? void 0 : _runner$getWorkerCont.call(runner);
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 ((_runner$onBeforeColle = runner.onBeforeCollect) === null || _runner$onBeforeColle === void 0 ? void 0 : _runner$onBeforeColle.call(runner, paths));
2552
+ await runner.onBeforeCollect?.(paths);
2021
2553
  const files = await collectTests(specs, runner);
2022
- await ((_runner$onCollected = runner.onCollected) === null || _runner$onCollected === void 0 ? void 0 : _runner$onCollected.call(runner, files));
2023
- await ((_runner$onBeforeRunFi = runner.onBeforeRunFiles) === null || _runner$onBeforeRunFi === void 0 ? void 0 : _runner$onBeforeRunFi.call(runner, files));
2554
+ await runner.onCollected?.(files);
2555
+ await runner.onBeforeRunFiles?.(files);
2024
2556
  await runFiles(files, runner);
2025
- await ((_runner$onAfterRunFil = runner.onAfterRunFiles) === null || _runner$onAfterRunFil === void 0 ? void 0 : _runner$onAfterRunFil.call(runner, files));
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
- var _runner$onBeforeColle2, _runner$onCollected2;
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 ((_runner$onBeforeColle2 = runner.onBeforeCollect) === null || _runner$onBeforeColle2 === void 0 ? void 0 : _runner$onBeforeColle2.call(runner, paths));
2567
+ await runner.onBeforeCollect?.(paths);
2037
2568
  const files = await collectTests(specs, runner);
2038
- await ((_runner$onCollected2 = runner.onCollected) === null || _runner$onCollected2 === void 0 ? void 0 : _runner$onCollected2.call(runner, files));
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 };