@depup/vitest__runner 4.1.0-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2805 @@
1
+ import { processError } from '@vitest/utils/error';
2
+ import { isObject, filterOutComments, ordinal, createDefer, assertTypes, toArray, isNegativeNaN, unique, objectAttr, shuffle } from '@vitest/utils/helpers';
3
+ import { getSafeTimers } from '@vitest/utils/timers';
4
+ import { format, formatRegExp, objDisplay } from '@vitest/utils/display';
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
+ import '@vitest/utils/source-map';
7
+ import 'pathe';
8
+
9
+ class PendingError extends Error {
10
+ code = "VITEST_PENDING";
11
+ taskId;
12
+ constructor(message, task, note) {
13
+ super(message);
14
+ this.message = message;
15
+ this.note = note;
16
+ this.taskId = task.id;
17
+ }
18
+ }
19
+ class TestRunAbortError extends Error {
20
+ name = "TestRunAbortError";
21
+ reason;
22
+ constructor(message, reason) {
23
+ super(message);
24
+ this.reason = reason;
25
+ }
26
+ }
27
+ class FixtureDependencyError extends Error {
28
+ name = "FixtureDependencyError";
29
+ }
30
+ class FixtureAccessError extends Error {
31
+ name = "FixtureAccessError";
32
+ }
33
+ class FixtureParseError extends Error {
34
+ name = "FixtureParseError";
35
+ }
36
+ class AroundHookSetupError extends Error {
37
+ name = "AroundHookSetupError";
38
+ }
39
+ class AroundHookTeardownError extends Error {
40
+ name = "AroundHookTeardownError";
41
+ }
42
+ class AroundHookMultipleCallsError extends Error {
43
+ name = "AroundHookMultipleCallsError";
44
+ }
45
+
46
+ // use WeakMap here to make the Test and Suite object serializable
47
+ const fnMap = new WeakMap();
48
+ const testFixtureMap = new WeakMap();
49
+ const hooksMap = new WeakMap();
50
+ function setFn(key, fn) {
51
+ fnMap.set(key, fn);
52
+ }
53
+ function getFn(key) {
54
+ return fnMap.get(key);
55
+ }
56
+ function setTestFixture(key, fixture) {
57
+ testFixtureMap.set(key, fixture);
58
+ }
59
+ function getTestFixtures(key) {
60
+ return testFixtureMap.get(key);
61
+ }
62
+ function setHooks(key, hooks) {
63
+ hooksMap.set(key, hooks);
64
+ }
65
+ function getHooks(key) {
66
+ return hooksMap.get(key);
67
+ }
68
+
69
+ class TestFixtures {
70
+ _suiteContexts;
71
+ _overrides = new WeakMap();
72
+ _registrations;
73
+ static _definitions = [];
74
+ static _builtinFixtures = [
75
+ "task",
76
+ "signal",
77
+ "onTestFailed",
78
+ "onTestFinished",
79
+ "skip",
80
+ "annotate"
81
+ ];
82
+ static _fixtureOptionKeys = [
83
+ "auto",
84
+ "injected",
85
+ "scope"
86
+ ];
87
+ static _fixtureScopes = [
88
+ "test",
89
+ "file",
90
+ "worker"
91
+ ];
92
+ static _workerContextSuite = { type: "worker" };
93
+ static clearDefinitions() {
94
+ TestFixtures._definitions.length = 0;
95
+ }
96
+ static getWorkerContexts() {
97
+ return TestFixtures._definitions.map((f) => f.getWorkerContext());
98
+ }
99
+ static getFileContexts(file) {
100
+ return TestFixtures._definitions.map((f) => f.getFileContext(file));
101
+ }
102
+ constructor(registrations) {
103
+ this._registrations = registrations ?? new Map();
104
+ this._suiteContexts = new WeakMap();
105
+ TestFixtures._definitions.push(this);
106
+ }
107
+ extend(runner, userFixtures) {
108
+ const { suite } = getCurrentSuite();
109
+ const isTopLevel = !suite || suite.file === suite;
110
+ const registrations = this.parseUserFixtures(runner, userFixtures, isTopLevel);
111
+ return new TestFixtures(registrations);
112
+ }
113
+ get(suite) {
114
+ let currentSuite = suite;
115
+ while (currentSuite) {
116
+ const overrides = this._overrides.get(currentSuite);
117
+ // return the closest override
118
+ if (overrides) {
119
+ return overrides;
120
+ }
121
+ if (currentSuite === currentSuite.file) {
122
+ break;
123
+ }
124
+ currentSuite = currentSuite.suite || currentSuite.file;
125
+ }
126
+ return this._registrations;
127
+ }
128
+ override(runner, userFixtures) {
129
+ const { suite: currentSuite, file } = getCurrentSuite();
130
+ const suite = currentSuite || file;
131
+ const isTopLevel = !currentSuite || currentSuite.file === currentSuite;
132
+ // Create a copy of the closest parent's registrations to avoid modifying them
133
+ // For chained calls, this.get(suite) returns this suite's overrides; for first call, returns parent's
134
+ const suiteRegistrations = new Map(this.get(suite));
135
+ const registrations = this.parseUserFixtures(runner, userFixtures, isTopLevel, suiteRegistrations);
136
+ // If defined in top-level, just override all registrations
137
+ // We don't support overriding suite-level fixtures anyway (it will throw an error)
138
+ if (isTopLevel) {
139
+ this._registrations = registrations;
140
+ } else {
141
+ this._overrides.set(suite, registrations);
142
+ }
143
+ }
144
+ getFileContext(file) {
145
+ if (!this._suiteContexts.has(file)) {
146
+ this._suiteContexts.set(file, Object.create(null));
147
+ }
148
+ return this._suiteContexts.get(file);
149
+ }
150
+ getWorkerContext() {
151
+ if (!this._suiteContexts.has(TestFixtures._workerContextSuite)) {
152
+ this._suiteContexts.set(TestFixtures._workerContextSuite, Object.create(null));
153
+ }
154
+ return this._suiteContexts.get(TestFixtures._workerContextSuite);
155
+ }
156
+ parseUserFixtures(runner, userFixtures, supportNonTest, registrations = new Map(this._registrations)) {
157
+ const errors = [];
158
+ Object.entries(userFixtures).forEach(([name, fn]) => {
159
+ let options;
160
+ let value;
161
+ let _options;
162
+ if (Array.isArray(fn) && fn.length >= 2 && isObject(fn[1]) && Object.keys(fn[1]).some((key) => TestFixtures._fixtureOptionKeys.includes(key))) {
163
+ _options = fn[1];
164
+ options = {
165
+ auto: _options.auto ?? false,
166
+ scope: _options.scope ?? "test",
167
+ injected: _options.injected ?? false
168
+ };
169
+ value = options.injected ? runner.injectValue?.(name) ?? fn[0] : fn[0];
170
+ } else {
171
+ value = fn;
172
+ }
173
+ const parent = registrations.get(name);
174
+ if (parent && options) {
175
+ if (parent.scope !== options.scope) {
176
+ errors.push(new FixtureDependencyError(`The "${name}" fixture was already registered with a "${options.scope}" scope.`));
177
+ }
178
+ if (parent.auto !== options.auto) {
179
+ errors.push(new FixtureDependencyError(`The "${name}" fixture was already registered as { auto: ${options.auto} }.`));
180
+ }
181
+ } else if (parent) {
182
+ options = {
183
+ auto: parent.auto,
184
+ scope: parent.scope,
185
+ injected: parent.injected
186
+ };
187
+ } else if (!options) {
188
+ options = {
189
+ auto: false,
190
+ injected: false,
191
+ scope: "test"
192
+ };
193
+ }
194
+ if (options.scope && !TestFixtures._fixtureScopes.includes(options.scope)) {
195
+ errors.push(new FixtureDependencyError(`The "${name}" fixture has unknown scope "${options.scope}".`));
196
+ }
197
+ if (!supportNonTest && options.scope !== "test") {
198
+ 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.`));
199
+ }
200
+ const deps = isFixtureFunction(value) ? getUsedProps(value) : new Set();
201
+ const item = {
202
+ name,
203
+ value,
204
+ auto: options.auto ?? false,
205
+ injected: options.injected ?? false,
206
+ scope: options.scope ?? "test",
207
+ deps,
208
+ parent
209
+ };
210
+ registrations.set(name, item);
211
+ if (item.scope === "worker" && (runner.pool === "vmThreads" || runner.pool === "vmForks")) {
212
+ item.scope = "file";
213
+ }
214
+ });
215
+ // validate fixture dependency scopes
216
+ for (const fixture of registrations.values()) {
217
+ for (const depName of fixture.deps) {
218
+ if (TestFixtures._builtinFixtures.includes(depName)) {
219
+ continue;
220
+ }
221
+ const dep = registrations.get(depName);
222
+ if (!dep) {
223
+ errors.push(new FixtureDependencyError(`The "${fixture.name}" fixture depends on unknown fixture "${depName}".`));
224
+ continue;
225
+ }
226
+ if (depName === fixture.name && !fixture.parent) {
227
+ errors.push(new FixtureDependencyError(`The "${fixture.name}" fixture depends on itself, but does not have a base implementation.`));
228
+ continue;
229
+ }
230
+ if (TestFixtures._fixtureScopes.indexOf(fixture.scope) > TestFixtures._fixtureScopes.indexOf(dep.scope)) {
231
+ errors.push(new FixtureDependencyError(`The ${fixture.scope} "${fixture.name}" fixture cannot depend on a ${dep.scope} fixture "${dep.name}".`));
232
+ continue;
233
+ }
234
+ }
235
+ }
236
+ if (errors.length === 1) {
237
+ throw errors[0];
238
+ } else if (errors.length > 1) {
239
+ throw new AggregateError(errors, "Cannot resolve user fixtures. See errors for more information.");
240
+ }
241
+ return registrations;
242
+ }
243
+ }
244
+ const cleanupFnArrayMap = new WeakMap();
245
+ async function callFixtureCleanup(context) {
246
+ const cleanupFnArray = cleanupFnArrayMap.get(context) ?? [];
247
+ for (const cleanup of cleanupFnArray.reverse()) {
248
+ await cleanup();
249
+ }
250
+ cleanupFnArrayMap.delete(context);
251
+ }
252
+ /**
253
+ * Returns the current number of cleanup functions registered for the context.
254
+ * This can be used as a checkpoint to later clean up only fixtures added after this point.
255
+ */
256
+ function getFixtureCleanupCount(context) {
257
+ return cleanupFnArrayMap.get(context)?.length ?? 0;
258
+ }
259
+ /**
260
+ * Cleans up only fixtures that were added after the given checkpoint index.
261
+ * This is used by aroundEach to clean up fixtures created inside runTest()
262
+ * while preserving fixtures that were created for aroundEach itself.
263
+ */
264
+ async function callFixtureCleanupFrom(context, fromIndex) {
265
+ const cleanupFnArray = cleanupFnArrayMap.get(context);
266
+ if (!cleanupFnArray || cleanupFnArray.length <= fromIndex) {
267
+ return;
268
+ }
269
+ // Get items added after the checkpoint
270
+ const toCleanup = cleanupFnArray.slice(fromIndex);
271
+ // Clean up in reverse order
272
+ for (const cleanup of toCleanup.reverse()) {
273
+ await cleanup();
274
+ }
275
+ // Remove cleaned up items from the array, keeping items before checkpoint
276
+ cleanupFnArray.length = fromIndex;
277
+ }
278
+ const contextHasFixturesCache = new WeakMap();
279
+ function withFixtures(fn, options) {
280
+ const collector = getCurrentSuite();
281
+ const suite = options?.suite || collector.suite || collector.file;
282
+ return async (hookContext) => {
283
+ const context = hookContext || options?.context;
284
+ if (!context) {
285
+ if (options?.suiteHook) {
286
+ validateSuiteHook(fn, options.suiteHook, options.stackTraceError);
287
+ }
288
+ return fn({});
289
+ }
290
+ const fixtures = options?.fixtures || getTestFixtures(context);
291
+ if (!fixtures) {
292
+ return fn(context);
293
+ }
294
+ const registrations = fixtures.get(suite);
295
+ if (!registrations.size) {
296
+ return fn(context);
297
+ }
298
+ const usedFixtures = [];
299
+ const usedProps = getUsedProps(fn);
300
+ for (const fixture of registrations.values()) {
301
+ if (fixture.auto || usedProps.has(fixture.name)) {
302
+ usedFixtures.push(fixture);
303
+ }
304
+ }
305
+ if (!usedFixtures.length) {
306
+ return fn(context);
307
+ }
308
+ if (!cleanupFnArrayMap.has(context)) {
309
+ cleanupFnArrayMap.set(context, []);
310
+ }
311
+ const cleanupFnArray = cleanupFnArrayMap.get(context);
312
+ const pendingFixtures = resolveDeps(usedFixtures, registrations);
313
+ if (!pendingFixtures.length) {
314
+ return fn(context);
315
+ }
316
+ // Check if suite-level hook is trying to access test-scoped fixtures
317
+ // Suite hooks (beforeAll/afterAll/aroundAll) can only access file/worker scoped fixtures
318
+ if (options?.suiteHook) {
319
+ const testScopedFixtures = pendingFixtures.filter((f) => f.scope === "test");
320
+ if (testScopedFixtures.length > 0) {
321
+ const fixtureNames = testScopedFixtures.map((f) => `"${f.name}"`).join(", ");
322
+ const alternativeHook = {
323
+ aroundAll: "aroundEach",
324
+ beforeAll: "beforeEach",
325
+ afterAll: "afterEach"
326
+ };
327
+ 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.`);
328
+ // Use stack trace from hook registration for better error location
329
+ if (options.stackTraceError?.stack) {
330
+ error.stack = error.message + options.stackTraceError.stack.replace(options.stackTraceError.message, "");
331
+ }
332
+ throw error;
333
+ }
334
+ }
335
+ if (!contextHasFixturesCache.has(context)) {
336
+ contextHasFixturesCache.set(context, new WeakSet());
337
+ }
338
+ const cachedFixtures = contextHasFixturesCache.get(context);
339
+ for (const fixture of pendingFixtures) {
340
+ if (fixture.scope === "test") {
341
+ // fixture could be already initialized during "before" hook
342
+ // we can't check "fixture.name" in context because context may
343
+ // access the parent fixture ({ a: ({ a }) => {} })
344
+ if (cachedFixtures.has(fixture)) {
345
+ continue;
346
+ }
347
+ cachedFixtures.add(fixture);
348
+ const resolvedValue = await resolveTestFixtureValue(fixture, context, cleanupFnArray);
349
+ context[fixture.name] = resolvedValue;
350
+ cleanupFnArray.push(() => {
351
+ cachedFixtures.delete(fixture);
352
+ });
353
+ } else {
354
+ const resolvedValue = await resolveScopeFixtureValue(fixtures, suite, fixture);
355
+ context[fixture.name] = resolvedValue;
356
+ }
357
+ }
358
+ return fn(context);
359
+ };
360
+ }
361
+ function isFixtureFunction(value) {
362
+ return typeof value === "function";
363
+ }
364
+ function resolveTestFixtureValue(fixture, context, cleanupFnArray) {
365
+ if (!isFixtureFunction(fixture.value)) {
366
+ return fixture.value;
367
+ }
368
+ return resolveFixtureFunction(fixture.value, context, cleanupFnArray);
369
+ }
370
+ const scopedFixturePromiseCache = new WeakMap();
371
+ async function resolveScopeFixtureValue(fixtures, suite, fixture) {
372
+ const workerContext = fixtures.getWorkerContext();
373
+ const fileContext = fixtures.getFileContext(suite.file);
374
+ const fixtureContext = fixture.scope === "worker" ? workerContext : fileContext;
375
+ if (!isFixtureFunction(fixture.value)) {
376
+ fixtureContext[fixture.name] = fixture.value;
377
+ return fixture.value;
378
+ }
379
+ if (fixture.name in fixtureContext) {
380
+ return fixtureContext[fixture.name];
381
+ }
382
+ if (scopedFixturePromiseCache.has(fixture)) {
383
+ return scopedFixturePromiseCache.get(fixture);
384
+ }
385
+ if (!cleanupFnArrayMap.has(fixtureContext)) {
386
+ cleanupFnArrayMap.set(fixtureContext, []);
387
+ }
388
+ const cleanupFnFileArray = cleanupFnArrayMap.get(fixtureContext);
389
+ const promise = resolveFixtureFunction(fixture.value, fixture.scope === "file" ? {
390
+ ...workerContext,
391
+ ...fileContext
392
+ } : fixtureContext, cleanupFnFileArray).then((value) => {
393
+ fixtureContext[fixture.name] = value;
394
+ scopedFixturePromiseCache.delete(fixture);
395
+ return value;
396
+ });
397
+ scopedFixturePromiseCache.set(fixture, promise);
398
+ return promise;
399
+ }
400
+ async function resolveFixtureFunction(fixtureFn, context, cleanupFnArray) {
401
+ // wait for `use` call to extract fixture value
402
+ const useFnArgPromise = createDefer();
403
+ let isUseFnArgResolved = false;
404
+ const fixtureReturn = fixtureFn(context, async (useFnArg) => {
405
+ // extract `use` argument
406
+ isUseFnArgResolved = true;
407
+ useFnArgPromise.resolve(useFnArg);
408
+ // suspend fixture teardown by holding off `useReturnPromise` resolution until cleanup
409
+ const useReturnPromise = createDefer();
410
+ cleanupFnArray.push(async () => {
411
+ // start teardown by resolving `use` Promise
412
+ useReturnPromise.resolve();
413
+ // wait for finishing teardown
414
+ await fixtureReturn;
415
+ });
416
+ await useReturnPromise;
417
+ }).catch((e) => {
418
+ // treat fixture setup error as test failure
419
+ if (!isUseFnArgResolved) {
420
+ useFnArgPromise.reject(e);
421
+ return;
422
+ }
423
+ // otherwise re-throw to avoid silencing error during cleanup
424
+ throw e;
425
+ });
426
+ return useFnArgPromise;
427
+ }
428
+ function resolveDeps(usedFixtures, registrations, depSet = new Set(), pendingFixtures = []) {
429
+ usedFixtures.forEach((fixture) => {
430
+ if (pendingFixtures.includes(fixture)) {
431
+ return;
432
+ }
433
+ if (!isFixtureFunction(fixture.value) || !fixture.deps) {
434
+ pendingFixtures.push(fixture);
435
+ return;
436
+ }
437
+ if (depSet.has(fixture)) {
438
+ if (fixture.parent) {
439
+ fixture = fixture.parent;
440
+ } else {
441
+ throw new Error(`Circular fixture dependency detected: ${fixture.name} <- ${[...depSet].reverse().map((d) => d.name).join(" <- ")}`);
442
+ }
443
+ }
444
+ depSet.add(fixture);
445
+ resolveDeps([...fixture.deps].map((n) => n === fixture.name ? fixture.parent : registrations.get(n)).filter((n) => !!n), registrations, depSet, pendingFixtures);
446
+ pendingFixtures.push(fixture);
447
+ depSet.clear();
448
+ });
449
+ return pendingFixtures;
450
+ }
451
+ function validateSuiteHook(fn, hook, suiteError) {
452
+ const usedProps = getUsedProps(fn, {
453
+ sourceError: suiteError,
454
+ suiteHook: hook
455
+ });
456
+ if (usedProps.size) {
457
+ const error = new FixtureAccessError(`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}()"?\n` + `If you used internal "suite" task as the first argument previously, access it in the second argument instead. ` + `See https://vitest.dev/guide/test-context#suite-level-hooks`);
458
+ if (suiteError) {
459
+ error.stack = suiteError.stack?.replace(suiteError.message, error.message);
460
+ }
461
+ throw error;
462
+ }
463
+ }
464
+ const kPropsSymbol = Symbol("$vitest:fixture-props");
465
+ const kPropNamesSymbol = Symbol("$vitest:fixture-prop-names");
466
+ function configureProps(fn, options) {
467
+ Object.defineProperty(fn, kPropsSymbol, {
468
+ value: options,
469
+ enumerable: false
470
+ });
471
+ }
472
+ function memoProps(fn, props) {
473
+ fn[kPropNamesSymbol] = props;
474
+ return props;
475
+ }
476
+ function getUsedProps(fn, { sourceError, suiteHook } = {}) {
477
+ if (kPropNamesSymbol in fn) {
478
+ return fn[kPropNamesSymbol];
479
+ }
480
+ const { index: fixturesIndex = 0, original: implementation = fn } = kPropsSymbol in fn ? fn[kPropsSymbol] : {};
481
+ let fnString = filterOutComments(implementation.toString());
482
+ // match lowered async function and strip it off
483
+ // example code on esbuild-try https://esbuild.github.io/try/#YgAwLjI0LjAALS1zdXBwb3J0ZWQ6YXN5bmMtYXdhaXQ9ZmFsc2UAZQBlbnRyeS50cwBjb25zdCBvID0gewogIGYxOiBhc3luYyAoKSA9PiB7fSwKICBmMjogYXN5bmMgKGEpID0+IHt9LAogIGYzOiBhc3luYyAoYSwgYikgPT4ge30sCiAgZjQ6IGFzeW5jIGZ1bmN0aW9uKGEpIHt9LAogIGY1OiBhc3luYyBmdW5jdGlvbiBmZihhKSB7fSwKICBhc3luYyBmNihhKSB7fSwKCiAgZzE6IGFzeW5jICgpID0+IHt9LAogIGcyOiBhc3luYyAoeyBhIH0pID0+IHt9LAogIGczOiBhc3luYyAoeyBhIH0sIGIpID0+IHt9LAogIGc0OiBhc3luYyBmdW5jdGlvbiAoeyBhIH0pIHt9LAogIGc1OiBhc3luYyBmdW5jdGlvbiBnZyh7IGEgfSkge30sCiAgYXN5bmMgZzYoeyBhIH0pIHt9LAoKICBoMTogYXN5bmMgKCkgPT4ge30sCiAgLy8gY29tbWVudCBiZXR3ZWVuCiAgaDI6IGFzeW5jIChhKSA9PiB7fSwKfQ
484
+ // __async(this, null, function*
485
+ // __async(this, arguments, function*
486
+ // __async(this, [_0, _1], function*
487
+ if (/__async\((?:this|null), (?:null|arguments|\[[_0-9, ]*\]), function\*/.test(fnString)) {
488
+ fnString = fnString.split(/__async\((?:this|null),/)[1];
489
+ }
490
+ const match = fnString.match(/[^(]*\(([^)]*)/);
491
+ if (!match) {
492
+ return memoProps(fn, new Set());
493
+ }
494
+ const args = splitByComma(match[1]);
495
+ if (!args.length) {
496
+ return memoProps(fn, new Set());
497
+ }
498
+ const fixturesArgument = args[fixturesIndex];
499
+ if (!fixturesArgument) {
500
+ return memoProps(fn, new Set());
501
+ }
502
+ if (!(fixturesArgument[0] === "{" && fixturesArgument.endsWith("}"))) {
503
+ const ordinalArgument = ordinal(fixturesIndex + 1);
504
+ const error = new FixtureParseError(`The ${ordinalArgument} argument inside a fixture must use object destructuring pattern, e.g. ({ task } => {}). ` + `Instead, received "${fixturesArgument}".` + `${suiteHook ? ` If you used internal "suite" task as the ${ordinalArgument} argument previously, access it in the ${ordinal(fixturesIndex + 2)} argument instead.` : ""}`);
505
+ if (sourceError) {
506
+ error.stack = sourceError.stack?.replace(sourceError.message, error.message);
507
+ }
508
+ throw error;
509
+ }
510
+ const _first = fixturesArgument.slice(1, -1).replace(/\s/g, "");
511
+ const props = splitByComma(_first).map((prop) => {
512
+ return prop.replace(/:.*|=.*/g, "");
513
+ });
514
+ const last = props.at(-1);
515
+ if (last && last.startsWith("...")) {
516
+ const error = new FixtureParseError(`Rest parameters are not supported in fixtures, received "${last}".`);
517
+ if (sourceError) {
518
+ error.stack = sourceError.stack?.replace(sourceError.message, error.message);
519
+ }
520
+ throw error;
521
+ }
522
+ return memoProps(fn, new Set(props));
523
+ }
524
+ function splitByComma(s) {
525
+ const result = [];
526
+ const stack = [];
527
+ let start = 0;
528
+ for (let i = 0; i < s.length; i++) {
529
+ if (s[i] === "{" || s[i] === "[") {
530
+ stack.push(s[i] === "{" ? "}" : "]");
531
+ } else if (s[i] === stack.at(-1)) {
532
+ stack.pop();
533
+ } else if (!stack.length && s[i] === ",") {
534
+ const token = s.substring(start, i).trim();
535
+ if (token) {
536
+ result.push(token);
537
+ }
538
+ start = i + 1;
539
+ }
540
+ }
541
+ const lastToken = s.substring(start).trim();
542
+ if (lastToken) {
543
+ result.push(lastToken);
544
+ }
545
+ return result;
546
+ }
547
+
548
+ let _test;
549
+ function setCurrentTest(test) {
550
+ _test = test;
551
+ }
552
+ function getCurrentTest() {
553
+ return _test;
554
+ }
555
+ const tests = [];
556
+ function addRunningTest(test) {
557
+ tests.push(test);
558
+ return () => {
559
+ tests.splice(tests.indexOf(test));
560
+ };
561
+ }
562
+ function getRunningTests() {
563
+ return tests;
564
+ }
565
+
566
+ function getDefaultHookTimeout() {
567
+ return getRunner().config.hookTimeout;
568
+ }
569
+ const CLEANUP_TIMEOUT_KEY = Symbol.for("VITEST_CLEANUP_TIMEOUT");
570
+ const CLEANUP_STACK_TRACE_KEY = Symbol.for("VITEST_CLEANUP_STACK_TRACE");
571
+ const AROUND_TIMEOUT_KEY = Symbol.for("VITEST_AROUND_TIMEOUT");
572
+ const AROUND_STACK_TRACE_KEY = Symbol.for("VITEST_AROUND_STACK_TRACE");
573
+ function getBeforeHookCleanupCallback(hook, result, context) {
574
+ if (typeof result === "function") {
575
+ const timeout = CLEANUP_TIMEOUT_KEY in hook && typeof hook[CLEANUP_TIMEOUT_KEY] === "number" ? hook[CLEANUP_TIMEOUT_KEY] : getDefaultHookTimeout();
576
+ const stackTraceError = CLEANUP_STACK_TRACE_KEY in hook && hook[CLEANUP_STACK_TRACE_KEY] instanceof Error ? hook[CLEANUP_STACK_TRACE_KEY] : undefined;
577
+ return withTimeout(result, timeout, true, stackTraceError, (_, error) => {
578
+ if (context) {
579
+ abortContextSignal(context, error);
580
+ }
581
+ });
582
+ }
583
+ }
584
+ /**
585
+ * Registers a callback function to be executed once before all tests within the current suite.
586
+ * This hook is useful for scenarios where you need to perform setup operations that are common to all tests in a suite, such as initializing a database connection or setting up a test environment.
587
+ *
588
+ * **Note:** The `beforeAll` hooks are executed in the order they are defined one after another. You can configure this by changing the `sequence.hooks` option in the config file.
589
+ *
590
+ * @param {Function} fn - The callback function to be executed before all tests.
591
+ * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
592
+ * @returns {void}
593
+ * @example
594
+ * ```ts
595
+ * // Example of using beforeAll to set up a database connection
596
+ * beforeAll(async () => {
597
+ * await database.connect();
598
+ * });
599
+ * ```
600
+ */
601
+ function beforeAll(fn, timeout = getDefaultHookTimeout()) {
602
+ assertTypes(fn, "\"beforeAll\" callback", ["function"]);
603
+ const stackTraceError = new Error("STACK_TRACE_ERROR");
604
+ const context = getChainableContext(this);
605
+ return getCurrentSuite().on("beforeAll", Object.assign(withTimeout(withSuiteFixtures("beforeAll", fn, context, stackTraceError), timeout, true, stackTraceError), {
606
+ [CLEANUP_TIMEOUT_KEY]: timeout,
607
+ [CLEANUP_STACK_TRACE_KEY]: stackTraceError
608
+ }));
609
+ }
610
+ /**
611
+ * Registers a callback function to be executed once after all tests within the current suite have completed.
612
+ * This hook is useful for scenarios where you need to perform cleanup operations after all tests in a suite have run, such as closing database connections or cleaning up temporary files.
613
+ *
614
+ * **Note:** The `afterAll` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
615
+ *
616
+ * @param {Function} fn - The callback function to be executed after all tests.
617
+ * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
618
+ * @returns {void}
619
+ * @example
620
+ * ```ts
621
+ * // Example of using afterAll to close a database connection
622
+ * afterAll(async () => {
623
+ * await database.disconnect();
624
+ * });
625
+ * ```
626
+ */
627
+ function afterAll(fn, timeout) {
628
+ assertTypes(fn, "\"afterAll\" callback", ["function"]);
629
+ const context = getChainableContext(this);
630
+ const stackTraceError = new Error("STACK_TRACE_ERROR");
631
+ return getCurrentSuite().on("afterAll", withTimeout(withSuiteFixtures("afterAll", fn, context, stackTraceError), timeout ?? getDefaultHookTimeout(), true, stackTraceError));
632
+ }
633
+ /**
634
+ * Registers a callback function to be executed before each test within the current suite.
635
+ * This hook is useful for scenarios where you need to reset or reinitialize the test environment before each test runs, such as resetting database states, clearing caches, or reinitializing variables.
636
+ *
637
+ * **Note:** The `beforeEach` hooks are executed in the order they are defined one after another. You can configure this by changing the `sequence.hooks` option in the config file.
638
+ *
639
+ * @param {Function} fn - The callback function to be executed before each test. This function receives an `TestContext` parameter if additional test context is needed.
640
+ * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
641
+ * @returns {void}
642
+ * @example
643
+ * ```ts
644
+ * // Example of using beforeEach to reset a database state
645
+ * beforeEach(async () => {
646
+ * await database.reset();
647
+ * });
648
+ * ```
649
+ */
650
+ function beforeEach(fn, timeout = getDefaultHookTimeout()) {
651
+ assertTypes(fn, "\"beforeEach\" callback", ["function"]);
652
+ const stackTraceError = new Error("STACK_TRACE_ERROR");
653
+ const wrapper = (context, suite) => {
654
+ const fixtureResolver = withFixtures(fn, { suite });
655
+ return fixtureResolver(context);
656
+ };
657
+ return getCurrentSuite().on("beforeEach", Object.assign(withTimeout(wrapper, timeout ?? getDefaultHookTimeout(), true, stackTraceError, abortIfTimeout), {
658
+ [CLEANUP_TIMEOUT_KEY]: timeout,
659
+ [CLEANUP_STACK_TRACE_KEY]: stackTraceError
660
+ }));
661
+ }
662
+ /**
663
+ * Registers a callback function to be executed after each test within the current suite has completed.
664
+ * This hook is useful for scenarios where you need to clean up or reset the test environment after each test runs, such as deleting temporary files, clearing test-specific database entries, or resetting mocked functions.
665
+ *
666
+ * **Note:** The `afterEach` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
667
+ *
668
+ * @param {Function} fn - The callback function to be executed after each test. This function receives an `TestContext` parameter if additional test context is needed.
669
+ * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
670
+ * @returns {void}
671
+ * @example
672
+ * ```ts
673
+ * // Example of using afterEach to delete temporary files created during a test
674
+ * afterEach(async () => {
675
+ * await fileSystem.deleteTempFiles();
676
+ * });
677
+ * ```
678
+ */
679
+ function afterEach(fn, timeout) {
680
+ assertTypes(fn, "\"afterEach\" callback", ["function"]);
681
+ const wrapper = (context, suite) => {
682
+ const fixtureResolver = withFixtures(fn, { suite });
683
+ return fixtureResolver(context);
684
+ };
685
+ return getCurrentSuite().on("afterEach", withTimeout(wrapper, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
686
+ }
687
+ /**
688
+ * Registers a callback function to be executed when a test fails within the current suite.
689
+ * This function allows for custom actions to be performed in response to test failures, such as logging, cleanup, or additional diagnostics.
690
+ *
691
+ * **Note:** The `onTestFailed` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
692
+ *
693
+ * @param {Function} fn - The callback function to be executed upon a test failure. The function receives the test result (including errors).
694
+ * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
695
+ * @throws {Error} Throws an error if the function is not called within a test.
696
+ * @returns {void}
697
+ * @example
698
+ * ```ts
699
+ * // Example of using onTestFailed to log failure details
700
+ * onTestFailed(({ errors }) => {
701
+ * console.log(`Test failed: ${test.name}`, errors);
702
+ * });
703
+ * ```
704
+ */
705
+ const onTestFailed = createTestHook("onTestFailed", (test, handler, timeout) => {
706
+ test.onFailed ||= [];
707
+ test.onFailed.push(withTimeout(handler, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
708
+ });
709
+ /**
710
+ * Registers a callback function to be executed when the current test finishes, regardless of the outcome (pass or fail).
711
+ * This function is ideal for performing actions that should occur after every test execution, such as cleanup, logging, or resetting shared resources.
712
+ *
713
+ * This hook is useful if you have access to a resource in the test itself and you want to clean it up after the test finishes. It is a more compact way to clean up resources than using the combination of `beforeEach` and `afterEach`.
714
+ *
715
+ * **Note:** The `onTestFinished` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
716
+ *
717
+ * **Note:** The `onTestFinished` hook is not called if the test is canceled with a dynamic `ctx.skip()` call.
718
+ *
719
+ * @param {Function} fn - The callback function to be executed after a test finishes. The function can receive parameters providing details about the completed test, including its success or failure status.
720
+ * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
721
+ * @throws {Error} Throws an error if the function is not called within a test.
722
+ * @returns {void}
723
+ * @example
724
+ * ```ts
725
+ * // Example of using onTestFinished for cleanup
726
+ * const db = await connectToDatabase();
727
+ * onTestFinished(async () => {
728
+ * await db.disconnect();
729
+ * });
730
+ * ```
731
+ */
732
+ const onTestFinished = createTestHook("onTestFinished", (test, handler, timeout) => {
733
+ test.onFinished ||= [];
734
+ test.onFinished.push(withTimeout(handler, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
735
+ });
736
+ /**
737
+ * Registers a callback function that wraps around all tests within the current suite.
738
+ * The callback receives a `runSuite` function that must be called to run the suite's tests.
739
+ * This hook is useful for scenarios where you need to wrap an entire suite in a context
740
+ * (e.g., starting a server, opening a database connection that all tests share).
741
+ *
742
+ * **Note:** When multiple `aroundAll` hooks are registered, they are nested inside each other.
743
+ * The first registered hook is the outermost wrapper.
744
+ *
745
+ * @param {Function} fn - The callback function that wraps the suite. Must call `runSuite()` to run the tests.
746
+ * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
747
+ * @returns {void}
748
+ * @example
749
+ * ```ts
750
+ * // Example of using aroundAll to wrap suite in a tracing span
751
+ * aroundAll(async (runSuite) => {
752
+ * await tracer.trace('test-suite', runSuite);
753
+ * });
754
+ * ```
755
+ * @example
756
+ * ```ts
757
+ * // Example of using aroundAll with fixtures
758
+ * aroundAll(async (runSuite, { db }) => {
759
+ * await db.transaction(() => runSuite());
760
+ * });
761
+ * ```
762
+ */
763
+ function aroundAll(fn, timeout) {
764
+ assertTypes(fn, "\"aroundAll\" callback", ["function"]);
765
+ const stackTraceError = new Error("STACK_TRACE_ERROR");
766
+ const resolvedTimeout = timeout ?? getDefaultHookTimeout();
767
+ const context = getChainableContext(this);
768
+ return getCurrentSuite().on("aroundAll", Object.assign(withSuiteFixtures("aroundAll", fn, context, stackTraceError, 1), {
769
+ [AROUND_TIMEOUT_KEY]: resolvedTimeout,
770
+ [AROUND_STACK_TRACE_KEY]: stackTraceError
771
+ }));
772
+ }
773
+ /**
774
+ * Registers a callback function that wraps around each test within the current suite.
775
+ * The callback receives a `runTest` function that must be called to run the test.
776
+ * This hook is useful for scenarios where you need to wrap tests in a context (e.g., database transactions).
777
+ *
778
+ * **Note:** When multiple `aroundEach` hooks are registered, they are nested inside each other.
779
+ * The first registered hook is the outermost wrapper.
780
+ *
781
+ * @param {Function} fn - The callback function that wraps the test. Must call `runTest()` to run the test.
782
+ * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
783
+ * @returns {void}
784
+ * @example
785
+ * ```ts
786
+ * // Example of using aroundEach to wrap tests in a database transaction
787
+ * aroundEach(async (runTest) => {
788
+ * await database.transaction(() => runTest());
789
+ * });
790
+ * ```
791
+ * @example
792
+ * ```ts
793
+ * // Example of using aroundEach with fixtures
794
+ * aroundEach(async (runTest, { db }) => {
795
+ * await db.transaction(() => runTest());
796
+ * });
797
+ * ```
798
+ */
799
+ function aroundEach(fn, timeout) {
800
+ assertTypes(fn, "\"aroundEach\" callback", ["function"]);
801
+ const stackTraceError = new Error("STACK_TRACE_ERROR");
802
+ const resolvedTimeout = timeout ?? getDefaultHookTimeout();
803
+ const wrapper = (runTest, context, suite) => {
804
+ const innerFn = (ctx) => fn(runTest, ctx, suite);
805
+ configureProps(innerFn, {
806
+ index: 1,
807
+ original: fn
808
+ });
809
+ const fixtureResolver = withFixtures(innerFn, { suite });
810
+ return fixtureResolver(context);
811
+ };
812
+ return getCurrentSuite().on("aroundEach", Object.assign(wrapper, {
813
+ [AROUND_TIMEOUT_KEY]: resolvedTimeout,
814
+ [AROUND_STACK_TRACE_KEY]: stackTraceError
815
+ }));
816
+ }
817
+ function withSuiteFixtures(suiteHook, fn, context, stackTraceError, contextIndex = 0) {
818
+ return (...args) => {
819
+ const suite = args.at(-1);
820
+ const prefix = args.slice(0, -1);
821
+ const wrapper = (ctx) => fn(...prefix, ctx, suite);
822
+ configureProps(wrapper, {
823
+ index: contextIndex,
824
+ original: fn
825
+ });
826
+ const fixtures = context?.getFixtures();
827
+ const fileContext = fixtures?.getFileContext(suite.file);
828
+ const fixtured = withFixtures(wrapper, {
829
+ suiteHook,
830
+ fixtures,
831
+ context: fileContext,
832
+ stackTraceError
833
+ });
834
+ return fixtured();
835
+ };
836
+ }
837
+ function getAroundHookTimeout(hook) {
838
+ return AROUND_TIMEOUT_KEY in hook && typeof hook[AROUND_TIMEOUT_KEY] === "number" ? hook[AROUND_TIMEOUT_KEY] : getDefaultHookTimeout();
839
+ }
840
+ function getAroundHookStackTrace(hook) {
841
+ return AROUND_STACK_TRACE_KEY in hook && hook[AROUND_STACK_TRACE_KEY] instanceof Error ? hook[AROUND_STACK_TRACE_KEY] : undefined;
842
+ }
843
+ function createTestHook(name, handler) {
844
+ return (fn, timeout) => {
845
+ assertTypes(fn, `"${name}" callback`, ["function"]);
846
+ const current = getCurrentTest();
847
+ if (!current) {
848
+ throw new Error(`Hook ${name}() can only be called inside a test`);
849
+ }
850
+ return handler(current, fn, timeout);
851
+ };
852
+ }
853
+
854
+ /**
855
+ * Creates a suite of tests, allowing for grouping and hierarchical organization of tests.
856
+ * Suites can contain both tests and other suites, enabling complex test structures.
857
+ *
858
+ * @param {string} name - The name of the suite, used for identification and reporting.
859
+ * @param {Function} fn - A function that defines the tests and suites within this suite.
860
+ * @example
861
+ * ```ts
862
+ * // Define a suite with two tests
863
+ * suite('Math operations', () => {
864
+ * test('should add two numbers', () => {
865
+ * expect(add(1, 2)).toBe(3);
866
+ * });
867
+ *
868
+ * test('should subtract two numbers', () => {
869
+ * expect(subtract(5, 2)).toBe(3);
870
+ * });
871
+ * });
872
+ * ```
873
+ * @example
874
+ * ```ts
875
+ * // Define nested suites
876
+ * suite('String operations', () => {
877
+ * suite('Trimming', () => {
878
+ * test('should trim whitespace from start and end', () => {
879
+ * expect(' hello '.trim()).toBe('hello');
880
+ * });
881
+ * });
882
+ *
883
+ * suite('Concatenation', () => {
884
+ * test('should concatenate two strings', () => {
885
+ * expect('hello' + ' ' + 'world').toBe('hello world');
886
+ * });
887
+ * });
888
+ * });
889
+ * ```
890
+ */
891
+ const suite = createSuite();
892
+ /**
893
+ * Defines a test case with a given name and test function. The test function can optionally be configured with test options.
894
+ *
895
+ * @param {string | Function} name - The name of the test or a function that will be used as a test name.
896
+ * @param {TestOptions | TestFunction} [optionsOrFn] - Optional. The test options or the test function if no explicit name is provided.
897
+ * @param {number | TestOptions | TestFunction} [optionsOrTest] - Optional. The test function or options, depending on the previous parameters.
898
+ * @throws {Error} If called inside another test function.
899
+ * @example
900
+ * ```ts
901
+ * // Define a simple test
902
+ * test('should add two numbers', () => {
903
+ * expect(add(1, 2)).toBe(3);
904
+ * });
905
+ * ```
906
+ * @example
907
+ * ```ts
908
+ * // Define a test with options
909
+ * test('should subtract two numbers', { retry: 3 }, () => {
910
+ * expect(subtract(5, 2)).toBe(3);
911
+ * });
912
+ * ```
913
+ */
914
+ const test = createTest(function(name, optionsOrFn, optionsOrTest) {
915
+ if (getCurrentTest()) {
916
+ throw new Error("Calling the test function inside another test function is not allowed. Please put it inside \"describe\" or \"suite\" so it can be properly collected.");
917
+ }
918
+ getCurrentSuite().test.fn.call(this, formatName(name), optionsOrFn, optionsOrTest);
919
+ });
920
+ /**
921
+ * Creates a suite of tests, allowing for grouping and hierarchical organization of tests.
922
+ * Suites can contain both tests and other suites, enabling complex test structures.
923
+ *
924
+ * @param {string} name - The name of the suite, used for identification and reporting.
925
+ * @param {Function} fn - A function that defines the tests and suites within this suite.
926
+ * @example
927
+ * ```ts
928
+ * // Define a suite with two tests
929
+ * describe('Math operations', () => {
930
+ * test('should add two numbers', () => {
931
+ * expect(add(1, 2)).toBe(3);
932
+ * });
933
+ *
934
+ * test('should subtract two numbers', () => {
935
+ * expect(subtract(5, 2)).toBe(3);
936
+ * });
937
+ * });
938
+ * ```
939
+ * @example
940
+ * ```ts
941
+ * // Define nested suites
942
+ * describe('String operations', () => {
943
+ * describe('Trimming', () => {
944
+ * test('should trim whitespace from start and end', () => {
945
+ * expect(' hello '.trim()).toBe('hello');
946
+ * });
947
+ * });
948
+ *
949
+ * describe('Concatenation', () => {
950
+ * test('should concatenate two strings', () => {
951
+ * expect('hello' + ' ' + 'world').toBe('hello world');
952
+ * });
953
+ * });
954
+ * });
955
+ * ```
956
+ */
957
+ const describe = suite;
958
+ /**
959
+ * Defines a test case with a given name and test function. The test function can optionally be configured with test options.
960
+ *
961
+ * @param {string | Function} name - The name of the test or a function that will be used as a test name.
962
+ * @param {TestOptions | TestFunction} [optionsOrFn] - Optional. The test options or the test function if no explicit name is provided.
963
+ * @param {number | TestOptions | TestFunction} [optionsOrTest] - Optional. The test function or options, depending on the previous parameters.
964
+ * @throws {Error} If called inside another test function.
965
+ * @example
966
+ * ```ts
967
+ * // Define a simple test
968
+ * it('adds two numbers', () => {
969
+ * expect(add(1, 2)).toBe(3);
970
+ * });
971
+ * ```
972
+ * @example
973
+ * ```ts
974
+ * // Define a test with options
975
+ * it('subtracts two numbers', { retry: 3 }, () => {
976
+ * expect(subtract(5, 2)).toBe(3);
977
+ * });
978
+ * ```
979
+ */
980
+ const it = test;
981
+ let runner;
982
+ let defaultSuite;
983
+ let currentTestFilepath;
984
+ function assert(condition, message) {
985
+ if (!condition) {
986
+ throw new Error(`Vitest failed to find ${message}. One of the following is possible:` + "\n- \"vitest\" is imported directly without running \"vitest\" command" + "\n- \"vitest\" is imported inside \"globalSetup\" (to fix this, use \"setupFiles\" instead, because \"globalSetup\" runs in a different context)" + "\n- \"vitest\" is imported inside Vite / Vitest config file" + "\n- Otherwise, it might be a Vitest bug. Please report it to https://github.com/vitest-dev/vitest/issues\n");
987
+ }
988
+ }
989
+ function getDefaultSuite() {
990
+ assert(defaultSuite, "the default suite");
991
+ return defaultSuite;
992
+ }
993
+ function getRunner() {
994
+ assert(runner, "the runner");
995
+ return runner;
996
+ }
997
+ function createDefaultSuite(runner) {
998
+ const config = runner.config.sequence;
999
+ const options = {};
1000
+ if (config.concurrent != null) {
1001
+ options.concurrent = config.concurrent;
1002
+ }
1003
+ const collector = suite("", options, () => {});
1004
+ // no parent suite for top-level tests
1005
+ delete collector.suite;
1006
+ return collector;
1007
+ }
1008
+ function clearCollectorContext(file, currentRunner) {
1009
+ currentTestFilepath = file.filepath;
1010
+ runner = currentRunner;
1011
+ if (!defaultSuite) {
1012
+ defaultSuite = createDefaultSuite(currentRunner);
1013
+ }
1014
+ defaultSuite.file = file;
1015
+ collectorContext.tasks.length = 0;
1016
+ defaultSuite.clear();
1017
+ collectorContext.currentSuite = defaultSuite;
1018
+ }
1019
+ function getCurrentSuite() {
1020
+ const currentSuite = collectorContext.currentSuite || defaultSuite;
1021
+ assert(currentSuite, "the current suite");
1022
+ return currentSuite;
1023
+ }
1024
+ function createSuiteHooks() {
1025
+ return {
1026
+ beforeAll: [],
1027
+ afterAll: [],
1028
+ beforeEach: [],
1029
+ afterEach: [],
1030
+ aroundEach: [],
1031
+ aroundAll: []
1032
+ };
1033
+ }
1034
+ const POSITIVE_INFINITY = Number.POSITIVE_INFINITY;
1035
+ function parseArguments(optionsOrFn, timeoutOrTest) {
1036
+ if (timeoutOrTest != null && typeof timeoutOrTest === "object") {
1037
+ throw new TypeError(`Signature "test(name, fn, { ... })" was deprecated in Vitest 3 and removed in Vitest 4. Please, provide options as a second argument instead.`);
1038
+ }
1039
+ let options = {};
1040
+ let fn;
1041
+ // it('', () => {}, 1000)
1042
+ if (typeof timeoutOrTest === "number") {
1043
+ options = { timeout: timeoutOrTest };
1044
+ } else if (typeof optionsOrFn === "object") {
1045
+ options = optionsOrFn;
1046
+ }
1047
+ if (typeof optionsOrFn === "function") {
1048
+ if (typeof timeoutOrTest === "function") {
1049
+ throw new TypeError("Cannot use two functions as arguments. Please use the second argument for options.");
1050
+ }
1051
+ fn = optionsOrFn;
1052
+ } else if (typeof timeoutOrTest === "function") {
1053
+ fn = timeoutOrTest;
1054
+ }
1055
+ return {
1056
+ options,
1057
+ handler: fn
1058
+ };
1059
+ }
1060
+ // implementations
1061
+ function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions) {
1062
+ const tasks = [];
1063
+ let suite;
1064
+ initSuite(true);
1065
+ const task = function(name = "", options = {}) {
1066
+ const currentSuite = collectorContext.currentSuite?.suite;
1067
+ const parentTask = currentSuite ?? collectorContext.currentSuite?.file;
1068
+ const parentTags = parentTask?.tags || [];
1069
+ const testTags = unique([...parentTags, ...toArray(options.tags)]);
1070
+ const tagsOptions = testTags.map((tag) => {
1071
+ const tagDefinition = runner.config.tags?.find((t) => t.name === tag);
1072
+ if (!tagDefinition && runner.config.strictTags) {
1073
+ throw createNoTagsError(runner.config.tags, tag);
1074
+ }
1075
+ return tagDefinition;
1076
+ }).filter((r) => r != null).sort((tag1, tag2) => (tag2.priority ?? POSITIVE_INFINITY) - (tag1.priority ?? POSITIVE_INFINITY)).reduce((acc, tag) => {
1077
+ const { name, description, priority, meta, ...options } = tag;
1078
+ Object.assign(acc, options);
1079
+ if (meta) {
1080
+ acc.meta = Object.assign(acc.meta ?? Object.create(null), meta);
1081
+ }
1082
+ return acc;
1083
+ }, {});
1084
+ const testOwnMeta = options.meta;
1085
+ options = {
1086
+ ...tagsOptions,
1087
+ ...options
1088
+ };
1089
+ const timeout = options.timeout ?? runner.config.testTimeout;
1090
+ const parentMeta = currentSuite?.meta;
1091
+ const tagMeta = tagsOptions.meta;
1092
+ const testMeta = Object.create(null);
1093
+ if (tagMeta) {
1094
+ Object.assign(testMeta, tagMeta);
1095
+ }
1096
+ if (parentMeta) {
1097
+ Object.assign(testMeta, parentMeta);
1098
+ }
1099
+ if (testOwnMeta) {
1100
+ Object.assign(testMeta, testOwnMeta);
1101
+ }
1102
+ const task = {
1103
+ id: "",
1104
+ name,
1105
+ fullName: createTaskName([currentSuite?.fullName ?? collectorContext.currentSuite?.file?.fullName, name]),
1106
+ fullTestName: createTaskName([currentSuite?.fullTestName, name]),
1107
+ suite: currentSuite,
1108
+ each: options.each,
1109
+ fails: options.fails,
1110
+ context: undefined,
1111
+ type: "test",
1112
+ file: currentSuite?.file ?? collectorContext.currentSuite?.file,
1113
+ timeout,
1114
+ retry: options.retry ?? runner.config.retry,
1115
+ repeats: options.repeats,
1116
+ mode: options.only ? "only" : options.skip ? "skip" : options.todo ? "todo" : "run",
1117
+ meta: testMeta,
1118
+ annotations: [],
1119
+ artifacts: [],
1120
+ tags: testTags
1121
+ };
1122
+ const handler = options.handler;
1123
+ if (task.mode === "run" && !handler) {
1124
+ task.mode = "todo";
1125
+ }
1126
+ if (options.concurrent || !options.sequential && runner.config.sequence.concurrent) {
1127
+ task.concurrent = true;
1128
+ }
1129
+ task.shuffle = suiteOptions?.shuffle;
1130
+ const context = createTestContext(task, runner);
1131
+ // create test context
1132
+ Object.defineProperty(task, "context", {
1133
+ value: context,
1134
+ enumerable: false
1135
+ });
1136
+ setTestFixture(context, options.fixtures ?? new TestFixtures());
1137
+ const limit = Error.stackTraceLimit;
1138
+ Error.stackTraceLimit = 10;
1139
+ const stackTraceError = new Error("STACK_TRACE_ERROR");
1140
+ Error.stackTraceLimit = limit;
1141
+ if (handler) {
1142
+ setFn(task, withTimeout(withCancel(withAwaitAsyncAssertions(withFixtures(handler, { context }), task), task.context.signal), timeout, false, stackTraceError, (_, error) => abortIfTimeout([context], error)));
1143
+ }
1144
+ if (runner.config.includeTaskLocation) {
1145
+ const error = stackTraceError.stack;
1146
+ const stack = findTestFileStackTrace(currentTestFilepath, error);
1147
+ if (stack) {
1148
+ task.location = {
1149
+ line: stack.line,
1150
+ column: stack.column
1151
+ };
1152
+ }
1153
+ }
1154
+ tasks.push(task);
1155
+ return task;
1156
+ };
1157
+ const test = createTest(function(name, optionsOrFn, timeoutOrTest) {
1158
+ let { options, handler } = parseArguments(optionsOrFn, timeoutOrTest);
1159
+ // inherit repeats, retry, timeout from suite
1160
+ if (typeof suiteOptions === "object") {
1161
+ options = Object.assign({}, suiteOptions, options);
1162
+ }
1163
+ // inherit concurrent / sequential from suite
1164
+ const concurrent = this.concurrent ?? (!this.sequential && options?.concurrent);
1165
+ if (options.concurrent != null && concurrent != null) {
1166
+ options.concurrent = concurrent;
1167
+ }
1168
+ const sequential = this.sequential ?? (!this.concurrent && options?.sequential);
1169
+ if (options.sequential != null && sequential != null) {
1170
+ options.sequential = sequential;
1171
+ }
1172
+ const test = task(formatName(name), {
1173
+ ...this,
1174
+ ...options,
1175
+ handler
1176
+ });
1177
+ test.type = "test";
1178
+ });
1179
+ const collector = {
1180
+ type: "collector",
1181
+ name,
1182
+ mode,
1183
+ suite,
1184
+ options: suiteOptions,
1185
+ test,
1186
+ file: suite.file,
1187
+ tasks,
1188
+ collect,
1189
+ task,
1190
+ clear,
1191
+ on: addHook
1192
+ };
1193
+ function addHook(name, ...fn) {
1194
+ getHooks(suite)[name].push(...fn);
1195
+ }
1196
+ function initSuite(includeLocation) {
1197
+ if (typeof suiteOptions === "number") {
1198
+ suiteOptions = { timeout: suiteOptions };
1199
+ }
1200
+ const currentSuite = collectorContext.currentSuite?.suite;
1201
+ const parentTask = currentSuite ?? collectorContext.currentSuite?.file;
1202
+ const suiteTags = toArray(suiteOptions?.tags);
1203
+ validateTags(runner.config, suiteTags);
1204
+ suite = {
1205
+ id: "",
1206
+ type: "suite",
1207
+ name,
1208
+ fullName: createTaskName([currentSuite?.fullName ?? collectorContext.currentSuite?.file?.fullName, name]),
1209
+ fullTestName: createTaskName([currentSuite?.fullTestName, name]),
1210
+ suite: currentSuite,
1211
+ mode,
1212
+ each,
1213
+ file: currentSuite?.file ?? collectorContext.currentSuite?.file,
1214
+ shuffle: suiteOptions?.shuffle,
1215
+ tasks: [],
1216
+ meta: suiteOptions?.meta ?? Object.create(null),
1217
+ concurrent: suiteOptions?.concurrent,
1218
+ tags: unique([...parentTask?.tags || [], ...suiteTags])
1219
+ };
1220
+ if (runner && includeLocation && runner.config.includeTaskLocation) {
1221
+ const limit = Error.stackTraceLimit;
1222
+ Error.stackTraceLimit = 15;
1223
+ const error = new Error("stacktrace").stack;
1224
+ Error.stackTraceLimit = limit;
1225
+ const stack = findTestFileStackTrace(currentTestFilepath, error);
1226
+ if (stack) {
1227
+ suite.location = {
1228
+ line: stack.line,
1229
+ column: stack.column
1230
+ };
1231
+ }
1232
+ }
1233
+ setHooks(suite, createSuiteHooks());
1234
+ }
1235
+ function clear() {
1236
+ tasks.length = 0;
1237
+ initSuite(false);
1238
+ }
1239
+ async function collect(file) {
1240
+ if (!file) {
1241
+ throw new TypeError("File is required to collect tasks.");
1242
+ }
1243
+ if (factory) {
1244
+ await runWithSuite(collector, () => factory(test));
1245
+ }
1246
+ const allChildren = [];
1247
+ for (const i of tasks) {
1248
+ allChildren.push(i.type === "collector" ? await i.collect(file) : i);
1249
+ }
1250
+ suite.tasks = allChildren;
1251
+ return suite;
1252
+ }
1253
+ collectTask(collector);
1254
+ return collector;
1255
+ }
1256
+ function withAwaitAsyncAssertions(fn, task) {
1257
+ return (async (...args) => {
1258
+ const fnResult = await fn(...args);
1259
+ // some async expect will be added to this array, in case user forget to await them
1260
+ if (task.promises) {
1261
+ const result = await Promise.allSettled(task.promises);
1262
+ const errors = result.map((r) => r.status === "rejected" ? r.reason : undefined).filter(Boolean);
1263
+ if (errors.length) {
1264
+ throw errors;
1265
+ }
1266
+ }
1267
+ return fnResult;
1268
+ });
1269
+ }
1270
+ function createSuite() {
1271
+ function suiteFn(name, factoryOrOptions, optionsOrFactory) {
1272
+ if (getCurrentTest()) {
1273
+ 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.");
1274
+ }
1275
+ const currentSuite = collectorContext.currentSuite || defaultSuite;
1276
+ let { options, handler: factory } = parseArguments(factoryOrOptions, optionsOrFactory);
1277
+ const isConcurrentSpecified = options.concurrent || this.concurrent || options.sequential === false;
1278
+ const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false;
1279
+ const { meta: parentMeta, ...parentOptions } = currentSuite?.options || {};
1280
+ // inherit options from current suite
1281
+ options = {
1282
+ ...parentOptions,
1283
+ ...options
1284
+ };
1285
+ const shuffle = this.shuffle ?? options.shuffle ?? currentSuite?.options?.shuffle ?? runner?.config.sequence.shuffle;
1286
+ if (shuffle != null) {
1287
+ options.shuffle = shuffle;
1288
+ }
1289
+ let mode = this.only ?? options.only ? "only" : this.skip ?? options.skip ? "skip" : this.todo ?? options.todo ? "todo" : "run";
1290
+ // passed as test(name), assume it's a "todo"
1291
+ if (mode === "run" && !factory) {
1292
+ mode = "todo";
1293
+ }
1294
+ // inherit concurrent / sequential from suite
1295
+ const isConcurrent = isConcurrentSpecified || options.concurrent && !isSequentialSpecified;
1296
+ const isSequential = isSequentialSpecified || options.sequential && !isConcurrentSpecified;
1297
+ if (isConcurrent != null) {
1298
+ options.concurrent = isConcurrent && !isSequential;
1299
+ }
1300
+ if (isSequential != null) {
1301
+ options.sequential = isSequential && !isConcurrent;
1302
+ }
1303
+ if (parentMeta) {
1304
+ options.meta = Object.assign(Object.create(null), parentMeta, options.meta);
1305
+ }
1306
+ return createSuiteCollector(formatName(name), factory, mode, this.each, options);
1307
+ }
1308
+ suiteFn.each = function(cases, ...args) {
1309
+ const context = getChainableContext(this);
1310
+ const suite = context.withContext();
1311
+ context.setContext("each", true);
1312
+ if (Array.isArray(cases) && args.length) {
1313
+ cases = formatTemplateString(cases, args);
1314
+ }
1315
+ return (name, optionsOrFn, fnOrOptions) => {
1316
+ const _name = formatName(name);
1317
+ const arrayOnlyCases = cases.every(Array.isArray);
1318
+ const { options, handler } = parseArguments(optionsOrFn, fnOrOptions);
1319
+ const fnFirst = typeof optionsOrFn === "function";
1320
+ cases.forEach((i, idx) => {
1321
+ const items = Array.isArray(i) ? i : [i];
1322
+ if (fnFirst) {
1323
+ if (arrayOnlyCases) {
1324
+ suite(formatTitle(_name, items, idx), handler ? () => handler(...items) : undefined, options.timeout);
1325
+ } else {
1326
+ suite(formatTitle(_name, items, idx), handler ? () => handler(i) : undefined, options.timeout);
1327
+ }
1328
+ } else {
1329
+ if (arrayOnlyCases) {
1330
+ suite(formatTitle(_name, items, idx), options, handler ? () => handler(...items) : undefined);
1331
+ } else {
1332
+ suite(formatTitle(_name, items, idx), options, handler ? () => handler(i) : undefined);
1333
+ }
1334
+ }
1335
+ });
1336
+ context.setContext("each", undefined);
1337
+ };
1338
+ };
1339
+ suiteFn.for = function(cases, ...args) {
1340
+ if (Array.isArray(cases) && args.length) {
1341
+ cases = formatTemplateString(cases, args);
1342
+ }
1343
+ return (name, optionsOrFn, fnOrOptions) => {
1344
+ const name_ = formatName(name);
1345
+ const { options, handler } = parseArguments(optionsOrFn, fnOrOptions);
1346
+ cases.forEach((item, idx) => {
1347
+ suite(formatTitle(name_, toArray(item), idx), options, handler ? () => handler(item) : undefined);
1348
+ });
1349
+ };
1350
+ };
1351
+ suiteFn.skipIf = (condition) => condition ? suite.skip : suite;
1352
+ suiteFn.runIf = (condition) => condition ? suite : suite.skip;
1353
+ return createChainable([
1354
+ "concurrent",
1355
+ "sequential",
1356
+ "shuffle",
1357
+ "skip",
1358
+ "only",
1359
+ "todo"
1360
+ ], suiteFn);
1361
+ }
1362
+ function createTaskCollector(fn) {
1363
+ const taskFn = fn;
1364
+ taskFn.each = function(cases, ...args) {
1365
+ const context = getChainableContext(this);
1366
+ const test = context.withContext();
1367
+ context.setContext("each", true);
1368
+ if (Array.isArray(cases) && args.length) {
1369
+ cases = formatTemplateString(cases, args);
1370
+ }
1371
+ return (name, optionsOrFn, fnOrOptions) => {
1372
+ const _name = formatName(name);
1373
+ const arrayOnlyCases = cases.every(Array.isArray);
1374
+ const { options, handler } = parseArguments(optionsOrFn, fnOrOptions);
1375
+ const fnFirst = typeof optionsOrFn === "function";
1376
+ cases.forEach((i, idx) => {
1377
+ const items = Array.isArray(i) ? i : [i];
1378
+ if (fnFirst) {
1379
+ if (arrayOnlyCases) {
1380
+ test(formatTitle(_name, items, idx), handler ? () => handler(...items) : undefined, options.timeout);
1381
+ } else {
1382
+ test(formatTitle(_name, items, idx), handler ? () => handler(i) : undefined, options.timeout);
1383
+ }
1384
+ } else {
1385
+ if (arrayOnlyCases) {
1386
+ test(formatTitle(_name, items, idx), options, handler ? () => handler(...items) : undefined);
1387
+ } else {
1388
+ test(formatTitle(_name, items, idx), options, handler ? () => handler(i) : undefined);
1389
+ }
1390
+ }
1391
+ });
1392
+ context.setContext("each", undefined);
1393
+ };
1394
+ };
1395
+ taskFn.for = function(cases, ...args) {
1396
+ const context = getChainableContext(this);
1397
+ const test = context.withContext();
1398
+ if (Array.isArray(cases) && args.length) {
1399
+ cases = formatTemplateString(cases, args);
1400
+ }
1401
+ return (name, optionsOrFn, fnOrOptions) => {
1402
+ const _name = formatName(name);
1403
+ const { options, handler } = parseArguments(optionsOrFn, fnOrOptions);
1404
+ cases.forEach((item, idx) => {
1405
+ // monkey-patch handler to allow parsing fixture
1406
+ const handlerWrapper = handler ? (ctx) => handler(item, ctx) : undefined;
1407
+ if (handlerWrapper) {
1408
+ configureProps(handlerWrapper, {
1409
+ index: 1,
1410
+ original: handler
1411
+ });
1412
+ }
1413
+ test(formatTitle(_name, toArray(item), idx), options, handlerWrapper);
1414
+ });
1415
+ };
1416
+ };
1417
+ taskFn.skipIf = function(condition) {
1418
+ return condition ? this.skip : this;
1419
+ };
1420
+ taskFn.runIf = function(condition) {
1421
+ return condition ? this : this.skip;
1422
+ };
1423
+ /**
1424
+ * Parse builder pattern arguments into a fixtures object.
1425
+ * Handles both builder pattern (name, options?, value) and object syntax.
1426
+ */
1427
+ function parseBuilderFixtures(fixturesOrName, optionsOrFn, maybeFn) {
1428
+ // Object syntax: just return as-is
1429
+ if (typeof fixturesOrName !== "string") {
1430
+ return fixturesOrName;
1431
+ }
1432
+ const fixtureName = fixturesOrName;
1433
+ let fixtureOptions;
1434
+ let fixtureValue;
1435
+ if (maybeFn !== undefined) {
1436
+ // (name, options, value) or (name, options, fn)
1437
+ fixtureOptions = optionsOrFn;
1438
+ fixtureValue = maybeFn;
1439
+ } else {
1440
+ // (name, value) or (name, fn)
1441
+ // Check if optionsOrFn looks like fixture options (has scope or auto)
1442
+ if (optionsOrFn !== null && typeof optionsOrFn === "object" && !Array.isArray(optionsOrFn) && ("scope" in optionsOrFn || "auto" in optionsOrFn)) {
1443
+ // (name, options) with no value - treat as empty object fixture
1444
+ fixtureOptions = optionsOrFn;
1445
+ fixtureValue = {};
1446
+ } else {
1447
+ // (name, value) or (name, fn)
1448
+ fixtureOptions = undefined;
1449
+ fixtureValue = optionsOrFn;
1450
+ }
1451
+ }
1452
+ // Function value: wrap with onCleanup pattern
1453
+ if (typeof fixtureValue === "function") {
1454
+ const builderFn = fixtureValue;
1455
+ // Wrap builder pattern function (returns value) to use() pattern
1456
+ const fixture = async (ctx, use) => {
1457
+ let cleanup;
1458
+ const onCleanup = (fn) => {
1459
+ if (cleanup !== undefined) {
1460
+ throw new Error(`onCleanup can only be called once per fixture. ` + `Define separate fixtures if you need multiple cleanup functions.`);
1461
+ }
1462
+ cleanup = fn;
1463
+ };
1464
+ const value = await builderFn(ctx, { onCleanup });
1465
+ await use(value);
1466
+ if (cleanup) {
1467
+ await cleanup();
1468
+ }
1469
+ };
1470
+ configureProps(fixture, { original: builderFn });
1471
+ if (fixtureOptions) {
1472
+ return { [fixtureName]: [fixture, fixtureOptions] };
1473
+ }
1474
+ return { [fixtureName]: fixture };
1475
+ }
1476
+ // Non-function value: use directly
1477
+ if (fixtureOptions) {
1478
+ return { [fixtureName]: [fixtureValue, fixtureOptions] };
1479
+ }
1480
+ return { [fixtureName]: fixtureValue };
1481
+ }
1482
+ taskFn.override = function(fixturesOrName, optionsOrFn, maybeFn) {
1483
+ const userFixtures = parseBuilderFixtures(fixturesOrName, optionsOrFn, maybeFn);
1484
+ getChainableContext(this).getFixtures().override(runner, userFixtures);
1485
+ return this;
1486
+ };
1487
+ taskFn.scoped = function(fixtures) {
1488
+ console.warn(`test.scoped() is deprecated and will be removed in future versions. Please use test.override() instead.`);
1489
+ return this.override(fixtures);
1490
+ };
1491
+ taskFn.extend = function(fixturesOrName, optionsOrFn, maybeFn) {
1492
+ const userFixtures = parseBuilderFixtures(fixturesOrName, optionsOrFn, maybeFn);
1493
+ const fixtures = getChainableContext(this).getFixtures().extend(runner, userFixtures);
1494
+ const _test = createTest(function(name, optionsOrFn, optionsOrTest) {
1495
+ fn.call(this, formatName(name), optionsOrFn, optionsOrTest);
1496
+ });
1497
+ getChainableContext(_test).mergeContext({ fixtures });
1498
+ return _test;
1499
+ };
1500
+ taskFn.describe = suite;
1501
+ taskFn.suite = suite;
1502
+ taskFn.beforeEach = beforeEach;
1503
+ taskFn.afterEach = afterEach;
1504
+ taskFn.beforeAll = beforeAll;
1505
+ taskFn.afterAll = afterAll;
1506
+ taskFn.aroundEach = aroundEach;
1507
+ taskFn.aroundAll = aroundAll;
1508
+ const _test = createChainable([
1509
+ "concurrent",
1510
+ "sequential",
1511
+ "skip",
1512
+ "only",
1513
+ "todo",
1514
+ "fails"
1515
+ ], taskFn, { fixtures: new TestFixtures() });
1516
+ return _test;
1517
+ }
1518
+ function createTest(fn) {
1519
+ return createTaskCollector(fn);
1520
+ }
1521
+ function formatName(name) {
1522
+ return typeof name === "string" ? name : typeof name === "function" ? name.name || "<anonymous>" : String(name);
1523
+ }
1524
+ function formatTitle(template, items, idx) {
1525
+ if (template.includes("%#") || template.includes("%$")) {
1526
+ // '%#' match index of the test case
1527
+ template = template.replace(/%%/g, "__vitest_escaped_%__").replace(/%#/g, `${idx}`).replace(/%\$/g, `${idx + 1}`).replace(/__vitest_escaped_%__/g, "%%");
1528
+ }
1529
+ const count = template.split("%").length - 1;
1530
+ if (template.includes("%f")) {
1531
+ const placeholders = template.match(/%f/g) || [];
1532
+ placeholders.forEach((_, i) => {
1533
+ if (isNegativeNaN(items[i]) || Object.is(items[i], -0)) {
1534
+ // Replace the i-th occurrence of '%f' with '-%f'
1535
+ let occurrence = 0;
1536
+ template = template.replace(/%f/g, (match) => {
1537
+ occurrence++;
1538
+ return occurrence === i + 1 ? "-%f" : match;
1539
+ });
1540
+ }
1541
+ });
1542
+ }
1543
+ const isObjectItem = isObject(items[0]);
1544
+ function formatAttribute(s) {
1545
+ return s.replace(/\$([$\w.]+)/g, (_, key) => {
1546
+ const isArrayKey = /^\d+$/.test(key);
1547
+ if (!isObjectItem && !isArrayKey) {
1548
+ return `$${key}`;
1549
+ }
1550
+ const arrayElement = isArrayKey ? objectAttr(items, key) : undefined;
1551
+ const value = isObjectItem ? objectAttr(items[0], key, arrayElement) : arrayElement;
1552
+ return objDisplay(value, { truncate: runner?.config?.chaiConfig?.truncateThreshold });
1553
+ });
1554
+ }
1555
+ let output = "";
1556
+ let i = 0;
1557
+ handleRegexMatch(
1558
+ template,
1559
+ formatRegExp,
1560
+ // format "%"
1561
+ (match) => {
1562
+ if (i < count) {
1563
+ output += format(match[0], items[i++]);
1564
+ } else {
1565
+ output += match[0];
1566
+ }
1567
+ },
1568
+ // format "$"
1569
+ (nonMatch) => {
1570
+ output += formatAttribute(nonMatch);
1571
+ }
1572
+ );
1573
+ return output;
1574
+ }
1575
+ // based on https://github.com/unocss/unocss/blob/2e74b31625bbe3b9c8351570749aa2d3f799d919/packages/autocomplete/src/parse.ts#L11
1576
+ function handleRegexMatch(input, regex, onMatch, onNonMatch) {
1577
+ let lastIndex = 0;
1578
+ for (const m of input.matchAll(regex)) {
1579
+ if (lastIndex < m.index) {
1580
+ onNonMatch(input.slice(lastIndex, m.index));
1581
+ }
1582
+ onMatch(m);
1583
+ lastIndex = m.index + m[0].length;
1584
+ }
1585
+ if (lastIndex < input.length) {
1586
+ onNonMatch(input.slice(lastIndex));
1587
+ }
1588
+ }
1589
+ function formatTemplateString(cases, args) {
1590
+ const header = cases.join("").trim().replace(/ /g, "").split("\n").map((i) => i.split("|"))[0];
1591
+ const res = [];
1592
+ for (let i = 0; i < Math.floor(args.length / header.length); i++) {
1593
+ const oneCase = {};
1594
+ for (let j = 0; j < header.length; j++) {
1595
+ oneCase[header[j]] = args[i * header.length + j];
1596
+ }
1597
+ res.push(oneCase);
1598
+ }
1599
+ return res;
1600
+ }
1601
+
1602
+ const now$2 = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now;
1603
+ const collectorContext = {
1604
+ tasks: [],
1605
+ currentSuite: null
1606
+ };
1607
+ function collectTask(task) {
1608
+ collectorContext.currentSuite?.tasks.push(task);
1609
+ }
1610
+ async function runWithSuite(suite, fn) {
1611
+ const prev = collectorContext.currentSuite;
1612
+ collectorContext.currentSuite = suite;
1613
+ await fn();
1614
+ collectorContext.currentSuite = prev;
1615
+ }
1616
+ function withTimeout(fn, timeout, isHook = false, stackTraceError, onTimeout) {
1617
+ if (timeout <= 0 || timeout === Number.POSITIVE_INFINITY) {
1618
+ return fn;
1619
+ }
1620
+ const { setTimeout, clearTimeout } = getSafeTimers();
1621
+ // this function name is used to filter error in test/cli/test/fails.test.ts
1622
+ return (function runWithTimeout(...args) {
1623
+ const startTime = now$2();
1624
+ const runner = getRunner();
1625
+ runner._currentTaskStartTime = startTime;
1626
+ runner._currentTaskTimeout = timeout;
1627
+ return new Promise((resolve_, reject_) => {
1628
+ const timer = setTimeout(() => {
1629
+ clearTimeout(timer);
1630
+ rejectTimeoutError();
1631
+ }, timeout);
1632
+ // `unref` might not exist in browser
1633
+ timer.unref?.();
1634
+ function rejectTimeoutError() {
1635
+ const error = makeTimeoutError(isHook, timeout, stackTraceError);
1636
+ onTimeout?.(args, error);
1637
+ reject_(error);
1638
+ }
1639
+ function resolve(result) {
1640
+ runner._currentTaskStartTime = undefined;
1641
+ runner._currentTaskTimeout = undefined;
1642
+ clearTimeout(timer);
1643
+ // if test/hook took too long in microtask, setTimeout won't be triggered,
1644
+ // but we still need to fail the test, see
1645
+ // https://github.com/vitest-dev/vitest/issues/2920
1646
+ if (now$2() - startTime >= timeout) {
1647
+ rejectTimeoutError();
1648
+ return;
1649
+ }
1650
+ resolve_(result);
1651
+ }
1652
+ function reject(error) {
1653
+ runner._currentTaskStartTime = undefined;
1654
+ runner._currentTaskTimeout = undefined;
1655
+ clearTimeout(timer);
1656
+ reject_(error);
1657
+ }
1658
+ // sync test/hook will be caught by try/catch
1659
+ try {
1660
+ const result = fn(...args);
1661
+ // the result is a thenable, we don't wrap this in Promise.resolve
1662
+ // to avoid creating new promises
1663
+ if (typeof result === "object" && result != null && typeof result.then === "function") {
1664
+ result.then(resolve, reject);
1665
+ } else {
1666
+ resolve(result);
1667
+ }
1668
+ }
1669
+ // user sync test/hook throws an error
1670
+ catch (error) {
1671
+ reject(error);
1672
+ }
1673
+ });
1674
+ });
1675
+ }
1676
+ function withCancel(fn, signal) {
1677
+ return (function runWithCancel(...args) {
1678
+ return new Promise((resolve, reject) => {
1679
+ signal.addEventListener("abort", () => reject(signal.reason));
1680
+ try {
1681
+ const result = fn(...args);
1682
+ if (typeof result === "object" && result != null && typeof result.then === "function") {
1683
+ result.then(resolve, reject);
1684
+ } else {
1685
+ resolve(result);
1686
+ }
1687
+ } catch (error) {
1688
+ reject(error);
1689
+ }
1690
+ });
1691
+ });
1692
+ }
1693
+ const abortControllers = new WeakMap();
1694
+ function abortIfTimeout([context], error) {
1695
+ if (context) {
1696
+ abortContextSignal(context, error);
1697
+ }
1698
+ }
1699
+ function abortContextSignal(context, error) {
1700
+ const abortController = abortControllers.get(context);
1701
+ abortController?.abort(error);
1702
+ }
1703
+ function createTestContext(test, runner) {
1704
+ const context = function() {
1705
+ throw new Error("done() callback is deprecated, use promise instead");
1706
+ };
1707
+ let abortController = abortControllers.get(context);
1708
+ if (!abortController) {
1709
+ abortController = new AbortController();
1710
+ abortControllers.set(context, abortController);
1711
+ }
1712
+ context.signal = abortController.signal;
1713
+ context.task = test;
1714
+ context.skip = (condition, note) => {
1715
+ if (condition === false) {
1716
+ // do nothing
1717
+ return undefined;
1718
+ }
1719
+ test.result ??= { state: "skip" };
1720
+ test.result.pending = true;
1721
+ throw new PendingError("test is skipped; abort execution", test, typeof condition === "string" ? condition : note);
1722
+ };
1723
+ context.annotate = ((message, type, attachment) => {
1724
+ if (test.result && test.result.state !== "run") {
1725
+ throw new Error(`Cannot annotate tests outside of the test run. The test "${test.name}" finished running with the "${test.result.state}" state already.`);
1726
+ }
1727
+ const annotation = {
1728
+ message,
1729
+ type: typeof type === "object" || type === undefined ? "notice" : type
1730
+ };
1731
+ const annotationAttachment = typeof type === "object" ? type : attachment;
1732
+ if (annotationAttachment) {
1733
+ annotation.attachment = annotationAttachment;
1734
+ manageArtifactAttachment(annotation.attachment);
1735
+ }
1736
+ return recordAsyncOperation(test, recordArtifact(test, {
1737
+ type: "internal:annotation",
1738
+ annotation
1739
+ }).then(async ({ annotation }) => {
1740
+ if (!runner.onTestAnnotate) {
1741
+ throw new Error(`Test runner doesn't support test annotations.`);
1742
+ }
1743
+ await finishSendTasksUpdate(runner);
1744
+ const resolvedAnnotation = await runner.onTestAnnotate(test, annotation);
1745
+ test.annotations.push(resolvedAnnotation);
1746
+ return resolvedAnnotation;
1747
+ }));
1748
+ });
1749
+ context.onTestFailed = (handler, timeout) => {
1750
+ test.onFailed ||= [];
1751
+ test.onFailed.push(withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error("STACK_TRACE_ERROR"), (_, error) => abortController.abort(error)));
1752
+ };
1753
+ context.onTestFinished = (handler, timeout) => {
1754
+ test.onFinished ||= [];
1755
+ test.onFinished.push(withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error("STACK_TRACE_ERROR"), (_, error) => abortController.abort(error)));
1756
+ };
1757
+ return runner.extendTaskContext?.(context) || context;
1758
+ }
1759
+ function makeTimeoutError(isHook, timeout, stackTraceError) {
1760
+ 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"}".`;
1761
+ const error = new Error(message);
1762
+ if (stackTraceError?.stack) {
1763
+ error.stack = stackTraceError.stack.replace(error.message, stackTraceError.message);
1764
+ }
1765
+ return error;
1766
+ }
1767
+
1768
+ async function runSetupFiles(config, files, runner) {
1769
+ if (config.sequence.setupFiles === "parallel") {
1770
+ await Promise.all(files.map(async (fsPath) => {
1771
+ await runner.importFile(fsPath, "setup");
1772
+ }));
1773
+ } else {
1774
+ for (const fsPath of files) {
1775
+ await runner.importFile(fsPath, "setup");
1776
+ }
1777
+ }
1778
+ }
1779
+
1780
+ const now$1 = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now;
1781
+ async function collectTests(specs, runner) {
1782
+ const files = [];
1783
+ const config = runner.config;
1784
+ const $ = runner.trace;
1785
+ let defaultTagsFilter;
1786
+ for (const spec of specs) {
1787
+ const filepath = typeof spec === "string" ? spec : spec.filepath;
1788
+ await $("collect_spec", { "code.file.path": filepath }, async () => {
1789
+ const testLocations = typeof spec === "string" ? undefined : spec.testLocations;
1790
+ const testNamePattern = typeof spec === "string" ? undefined : spec.testNamePattern;
1791
+ const testIds = typeof spec === "string" ? undefined : spec.testIds;
1792
+ const testTagsFilter = typeof spec === "object" && spec.testTagsFilter ? createTagsFilter(spec.testTagsFilter, config.tags) : undefined;
1793
+ const fileTags = typeof spec === "string" ? [] : spec.fileTags || [];
1794
+ const file = createFileTask(filepath, config.root, config.name, runner.pool, runner.viteEnvironment);
1795
+ file.tags = fileTags;
1796
+ file.shuffle = config.sequence.shuffle;
1797
+ try {
1798
+ validateTags(runner.config, fileTags);
1799
+ runner.onCollectStart?.(file);
1800
+ clearCollectorContext(file, runner);
1801
+ const setupFiles = toArray(config.setupFiles);
1802
+ if (setupFiles.length) {
1803
+ const setupStart = now$1();
1804
+ await runSetupFiles(config, setupFiles, runner);
1805
+ const setupEnd = now$1();
1806
+ file.setupDuration = setupEnd - setupStart;
1807
+ } else {
1808
+ file.setupDuration = 0;
1809
+ }
1810
+ const collectStart = now$1();
1811
+ await runner.importFile(filepath, "collect");
1812
+ const durations = runner.getImportDurations?.();
1813
+ if (durations) {
1814
+ file.importDurations = durations;
1815
+ }
1816
+ const defaultTasks = await getDefaultSuite().collect(file);
1817
+ const fileHooks = createSuiteHooks();
1818
+ mergeHooks(fileHooks, getHooks(defaultTasks));
1819
+ for (const c of [...defaultTasks.tasks, ...collectorContext.tasks]) {
1820
+ if (c.type === "test" || c.type === "suite") {
1821
+ file.tasks.push(c);
1822
+ } else if (c.type === "collector") {
1823
+ const suite = await c.collect(file);
1824
+ if (suite.name || suite.tasks.length) {
1825
+ mergeHooks(fileHooks, getHooks(suite));
1826
+ file.tasks.push(suite);
1827
+ }
1828
+ } else {
1829
+ // check that types are exhausted
1830
+ c;
1831
+ }
1832
+ }
1833
+ setHooks(file, fileHooks);
1834
+ file.collectDuration = now$1() - collectStart;
1835
+ } catch (e) {
1836
+ const errors = e instanceof AggregateError ? e.errors.map((e) => processError(e, runner.config.diffOptions)) : [processError(e, runner.config.diffOptions)];
1837
+ file.result = {
1838
+ state: "fail",
1839
+ errors
1840
+ };
1841
+ const durations = runner.getImportDurations?.();
1842
+ if (durations) {
1843
+ file.importDurations = durations;
1844
+ }
1845
+ }
1846
+ calculateSuiteHash(file);
1847
+ const hasOnlyTasks = someTasksAreOnly(file);
1848
+ if (!testTagsFilter && !defaultTagsFilter && config.tagsFilter) {
1849
+ defaultTagsFilter = createTagsFilter(config.tagsFilter, config.tags);
1850
+ }
1851
+ interpretTaskModes(file, testNamePattern ?? config.testNamePattern, testLocations, testIds, testTagsFilter ?? defaultTagsFilter, hasOnlyTasks, false, config.allowOnly);
1852
+ if (file.mode === "queued") {
1853
+ file.mode = "run";
1854
+ }
1855
+ files.push(file);
1856
+ });
1857
+ }
1858
+ return files;
1859
+ }
1860
+ function mergeHooks(baseHooks, hooks) {
1861
+ for (const _key in hooks) {
1862
+ const key = _key;
1863
+ baseHooks[key].push(...hooks[key]);
1864
+ }
1865
+ return baseHooks;
1866
+ }
1867
+
1868
+ const now = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now;
1869
+ const unixNow = Date.now;
1870
+ const { clearTimeout, setTimeout } = getSafeTimers();
1871
+ let limitMaxConcurrency;
1872
+ /**
1873
+ * Normalizes retry configuration to extract individual values.
1874
+ * Handles both number and object forms.
1875
+ */
1876
+ function getRetryCount(retry) {
1877
+ if (retry === undefined) {
1878
+ return 0;
1879
+ }
1880
+ if (typeof retry === "number") {
1881
+ return retry;
1882
+ }
1883
+ return retry.count ?? 0;
1884
+ }
1885
+ function getRetryDelay(retry) {
1886
+ if (retry === undefined) {
1887
+ return 0;
1888
+ }
1889
+ if (typeof retry === "number") {
1890
+ return 0;
1891
+ }
1892
+ return retry.delay ?? 0;
1893
+ }
1894
+ function getRetryCondition(retry) {
1895
+ if (retry === undefined) {
1896
+ return undefined;
1897
+ }
1898
+ if (typeof retry === "number") {
1899
+ return undefined;
1900
+ }
1901
+ return retry.condition;
1902
+ }
1903
+ function updateSuiteHookState(task, name, state, runner) {
1904
+ if (!task.result) {
1905
+ task.result = { state: "run" };
1906
+ }
1907
+ if (!task.result.hooks) {
1908
+ task.result.hooks = {};
1909
+ }
1910
+ const suiteHooks = task.result.hooks;
1911
+ if (suiteHooks) {
1912
+ suiteHooks[name] = state;
1913
+ let event = state === "run" ? "before-hook-start" : "before-hook-end";
1914
+ if (name === "afterAll" || name === "afterEach") {
1915
+ event = state === "run" ? "after-hook-start" : "after-hook-end";
1916
+ }
1917
+ updateTask(event, task, runner);
1918
+ }
1919
+ }
1920
+ function getSuiteHooks(suite, name, sequence) {
1921
+ const hooks = getHooks(suite)[name];
1922
+ if (sequence === "stack" && (name === "afterAll" || name === "afterEach")) {
1923
+ return hooks.slice().reverse();
1924
+ }
1925
+ return hooks;
1926
+ }
1927
+ async function callTestHooks(runner, test, hooks, sequence) {
1928
+ if (sequence === "stack") {
1929
+ hooks = hooks.slice().reverse();
1930
+ }
1931
+ if (!hooks.length) {
1932
+ return;
1933
+ }
1934
+ const context = test.context;
1935
+ const onTestFailed = test.context.onTestFailed;
1936
+ const onTestFinished = test.context.onTestFinished;
1937
+ context.onTestFailed = () => {
1938
+ throw new Error(`Cannot call "onTestFailed" inside a test hook.`);
1939
+ };
1940
+ context.onTestFinished = () => {
1941
+ throw new Error(`Cannot call "onTestFinished" inside a test hook.`);
1942
+ };
1943
+ if (sequence === "parallel") {
1944
+ try {
1945
+ await Promise.all(hooks.map((fn) => limitMaxConcurrency(() => fn(test.context))));
1946
+ } catch (e) {
1947
+ failTask(test.result, e, runner.config.diffOptions);
1948
+ }
1949
+ } else {
1950
+ for (const fn of hooks) {
1951
+ try {
1952
+ await limitMaxConcurrency(() => fn(test.context));
1953
+ } catch (e) {
1954
+ failTask(test.result, e, runner.config.diffOptions);
1955
+ }
1956
+ }
1957
+ }
1958
+ context.onTestFailed = onTestFailed;
1959
+ context.onTestFinished = onTestFinished;
1960
+ }
1961
+ async function callSuiteHook(suite, currentTask, name, runner, args) {
1962
+ const sequence = runner.config.sequence.hooks;
1963
+ const callbacks = [];
1964
+ // stop at file level
1965
+ const parentSuite = "filepath" in suite ? null : suite.suite || suite.file;
1966
+ if (name === "beforeEach" && parentSuite) {
1967
+ callbacks.push(...await callSuiteHook(parentSuite, currentTask, name, runner, args));
1968
+ }
1969
+ const hooks = getSuiteHooks(suite, name, sequence);
1970
+ if (hooks.length > 0) {
1971
+ updateSuiteHookState(currentTask, name, "run", runner);
1972
+ }
1973
+ async function runHook(hook) {
1974
+ return limitMaxConcurrency(async () => {
1975
+ return getBeforeHookCleanupCallback(hook, await hook(...args), name === "beforeEach" ? args[0] : undefined);
1976
+ });
1977
+ }
1978
+ if (sequence === "parallel") {
1979
+ callbacks.push(...await Promise.all(hooks.map((hook) => runHook(hook))));
1980
+ } else {
1981
+ for (const hook of hooks) {
1982
+ callbacks.push(await runHook(hook));
1983
+ }
1984
+ }
1985
+ if (hooks.length > 0) {
1986
+ updateSuiteHookState(currentTask, name, "pass", runner);
1987
+ }
1988
+ if (name === "afterEach" && parentSuite) {
1989
+ callbacks.push(...await callSuiteHook(parentSuite, currentTask, name, runner, args));
1990
+ }
1991
+ return callbacks;
1992
+ }
1993
+ function getAroundEachHooks(suite) {
1994
+ const hooks = [];
1995
+ const parentSuite = "filepath" in suite ? null : suite.suite || suite.file;
1996
+ if (parentSuite) {
1997
+ hooks.push(...getAroundEachHooks(parentSuite));
1998
+ }
1999
+ hooks.push(...getHooks(suite).aroundEach);
2000
+ return hooks;
2001
+ }
2002
+ function getAroundAllHooks(suite) {
2003
+ return getHooks(suite).aroundAll;
2004
+ }
2005
+ function makeAroundHookTimeoutError(hookName, phase, timeout, stackTraceError) {
2006
+ const message = `The ${phase} phase of "${hookName}" hook timed out after ${timeout}ms.`;
2007
+ const ErrorClass = phase === "setup" ? AroundHookSetupError : AroundHookTeardownError;
2008
+ const error = new ErrorClass(message);
2009
+ if (stackTraceError?.stack) {
2010
+ error.stack = stackTraceError.stack.replace(stackTraceError.message, error.message);
2011
+ }
2012
+ return error;
2013
+ }
2014
+ async function callAroundHooks(runInner, options) {
2015
+ const { hooks, hookName, callbackName, onTimeout, invokeHook } = options;
2016
+ if (!hooks.length) {
2017
+ await runInner();
2018
+ return;
2019
+ }
2020
+ const hookErrors = [];
2021
+ const createTimeoutPromise = (timeout, phase, stackTraceError) => {
2022
+ let timer;
2023
+ let timedout = false;
2024
+ const promise = new Promise((_, reject) => {
2025
+ if (timeout > 0 && timeout !== Number.POSITIVE_INFINITY) {
2026
+ timer = setTimeout(() => {
2027
+ timedout = true;
2028
+ const error = makeAroundHookTimeoutError(hookName, phase, timeout, stackTraceError);
2029
+ onTimeout?.(error);
2030
+ reject(error);
2031
+ }, timeout);
2032
+ timer.unref?.();
2033
+ }
2034
+ });
2035
+ const clear = () => {
2036
+ if (timer) {
2037
+ clearTimeout(timer);
2038
+ timer = undefined;
2039
+ }
2040
+ };
2041
+ return {
2042
+ promise,
2043
+ clear,
2044
+ isTimedOut: () => timedout
2045
+ };
2046
+ };
2047
+ const runNextHook = async (index) => {
2048
+ if (index >= hooks.length) {
2049
+ return runInner();
2050
+ }
2051
+ const hook = hooks[index];
2052
+ const timeout = getAroundHookTimeout(hook);
2053
+ const stackTraceError = getAroundHookStackTrace(hook);
2054
+ let useCalled = false;
2055
+ let setupTimeout;
2056
+ let teardownTimeout;
2057
+ let setupLimitConcurrencyRelease;
2058
+ let teardownLimitConcurrencyRelease;
2059
+ // Promise that resolves when use() is called (setup phase complete)
2060
+ let resolveUseCalled;
2061
+ const useCalledPromise = new Promise((resolve) => {
2062
+ resolveUseCalled = resolve;
2063
+ });
2064
+ // Promise that resolves when use() returns (inner hooks complete, teardown phase starts)
2065
+ let resolveUseReturned;
2066
+ const useReturnedPromise = new Promise((resolve) => {
2067
+ resolveUseReturned = resolve;
2068
+ });
2069
+ // Promise that resolves when hook completes
2070
+ let resolveHookComplete;
2071
+ let rejectHookComplete;
2072
+ const hookCompletePromise = new Promise((resolve, reject) => {
2073
+ resolveHookComplete = resolve;
2074
+ rejectHookComplete = reject;
2075
+ });
2076
+ const use = async () => {
2077
+ // shouldn't continue to next (runTest/Suite or inner aroundEach/All) when aroundEach/All setup timed out.
2078
+ if (setupTimeout.isTimedOut()) {
2079
+ // we can throw any error to bail out.
2080
+ // this error is not seen by end users since `runNextHook` already rejected with timeout error
2081
+ // and this error is caught by `rejectHookComplete`.
2082
+ throw new Error("__VITEST_INTERNAL_AROUND_HOOK_ABORT__");
2083
+ }
2084
+ if (useCalled) {
2085
+ throw new AroundHookMultipleCallsError(`The \`${callbackName}\` callback was called multiple times in the \`${hookName}\` hook. ` + `The callback can only be called once per hook.`);
2086
+ }
2087
+ useCalled = true;
2088
+ resolveUseCalled();
2089
+ // Setup phase completed - clear setup timer
2090
+ setupTimeout.clear();
2091
+ setupLimitConcurrencyRelease?.();
2092
+ // Run inner hooks - don't time this against our teardown timeout
2093
+ await runNextHook(index + 1).catch((e) => hookErrors.push(e));
2094
+ teardownLimitConcurrencyRelease = await limitMaxConcurrency.acquire();
2095
+ // Start teardown timer after inner hooks complete - only times this hook's teardown code
2096
+ teardownTimeout = createTimeoutPromise(timeout, "teardown", stackTraceError);
2097
+ // Signal that use() is returning (teardown phase starting)
2098
+ resolveUseReturned();
2099
+ };
2100
+ setupLimitConcurrencyRelease = await limitMaxConcurrency.acquire();
2101
+ // Start setup timeout
2102
+ setupTimeout = createTimeoutPromise(timeout, "setup", stackTraceError);
2103
+ (async () => {
2104
+ try {
2105
+ await invokeHook(hook, use);
2106
+ if (!useCalled) {
2107
+ 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"}.`);
2108
+ }
2109
+ resolveHookComplete();
2110
+ } catch (error) {
2111
+ rejectHookComplete(error);
2112
+ } finally {
2113
+ setupLimitConcurrencyRelease?.();
2114
+ teardownLimitConcurrencyRelease?.();
2115
+ }
2116
+ })();
2117
+ // Wait for either: use() to be called OR hook to complete (error) OR setup timeout
2118
+ try {
2119
+ await Promise.race([
2120
+ useCalledPromise,
2121
+ hookCompletePromise,
2122
+ setupTimeout.promise
2123
+ ]);
2124
+ } finally {
2125
+ setupLimitConcurrencyRelease?.();
2126
+ setupTimeout.clear();
2127
+ }
2128
+ // Wait for use() to return (inner hooks complete) OR hook to complete (error during inner hooks)
2129
+ await Promise.race([useReturnedPromise, hookCompletePromise]);
2130
+ // Now teardownTimeout is guaranteed to be set
2131
+ // Wait for hook to complete (teardown) OR teardown timeout
2132
+ try {
2133
+ await Promise.race([hookCompletePromise, teardownTimeout?.promise]);
2134
+ } finally {
2135
+ teardownLimitConcurrencyRelease?.();
2136
+ teardownTimeout?.clear();
2137
+ }
2138
+ };
2139
+ await runNextHook(0).catch((e) => hookErrors.push(e));
2140
+ if (hookErrors.length > 0) {
2141
+ throw hookErrors;
2142
+ }
2143
+ }
2144
+ async function callAroundAllHooks(suite, runSuiteInner) {
2145
+ await callAroundHooks(runSuiteInner, {
2146
+ hooks: getAroundAllHooks(suite),
2147
+ hookName: "aroundAll",
2148
+ callbackName: "runSuite()",
2149
+ invokeHook: (hook, use) => hook(use, suite)
2150
+ });
2151
+ }
2152
+ async function callAroundEachHooks(suite, test, runTest) {
2153
+ await callAroundHooks(
2154
+ // Take checkpoint right before runTest - at this point all aroundEach fixtures
2155
+ // have been resolved, so we can correctly identify which fixtures belong to
2156
+ // aroundEach (before checkpoint) vs inside runTest (after checkpoint)
2157
+ () => runTest(getFixtureCleanupCount(test.context)),
2158
+ {
2159
+ hooks: getAroundEachHooks(suite),
2160
+ hookName: "aroundEach",
2161
+ callbackName: "runTest()",
2162
+ onTimeout: (error) => abortContextSignal(test.context, error),
2163
+ invokeHook: (hook, use) => hook(use, test.context, suite)
2164
+ }
2165
+ );
2166
+ }
2167
+ const packs = new Map();
2168
+ const eventsPacks = [];
2169
+ const pendingTasksUpdates = [];
2170
+ function sendTasksUpdate(runner) {
2171
+ if (packs.size) {
2172
+ const taskPacks = Array.from(packs).map(([id, task]) => {
2173
+ return [
2174
+ id,
2175
+ task[0],
2176
+ task[1]
2177
+ ];
2178
+ });
2179
+ const p = runner.onTaskUpdate?.(taskPacks, eventsPacks);
2180
+ if (p) {
2181
+ pendingTasksUpdates.push(p);
2182
+ // remove successful promise to not grow array indefnitely,
2183
+ // but keep rejections so finishSendTasksUpdate can handle them
2184
+ p.then(() => pendingTasksUpdates.splice(pendingTasksUpdates.indexOf(p), 1), () => {});
2185
+ }
2186
+ eventsPacks.length = 0;
2187
+ packs.clear();
2188
+ }
2189
+ }
2190
+ async function finishSendTasksUpdate(runner) {
2191
+ sendTasksUpdate(runner);
2192
+ await Promise.all(pendingTasksUpdates);
2193
+ }
2194
+ function throttle(fn, ms) {
2195
+ let last = 0;
2196
+ let pendingCall;
2197
+ return function call(...args) {
2198
+ const now = unixNow();
2199
+ if (now - last > ms) {
2200
+ last = now;
2201
+ clearTimeout(pendingCall);
2202
+ pendingCall = undefined;
2203
+ return fn.apply(this, args);
2204
+ }
2205
+ // Make sure fn is still called even if there are no further calls
2206
+ pendingCall ??= setTimeout(() => call.bind(this)(...args), ms);
2207
+ };
2208
+ }
2209
+ // throttle based on summary reporter's DURATION_UPDATE_INTERVAL_MS
2210
+ const sendTasksUpdateThrottled = throttle(sendTasksUpdate, 100);
2211
+ function updateTask(event, task, runner) {
2212
+ eventsPacks.push([
2213
+ task.id,
2214
+ event,
2215
+ undefined
2216
+ ]);
2217
+ packs.set(task.id, [task.result, task.meta]);
2218
+ sendTasksUpdateThrottled(runner);
2219
+ }
2220
+ async function callCleanupHooks(runner, cleanups) {
2221
+ const sequence = runner.config.sequence.hooks;
2222
+ if (sequence === "stack") {
2223
+ cleanups = cleanups.slice().reverse();
2224
+ }
2225
+ if (sequence === "parallel") {
2226
+ await Promise.all(cleanups.map(async (fn) => {
2227
+ if (typeof fn !== "function") {
2228
+ return;
2229
+ }
2230
+ await limitMaxConcurrency(() => fn());
2231
+ }));
2232
+ } else {
2233
+ for (const fn of cleanups) {
2234
+ if (typeof fn !== "function") {
2235
+ continue;
2236
+ }
2237
+ await limitMaxConcurrency(() => fn());
2238
+ }
2239
+ }
2240
+ }
2241
+ /**
2242
+ * Determines if a test should be retried based on its retryCondition configuration
2243
+ */
2244
+ function passesRetryCondition(test, errors) {
2245
+ const condition = getRetryCondition(test.retry);
2246
+ if (!errors || errors.length === 0) {
2247
+ return false;
2248
+ }
2249
+ if (!condition) {
2250
+ return true;
2251
+ }
2252
+ const error = errors[errors.length - 1];
2253
+ if (condition instanceof RegExp) {
2254
+ return condition.test(error.message || "");
2255
+ } else if (typeof condition === "function") {
2256
+ return condition(error);
2257
+ }
2258
+ return false;
2259
+ }
2260
+ async function runTest(test, runner) {
2261
+ await runner.onBeforeRunTask?.(test);
2262
+ if (test.mode !== "run" && test.mode !== "queued") {
2263
+ updateTask("test-prepare", test, runner);
2264
+ updateTask("test-finished", test, runner);
2265
+ return;
2266
+ }
2267
+ if (test.result?.state === "fail") {
2268
+ // should not be possible to get here, I think this is just copy pasted from suite
2269
+ // TODO: maybe someone fails tests in `beforeAll` hooks?
2270
+ // https://github.com/vitest-dev/vitest/pull/7069
2271
+ updateTask("test-failed-early", test, runner);
2272
+ return;
2273
+ }
2274
+ const start = now();
2275
+ test.result = {
2276
+ state: "run",
2277
+ startTime: unixNow(),
2278
+ retryCount: 0
2279
+ };
2280
+ updateTask("test-prepare", test, runner);
2281
+ const cleanupRunningTest = addRunningTest(test);
2282
+ setCurrentTest(test);
2283
+ const suite = test.suite || test.file;
2284
+ const $ = runner.trace;
2285
+ const repeats = test.repeats ?? 0;
2286
+ for (let repeatCount = 0; repeatCount <= repeats; repeatCount++) {
2287
+ const retry = getRetryCount(test.retry);
2288
+ for (let retryCount = 0; retryCount <= retry; retryCount++) {
2289
+ let beforeEachCleanups = [];
2290
+ // fixtureCheckpoint is passed by callAroundEachHooks - it represents the count
2291
+ // of fixture cleanup functions AFTER all aroundEach fixtures have been resolved
2292
+ // but BEFORE the test runs. This allows us to clean up only fixtures created
2293
+ // inside runTest while preserving aroundEach fixtures for teardown.
2294
+ await callAroundEachHooks(suite, test, async (fixtureCheckpoint) => {
2295
+ try {
2296
+ await runner.onBeforeTryTask?.(test, {
2297
+ retry: retryCount,
2298
+ repeats: repeatCount
2299
+ });
2300
+ test.result.repeatCount = repeatCount;
2301
+ beforeEachCleanups = await $("test.beforeEach", () => callSuiteHook(suite, test, "beforeEach", runner, [test.context, suite]));
2302
+ if (runner.runTask) {
2303
+ await $("test.callback", () => limitMaxConcurrency(() => runner.runTask(test)));
2304
+ } else {
2305
+ const fn = getFn(test);
2306
+ if (!fn) {
2307
+ throw new Error("Test function is not found. Did you add it using `setFn`?");
2308
+ }
2309
+ await $("test.callback", () => limitMaxConcurrency(() => fn()));
2310
+ }
2311
+ await runner.onAfterTryTask?.(test, {
2312
+ retry: retryCount,
2313
+ repeats: repeatCount
2314
+ });
2315
+ if (test.result.state !== "fail") {
2316
+ test.result.state = "pass";
2317
+ }
2318
+ } catch (e) {
2319
+ failTask(test.result, e, runner.config.diffOptions);
2320
+ }
2321
+ try {
2322
+ await runner.onTaskFinished?.(test);
2323
+ } catch (e) {
2324
+ failTask(test.result, e, runner.config.diffOptions);
2325
+ }
2326
+ try {
2327
+ await $("test.afterEach", () => callSuiteHook(suite, test, "afterEach", runner, [test.context, suite]));
2328
+ if (beforeEachCleanups.length) {
2329
+ await $("test.cleanup", () => callCleanupHooks(runner, beforeEachCleanups));
2330
+ }
2331
+ // Only clean up fixtures created inside runTest (after the checkpoint)
2332
+ // Fixtures created for aroundEach will be cleaned up after aroundEach teardown
2333
+ await callFixtureCleanupFrom(test.context, fixtureCheckpoint);
2334
+ } catch (e) {
2335
+ failTask(test.result, e, runner.config.diffOptions);
2336
+ }
2337
+ if (test.onFinished?.length) {
2338
+ await $("test.onFinished", () => callTestHooks(runner, test, test.onFinished, "stack"));
2339
+ }
2340
+ if (test.result.state === "fail" && test.onFailed?.length) {
2341
+ await $("test.onFailed", () => callTestHooks(runner, test, test.onFailed, runner.config.sequence.hooks));
2342
+ }
2343
+ test.onFailed = undefined;
2344
+ test.onFinished = undefined;
2345
+ await runner.onAfterRetryTask?.(test, {
2346
+ retry: retryCount,
2347
+ repeats: repeatCount
2348
+ });
2349
+ }).catch((error) => {
2350
+ failTask(test.result, error, runner.config.diffOptions);
2351
+ });
2352
+ // Clean up fixtures that were created for aroundEach (before the checkpoint)
2353
+ // This runs after aroundEach teardown has completed
2354
+ try {
2355
+ await callFixtureCleanup(test.context);
2356
+ } catch (e) {
2357
+ failTask(test.result, e, runner.config.diffOptions);
2358
+ }
2359
+ // skipped with new PendingError
2360
+ if (test.result?.pending || test.result?.state === "skip") {
2361
+ test.mode = "skip";
2362
+ test.result = {
2363
+ state: "skip",
2364
+ note: test.result?.note,
2365
+ pending: true,
2366
+ duration: now() - start
2367
+ };
2368
+ updateTask("test-finished", test, runner);
2369
+ setCurrentTest(undefined);
2370
+ cleanupRunningTest();
2371
+ return;
2372
+ }
2373
+ if (test.result.state === "pass") {
2374
+ break;
2375
+ }
2376
+ if (retryCount < retry) {
2377
+ const shouldRetry = passesRetryCondition(test, test.result.errors);
2378
+ if (!shouldRetry) {
2379
+ break;
2380
+ }
2381
+ test.result.state = "run";
2382
+ test.result.retryCount = (test.result.retryCount ?? 0) + 1;
2383
+ const delay = getRetryDelay(test.retry);
2384
+ if (delay > 0) {
2385
+ await new Promise((resolve) => setTimeout(resolve, delay));
2386
+ }
2387
+ }
2388
+ // update retry info
2389
+ updateTask("test-retried", test, runner);
2390
+ }
2391
+ }
2392
+ // if test is marked to be failed, flip the result
2393
+ if (test.fails) {
2394
+ if (test.result.state === "pass") {
2395
+ const error = processError(new Error("Expect test to fail"));
2396
+ test.result.state = "fail";
2397
+ test.result.errors = [error];
2398
+ } else {
2399
+ test.result.state = "pass";
2400
+ test.result.errors = undefined;
2401
+ }
2402
+ }
2403
+ cleanupRunningTest();
2404
+ setCurrentTest(undefined);
2405
+ test.result.duration = now() - start;
2406
+ await runner.onAfterRunTask?.(test);
2407
+ updateTask("test-finished", test, runner);
2408
+ }
2409
+ function failTask(result, err, diffOptions) {
2410
+ if (err instanceof PendingError) {
2411
+ result.state = "skip";
2412
+ result.note = err.note;
2413
+ result.pending = true;
2414
+ return;
2415
+ }
2416
+ if (err instanceof TestRunAbortError) {
2417
+ result.state = "skip";
2418
+ result.note = err.message;
2419
+ return;
2420
+ }
2421
+ result.state = "fail";
2422
+ const errors = Array.isArray(err) ? err : [err];
2423
+ for (const e of errors) {
2424
+ const errors = e instanceof AggregateError ? e.errors.map((e) => processError(e, diffOptions)) : [processError(e, diffOptions)];
2425
+ result.errors ??= [];
2426
+ result.errors.push(...errors);
2427
+ }
2428
+ }
2429
+ function markTasksAsSkipped(suite, runner) {
2430
+ suite.tasks.forEach((t) => {
2431
+ t.mode = "skip";
2432
+ t.result = {
2433
+ ...t.result,
2434
+ state: "skip"
2435
+ };
2436
+ updateTask("test-finished", t, runner);
2437
+ if (t.type === "suite") {
2438
+ markTasksAsSkipped(t, runner);
2439
+ }
2440
+ });
2441
+ }
2442
+ function markPendingTasksAsSkipped(suite, runner, note) {
2443
+ suite.tasks.forEach((t) => {
2444
+ if (!t.result || t.result.state === "run") {
2445
+ t.mode = "skip";
2446
+ t.result = {
2447
+ ...t.result,
2448
+ state: "skip",
2449
+ note
2450
+ };
2451
+ updateTask("test-cancel", t, runner);
2452
+ }
2453
+ if (t.type === "suite") {
2454
+ markPendingTasksAsSkipped(t, runner, note);
2455
+ }
2456
+ });
2457
+ }
2458
+ async function runSuite(suite, runner) {
2459
+ await runner.onBeforeRunSuite?.(suite);
2460
+ if (suite.result?.state === "fail") {
2461
+ markTasksAsSkipped(suite, runner);
2462
+ // failed during collection
2463
+ updateTask("suite-failed-early", suite, runner);
2464
+ return;
2465
+ }
2466
+ const start = now();
2467
+ const mode = suite.mode;
2468
+ suite.result = {
2469
+ state: mode === "skip" || mode === "todo" ? mode : "run",
2470
+ startTime: unixNow()
2471
+ };
2472
+ const $ = runner.trace;
2473
+ updateTask("suite-prepare", suite, runner);
2474
+ let beforeAllCleanups = [];
2475
+ if (suite.mode === "skip") {
2476
+ suite.result.state = "skip";
2477
+ updateTask("suite-finished", suite, runner);
2478
+ } else if (suite.mode === "todo") {
2479
+ suite.result.state = "todo";
2480
+ updateTask("suite-finished", suite, runner);
2481
+ } else {
2482
+ let suiteRan = false;
2483
+ try {
2484
+ await callAroundAllHooks(suite, async () => {
2485
+ suiteRan = true;
2486
+ try {
2487
+ // beforeAll
2488
+ try {
2489
+ beforeAllCleanups = await $("suite.beforeAll", () => callSuiteHook(suite, suite, "beforeAll", runner, [suite]));
2490
+ } catch (e) {
2491
+ failTask(suite.result, e, runner.config.diffOptions);
2492
+ markTasksAsSkipped(suite, runner);
2493
+ return;
2494
+ }
2495
+ // run suite children
2496
+ if (runner.runSuite) {
2497
+ await runner.runSuite(suite);
2498
+ } else {
2499
+ for (let tasksGroup of partitionSuiteChildren(suite)) {
2500
+ if (tasksGroup[0].concurrent === true) {
2501
+ await Promise.all(tasksGroup.map((c) => runSuiteChild(c, runner)));
2502
+ } else {
2503
+ const { sequence } = runner.config;
2504
+ if (suite.shuffle) {
2505
+ // run describe block independently from tests
2506
+ const suites = tasksGroup.filter((group) => group.type === "suite");
2507
+ const tests = tasksGroup.filter((group) => group.type === "test");
2508
+ const groups = shuffle([suites, tests], sequence.seed);
2509
+ tasksGroup = groups.flatMap((group) => shuffle(group, sequence.seed));
2510
+ }
2511
+ for (const c of tasksGroup) {
2512
+ await runSuiteChild(c, runner);
2513
+ }
2514
+ }
2515
+ }
2516
+ }
2517
+ } finally {
2518
+ // afterAll runs even if beforeAll or suite children fail
2519
+ try {
2520
+ await $("suite.afterAll", () => callSuiteHook(suite, suite, "afterAll", runner, [suite]));
2521
+ if (beforeAllCleanups.length) {
2522
+ await $("suite.cleanup", () => callCleanupHooks(runner, beforeAllCleanups));
2523
+ }
2524
+ if (suite.file === suite) {
2525
+ const contexts = TestFixtures.getFileContexts(suite.file);
2526
+ await Promise.all(contexts.map((context) => callFixtureCleanup(context)));
2527
+ }
2528
+ } catch (e) {
2529
+ failTask(suite.result, e, runner.config.diffOptions);
2530
+ }
2531
+ }
2532
+ });
2533
+ } catch (e) {
2534
+ // mark tasks as skipped if aroundAll failed before the suite callback was executed
2535
+ if (!suiteRan) {
2536
+ markTasksAsSkipped(suite, runner);
2537
+ }
2538
+ failTask(suite.result, e, runner.config.diffOptions);
2539
+ }
2540
+ if (suite.mode === "run" || suite.mode === "queued") {
2541
+ if (!runner.config.passWithNoTests && !hasTests(suite)) {
2542
+ suite.result.state = "fail";
2543
+ if (!suite.result.errors?.length) {
2544
+ const error = processError(new Error(`No test found in suite ${suite.name}`));
2545
+ suite.result.errors = [error];
2546
+ }
2547
+ } else if (hasFailed(suite)) {
2548
+ suite.result.state = "fail";
2549
+ } else {
2550
+ suite.result.state = "pass";
2551
+ }
2552
+ }
2553
+ suite.result.duration = now() - start;
2554
+ await runner.onAfterRunSuite?.(suite);
2555
+ updateTask("suite-finished", suite, runner);
2556
+ }
2557
+ }
2558
+ async function runSuiteChild(c, runner) {
2559
+ const $ = runner.trace;
2560
+ if (c.type === "test") {
2561
+ return $("run.test", {
2562
+ "vitest.test.id": c.id,
2563
+ "vitest.test.name": c.name,
2564
+ "vitest.test.mode": c.mode,
2565
+ "vitest.test.timeout": c.timeout,
2566
+ "code.file.path": c.file.filepath,
2567
+ "code.line.number": c.location?.line,
2568
+ "code.column.number": c.location?.column
2569
+ }, () => runTest(c, runner));
2570
+ } else if (c.type === "suite") {
2571
+ return $("run.suite", {
2572
+ "vitest.suite.id": c.id,
2573
+ "vitest.suite.name": c.name,
2574
+ "vitest.suite.mode": c.mode,
2575
+ "code.file.path": c.file.filepath,
2576
+ "code.line.number": c.location?.line,
2577
+ "code.column.number": c.location?.column
2578
+ }, () => runSuite(c, runner));
2579
+ }
2580
+ }
2581
+ async function runFiles(files, runner) {
2582
+ limitMaxConcurrency ??= limitConcurrency(runner.config.maxConcurrency);
2583
+ for (const file of files) {
2584
+ if (!file.tasks.length && !runner.config.passWithNoTests) {
2585
+ if (!file.result?.errors?.length) {
2586
+ const error = processError(new Error(`No test suite found in file ${file.filepath}`));
2587
+ file.result = {
2588
+ state: "fail",
2589
+ errors: [error]
2590
+ };
2591
+ }
2592
+ }
2593
+ await runner.trace("run.spec", {
2594
+ "code.file.path": file.filepath,
2595
+ "vitest.suite.tasks.length": file.tasks.length
2596
+ }, () => runSuite(file, runner));
2597
+ }
2598
+ }
2599
+ const workerRunners = new WeakSet();
2600
+ function defaultTrace(_, attributes, cb) {
2601
+ if (typeof attributes === "function") {
2602
+ return attributes();
2603
+ }
2604
+ return cb();
2605
+ }
2606
+ async function startTests(specs, runner) {
2607
+ runner.trace ??= defaultTrace;
2608
+ const cancel = runner.cancel?.bind(runner);
2609
+ // Ideally, we need to have an event listener for this, but only have a runner here.
2610
+ // Adding another onCancel felt wrong (maybe it needs to be refactored)
2611
+ runner.cancel = (reason) => {
2612
+ // We intentionally create only one error since there is only one test run that can be cancelled
2613
+ const error = new TestRunAbortError("The test run was aborted by the user.", reason);
2614
+ getRunningTests().forEach((test) => {
2615
+ abortContextSignal(test.context, error);
2616
+ markPendingTasksAsSkipped(test.file, runner, error.message);
2617
+ });
2618
+ return cancel?.(reason);
2619
+ };
2620
+ if (!workerRunners.has(runner)) {
2621
+ runner.onCleanupWorkerContext?.(async () => {
2622
+ await Promise.all([...TestFixtures.getWorkerContexts()].map((context) => callFixtureCleanup(context))).finally(() => {
2623
+ TestFixtures.clearDefinitions();
2624
+ });
2625
+ });
2626
+ workerRunners.add(runner);
2627
+ }
2628
+ try {
2629
+ const paths = specs.map((f) => typeof f === "string" ? f : f.filepath);
2630
+ await runner.onBeforeCollect?.(paths);
2631
+ const files = await collectTests(specs, runner);
2632
+ await runner.onCollected?.(files);
2633
+ await runner.onBeforeRunFiles?.(files);
2634
+ await runFiles(files, runner);
2635
+ await runner.onAfterRunFiles?.(files);
2636
+ await finishSendTasksUpdate(runner);
2637
+ return files;
2638
+ } finally {
2639
+ runner.cancel = cancel;
2640
+ }
2641
+ }
2642
+ async function publicCollect(specs, runner) {
2643
+ runner.trace ??= defaultTrace;
2644
+ const paths = specs.map((f) => typeof f === "string" ? f : f.filepath);
2645
+ await runner.onBeforeCollect?.(paths);
2646
+ const files = await collectTests(specs, runner);
2647
+ await runner.onCollected?.(files);
2648
+ return files;
2649
+ }
2650
+
2651
+ /**
2652
+ * @experimental
2653
+ * @advanced
2654
+ *
2655
+ * Records a custom test artifact during test execution.
2656
+ *
2657
+ * This function allows you to attach structured data, files, or metadata to a test.
2658
+ *
2659
+ * Vitest automatically injects the source location where the artifact was created and manages any attachments you include.
2660
+ *
2661
+ * **Note:** artifacts must be recorded before the task is reported. Any artifacts recorded after that will not be included in the task.
2662
+ *
2663
+ * @param task - The test task context, typically accessed via `this.task` in custom matchers or `context.task` in tests
2664
+ * @param artifact - The artifact to record. Must extend {@linkcode TestArtifactBase}
2665
+ *
2666
+ * @returns A promise that resolves to the recorded artifact with location injected
2667
+ *
2668
+ * @throws {Error} If the test runner doesn't support artifacts
2669
+ *
2670
+ * @example
2671
+ * ```ts
2672
+ * // In a custom assertion
2673
+ * async function toHaveValidSchema(this: MatcherState, actual: unknown) {
2674
+ * const validation = validateSchema(actual)
2675
+ *
2676
+ * await recordArtifact(this.task, {
2677
+ * type: 'my-plugin:schema-validation',
2678
+ * passed: validation.valid,
2679
+ * errors: validation.errors,
2680
+ * })
2681
+ *
2682
+ * return { pass: validation.valid, message: () => '...' }
2683
+ * }
2684
+ * ```
2685
+ */
2686
+ async function recordArtifact(task, artifact) {
2687
+ const runner = getRunner();
2688
+ const stack = findTestFileStackTrace(task.file.filepath, new Error("STACK_TRACE").stack);
2689
+ if (stack) {
2690
+ artifact.location = {
2691
+ file: stack.file,
2692
+ line: stack.line,
2693
+ column: stack.column
2694
+ };
2695
+ if (artifact.type === "internal:annotation") {
2696
+ artifact.annotation.location = artifact.location;
2697
+ }
2698
+ }
2699
+ if (Array.isArray(artifact.attachments)) {
2700
+ for (const attachment of artifact.attachments) {
2701
+ manageArtifactAttachment(attachment);
2702
+ }
2703
+ }
2704
+ // annotations won't resolve as artifacts for backwards compatibility until next major
2705
+ if (artifact.type === "internal:annotation") {
2706
+ return artifact;
2707
+ }
2708
+ if (!runner.onTestArtifactRecord) {
2709
+ throw new Error(`Test runner doesn't support test artifacts.`);
2710
+ }
2711
+ await finishSendTasksUpdate(runner);
2712
+ const resolvedArtifact = await runner.onTestArtifactRecord(task, artifact);
2713
+ task.artifacts.push(resolvedArtifact);
2714
+ return resolvedArtifact;
2715
+ }
2716
+ const table = [];
2717
+ for (let i = 65; i < 91; i++) {
2718
+ table.push(String.fromCharCode(i));
2719
+ }
2720
+ for (let i = 97; i < 123; i++) {
2721
+ table.push(String.fromCharCode(i));
2722
+ }
2723
+ for (let i = 0; i < 10; i++) {
2724
+ table.push(i.toString(10));
2725
+ }
2726
+ table.push("+", "/");
2727
+ function encodeUint8Array(bytes) {
2728
+ let base64 = "";
2729
+ const len = bytes.byteLength;
2730
+ for (let i = 0; i < len; i += 3) {
2731
+ if (len === i + 1) {
2732
+ const a = (bytes[i] & 252) >> 2;
2733
+ const b = (bytes[i] & 3) << 4;
2734
+ base64 += table[a];
2735
+ base64 += table[b];
2736
+ base64 += "==";
2737
+ } else if (len === i + 2) {
2738
+ const a = (bytes[i] & 252) >> 2;
2739
+ const b = (bytes[i] & 3) << 4 | (bytes[i + 1] & 240) >> 4;
2740
+ const c = (bytes[i + 1] & 15) << 2;
2741
+ base64 += table[a];
2742
+ base64 += table[b];
2743
+ base64 += table[c];
2744
+ base64 += "=";
2745
+ } else {
2746
+ const a = (bytes[i] & 252) >> 2;
2747
+ const b = (bytes[i] & 3) << 4 | (bytes[i + 1] & 240) >> 4;
2748
+ const c = (bytes[i + 1] & 15) << 2 | (bytes[i + 2] & 192) >> 6;
2749
+ const d = bytes[i + 2] & 63;
2750
+ base64 += table[a];
2751
+ base64 += table[b];
2752
+ base64 += table[c];
2753
+ base64 += table[d];
2754
+ }
2755
+ }
2756
+ return base64;
2757
+ }
2758
+ /**
2759
+ * Records an async operation associated with a test task.
2760
+ *
2761
+ * This function tracks promises that should be awaited before a test completes.
2762
+ * The promise is automatically removed from the test's promise list once it settles.
2763
+ */
2764
+ function recordAsyncOperation(test, promise) {
2765
+ // if promise is explicitly awaited, remove it from the list
2766
+ promise = promise.finally(() => {
2767
+ if (!test.promises) {
2768
+ return;
2769
+ }
2770
+ const index = test.promises.indexOf(promise);
2771
+ if (index !== -1) {
2772
+ test.promises.splice(index, 1);
2773
+ }
2774
+ });
2775
+ // record promise
2776
+ if (!test.promises) {
2777
+ test.promises = [];
2778
+ }
2779
+ test.promises.push(promise);
2780
+ return promise;
2781
+ }
2782
+ /**
2783
+ * Validates and prepares a test attachment for serialization.
2784
+ *
2785
+ * This function ensures attachments have either `body` or `path` set (but not both), and converts `Uint8Array` bodies to base64-encoded strings for easier serialization.
2786
+ *
2787
+ * @param attachment - The attachment to validate and prepare
2788
+ *
2789
+ * @throws {TypeError} If neither `body` nor `path` is provided
2790
+ * @throws {TypeError} If both `body` and `path` are provided
2791
+ */
2792
+ function manageArtifactAttachment(attachment) {
2793
+ if (attachment.body == null && !attachment.path) {
2794
+ throw new TypeError(`Test attachment requires "body" or "path" to be set. Both are missing.`);
2795
+ }
2796
+ if (attachment.body && attachment.path) {
2797
+ throw new TypeError(`Test attachment requires only one of "body" or "path" to be set. Both are specified.`);
2798
+ }
2799
+ // convert to a string so it's easier to serialise
2800
+ if (attachment.body instanceof Uint8Array) {
2801
+ attachment.body = encodeUint8Array(attachment.body);
2802
+ }
2803
+ }
2804
+
2805
+ 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 };