@vitest/runner 4.1.0 → 4.1.1

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