@vitest/runner 4.0.17 → 4.1.0-beta.2

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.
@@ -34,7 +34,7 @@ function createChainable(keys, fn) {
34
34
  /**
35
35
  * If any tasks been marked as `only`, mark all other tasks as `skip`.
36
36
  */
37
- function interpretTaskModes(file, namePattern, testLocations, onlyMode, parentIsOnly, allowOnly) {
37
+ function interpretTaskModes(file, namePattern, testLocations, testIds, testTagsFilter, onlyMode, parentIsOnly, allowOnly) {
38
38
  const matchedLocations = [];
39
39
  const traverseSuite = (suite, parentIsOnly, parentMatchedWithLocation) => {
40
40
  const suiteIsOnly = parentIsOnly || suite.mode === "only";
@@ -60,7 +60,7 @@ function interpretTaskModes(file, namePattern, testLocations, onlyMode, parentIs
60
60
  }
61
61
  let hasLocationMatch = parentMatchedWithLocation;
62
62
  // Match test location against provided locations, only run if present
63
- // in `testLocations`. Note: if `includeTaskLocations` is not enabled,
63
+ // in `testLocations`. Note: if `includeTaskLocation` is not enabled,
64
64
  // all test will be skipped.
65
65
  if (testLocations !== undefined && testLocations.length !== 0) {
66
66
  if (t.location && (testLocations === null || testLocations === void 0 ? void 0 : testLocations.includes(t.location.line))) {
@@ -77,6 +77,12 @@ function interpretTaskModes(file, namePattern, testLocations, onlyMode, parentIs
77
77
  if (namePattern && !getTaskFullName(t).match(namePattern)) {
78
78
  t.mode = "skip";
79
79
  }
80
+ if (testIds && !testIds.includes(t.id)) {
81
+ t.mode = "skip";
82
+ }
83
+ if (testTagsFilter && !testTagsFilter(t.tags || [])) {
84
+ t.mode = "skip";
85
+ }
80
86
  } else if (t.type === "suite") {
81
87
  if (t.mode === "skip") {
82
88
  skipAllTasks(t);
@@ -274,6 +280,241 @@ function partitionSuiteChildren(suite) {
274
280
  return tasksGroups;
275
281
  }
276
282
 
283
+ function validateTags(config, tags) {
284
+ if (!config.strictTags) {
285
+ return;
286
+ }
287
+ const availableTags = new Set(config.tags.map((tag) => tag.name));
288
+ for (const tag of tags) {
289
+ if (!availableTags.has(tag)) {
290
+ throw createNoTagsError(config.tags, tag);
291
+ }
292
+ }
293
+ }
294
+ function createNoTagsError(availableTags, tag, prefix = "tag") {
295
+ if (!availableTags.length) {
296
+ 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`);
297
+ }
298
+ 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")}`);
299
+ }
300
+ function createTagsFilter(tagsExpr, availableTags) {
301
+ const matchers = tagsExpr.map((expr) => parseTagsExpression(expr, availableTags));
302
+ return (testTags) => {
303
+ return matchers.every((matcher) => matcher(testTags));
304
+ };
305
+ }
306
+ function parseTagsExpression(expr, availableTags) {
307
+ const tokens = tokenize(expr);
308
+ const stream = new TokenStream(tokens, expr);
309
+ const ast = parseOrExpression(stream, availableTags);
310
+ if (stream.peek().type !== "EOF") {
311
+ throw new Error(`Invalid tags expression: unexpected "${formatToken(stream.peek())}" in "${expr}"`);
312
+ }
313
+ return (tags) => evaluateNode(ast, tags);
314
+ }
315
+ function formatToken(token) {
316
+ switch (token.type) {
317
+ case "TAG": return token.value;
318
+ default: return formatTokenType(token.type);
319
+ }
320
+ }
321
+ function tokenize(expr) {
322
+ const tokens = [];
323
+ let i = 0;
324
+ while (i < expr.length) {
325
+ if (expr[i] === " " || expr[i] === " ") {
326
+ i++;
327
+ continue;
328
+ }
329
+ if (expr[i] === "(") {
330
+ tokens.push({ type: "LPAREN" });
331
+ i++;
332
+ continue;
333
+ }
334
+ if (expr[i] === ")") {
335
+ tokens.push({ type: "RPAREN" });
336
+ i++;
337
+ continue;
338
+ }
339
+ if (expr[i] === "!") {
340
+ tokens.push({ type: "NOT" });
341
+ i++;
342
+ continue;
343
+ }
344
+ if (expr.slice(i, i + 2) === "&&") {
345
+ tokens.push({ type: "AND" });
346
+ i += 2;
347
+ continue;
348
+ }
349
+ if (expr.slice(i, i + 2) === "||") {
350
+ tokens.push({ type: "OR" });
351
+ i += 2;
352
+ continue;
353
+ }
354
+ if (/^and(?:\s|\)|$)/i.test(expr.slice(i))) {
355
+ tokens.push({ type: "AND" });
356
+ i += 3;
357
+ continue;
358
+ }
359
+ if (/^or(?:\s|\)|$)/i.test(expr.slice(i))) {
360
+ tokens.push({ type: "OR" });
361
+ i += 2;
362
+ continue;
363
+ }
364
+ if (/^not\s/i.test(expr.slice(i))) {
365
+ tokens.push({ type: "NOT" });
366
+ i += 3;
367
+ continue;
368
+ }
369
+ let tag = "";
370
+ while (i < expr.length && expr[i] !== " " && expr[i] !== " " && expr[i] !== "(" && expr[i] !== ")" && expr[i] !== "!" && expr[i] !== "&" && expr[i] !== "|") {
371
+ const remaining = expr.slice(i);
372
+ // Only treat and/or/not as operators if we're at the start of a tag (after whitespace)
373
+ // This allows tags like "demand", "editor", "cannot" to work correctly
374
+ if (tag === "" && (/^and(?:\s|\)|$)/i.test(remaining) || /^or(?:\s|\)|$)/i.test(remaining) || /^not\s/i.test(remaining))) {
375
+ break;
376
+ }
377
+ tag += expr[i];
378
+ i++;
379
+ }
380
+ if (tag) {
381
+ tokens.push({
382
+ type: "TAG",
383
+ value: tag
384
+ });
385
+ }
386
+ }
387
+ tokens.push({ type: "EOF" });
388
+ return tokens;
389
+ }
390
+ class TokenStream {
391
+ pos = 0;
392
+ constructor(tokens, expr) {
393
+ this.tokens = tokens;
394
+ this.expr = expr;
395
+ }
396
+ peek() {
397
+ return this.tokens[this.pos];
398
+ }
399
+ next() {
400
+ return this.tokens[this.pos++];
401
+ }
402
+ expect(type) {
403
+ const token = this.next();
404
+ if (token.type !== type) {
405
+ if (type === "RPAREN" && token.type === "EOF") {
406
+ throw new Error(`Invalid tags expression: missing closing ")" in "${this.expr}"`);
407
+ }
408
+ throw new Error(`Invalid tags expression: expected "${formatTokenType(type)}" but got "${formatToken(token)}" in "${this.expr}"`);
409
+ }
410
+ return token;
411
+ }
412
+ unexpectedToken() {
413
+ const token = this.peek();
414
+ if (token.type === "EOF") {
415
+ throw new Error(`Invalid tags expression: unexpected end of expression in "${this.expr}"`);
416
+ }
417
+ throw new Error(`Invalid tags expression: unexpected "${formatToken(token)}" in "${this.expr}"`);
418
+ }
419
+ }
420
+ function formatTokenType(type) {
421
+ switch (type) {
422
+ case "TAG": return "tag";
423
+ case "AND": return "and";
424
+ case "OR": return "or";
425
+ case "NOT": return "not";
426
+ case "LPAREN": return "(";
427
+ case "RPAREN": return ")";
428
+ case "EOF": return "end of expression";
429
+ }
430
+ }
431
+ function parseOrExpression(stream, availableTags) {
432
+ let left = parseAndExpression(stream, availableTags);
433
+ while (stream.peek().type === "OR") {
434
+ stream.next();
435
+ const right = parseAndExpression(stream, availableTags);
436
+ left = {
437
+ type: "or",
438
+ left,
439
+ right
440
+ };
441
+ }
442
+ return left;
443
+ }
444
+ function parseAndExpression(stream, availableTags) {
445
+ let left = parseUnaryExpression(stream, availableTags);
446
+ while (stream.peek().type === "AND") {
447
+ stream.next();
448
+ const right = parseUnaryExpression(stream, availableTags);
449
+ left = {
450
+ type: "and",
451
+ left,
452
+ right
453
+ };
454
+ }
455
+ return left;
456
+ }
457
+ function parseUnaryExpression(stream, availableTags) {
458
+ if (stream.peek().type === "NOT") {
459
+ stream.next();
460
+ const operand = parseUnaryExpression(stream, availableTags);
461
+ return {
462
+ type: "not",
463
+ operand
464
+ };
465
+ }
466
+ return parsePrimaryExpression(stream, availableTags);
467
+ }
468
+ function parsePrimaryExpression(stream, availableTags) {
469
+ const token = stream.peek();
470
+ if (token.type === "LPAREN") {
471
+ stream.next();
472
+ const expr = parseOrExpression(stream, availableTags);
473
+ stream.expect("RPAREN");
474
+ return expr;
475
+ }
476
+ if (token.type === "TAG") {
477
+ stream.next();
478
+ const tagValue = token.value;
479
+ const pattern = resolveTagPattern(tagValue, availableTags);
480
+ return {
481
+ type: "tag",
482
+ value: tagValue,
483
+ pattern
484
+ };
485
+ }
486
+ stream.unexpectedToken();
487
+ }
488
+ function createWildcardRegex(pattern) {
489
+ return new RegExp(`^${pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
490
+ }
491
+ function resolveTagPattern(tagPattern, availableTags) {
492
+ if (tagPattern.includes("*")) {
493
+ const regex = createWildcardRegex(tagPattern);
494
+ const hasMatch = availableTags.some((tag) => regex.test(tag.name));
495
+ if (!hasMatch) {
496
+ throw createNoTagsError(availableTags, tagPattern, "tag pattern");
497
+ }
498
+ return regex;
499
+ }
500
+ if (!availableTags.length || !availableTags.some((tag) => tag.name === tagPattern)) {
501
+ throw createNoTagsError(availableTags, tagPattern, "tag pattern");
502
+ }
503
+ return null;
504
+ }
505
+ function evaluateNode(node, tags) {
506
+ switch (node.type) {
507
+ case "tag":
508
+ if (node.pattern) {
509
+ return tags.some((tag) => node.pattern.test(tag));
510
+ }
511
+ return tags.includes(node.value);
512
+ case "not": return !evaluateNode(node.operand, tags);
513
+ case "and": return evaluateNode(node.left, tags) && evaluateNode(node.right, tags);
514
+ case "or": return evaluateNode(node.left, tags) || evaluateNode(node.right, tags);
515
+ }
516
+ }
517
+
277
518
  function isTestCase(s) {
278
519
  return s.type === "test";
279
520
  }
@@ -337,4 +578,4 @@ function createTaskName(names, separator = " > ") {
337
578
  return names.filter((name) => name !== undefined).join(separator);
338
579
  }
339
580
 
340
- export { calculateSuiteHash as a, createFileTask as b, createChainable as c, generateHash as d, createTaskName as e, findTestFileStackTrace as f, generateFileHash as g, getFullName as h, interpretTaskModes as i, getNames as j, getSuites as k, limitConcurrency as l, getTasks as m, getTestName as n, getTests as o, partitionSuiteChildren as p, hasFailed as q, hasTests as r, someTasksAreOnly as s, isTestCase as t };
581
+ export { calculateSuiteHash as a, createFileTask as b, createChainable as c, generateHash as d, createTagsFilter as e, findTestFileStackTrace as f, generateFileHash as g, createTaskName as h, interpretTaskModes as i, getFullName as j, getNames as k, limitConcurrency as l, getSuites as m, getTasks as n, getTestName as o, partitionSuiteChildren as p, getTests as q, hasFailed as r, someTasksAreOnly as s, hasTests as t, isTestCase as u, validateTags as v, createNoTagsError as w };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { b as TestArtifact, a as Test, S as Suite, d as SuiteHooks, F as File, e as TaskUpdateEvent, T as Task, f as TestAPI, g as SuiteAPI, h as SuiteCollector } from './tasks.d-C7UxawJ9.js';
2
- export { A as AfterAllListener, n as AfterEachListener, B as BeforeAllListener, p as BeforeEachListener, q as Fixture, r as FixtureFn, s as FixtureOptions, t as Fixtures, I as ImportDuration, u as InferFixturesTypes, O as OnTestFailedHandler, v as OnTestFinishedHandler, R as RunMode, w as RuntimeContext, x as SequenceHooks, y as SequenceSetupFiles, z as SuiteFactory, D as TaskBase, E as TaskCustomOptions, G as TaskEventPack, H as TaskHook, J as TaskMeta, K as TaskPopulated, L as TaskResult, M as TaskResultPack, N as TaskState, P as TestAnnotation, Q as TestAnnotationArtifact, U as TestAnnotationLocation, V as TestArtifactBase, W as TestArtifactLocation, X as TestArtifactRegistry, Y as TestAttachment, Z as TestContext, _ as TestFunction, $ as TestOptions, a0 as Use, a1 as VisualRegressionArtifact, i as afterAll, j as afterEach, k as beforeAll, l as beforeEach, o as onTestFailed, m as onTestFinished } from './tasks.d-C7UxawJ9.js';
1
+ import { T as TestArtifact, a as Test, S as Suite, b as SuiteHooks, F as FileSpecification, V as VitestRunner, c as File, d as TaskUpdateEvent, e as Task, f as TestAPI, g as SuiteAPI, h as SuiteCollector } from './tasks.d-CLPU8HE4.js';
2
+ export { A as AfterAllListener, s as AfterEachListener, B as BeforeAllListener, t as BeforeEachListener, C as CancelReason, u as Fixture, v as FixtureFn, w as FixtureOptions, x as Fixtures, I as ImportDuration, y as InferFixturesTypes, O as OnTestFailedHandler, z as OnTestFinishedHandler, R as Retry, D as RunMode, E as RuntimeContext, G as SequenceHooks, H as SequenceSetupFiles, J as SerializableRetry, K as SuiteFactory, L as TaskBase, M as TaskCustomOptions, N as TaskEventPack, P as TaskHook, Q as TaskMeta, U as TaskPopulated, W as TaskResult, X as TaskResultPack, Y as TaskState, Z as TestAnnotation, _ as TestAnnotationArtifact, $ as TestAnnotationLocation, a0 as TestArtifactBase, a1 as TestArtifactLocation, a2 as TestArtifactRegistry, a3 as TestAttachment, a4 as TestContext, a5 as TestFunction, a6 as TestOptions, n as TestTagDefinition, a7 as TestTags, a8 as Use, a9 as VisualRegressionArtifact, p as VitestRunnerConfig, q as VitestRunnerConstructor, r as VitestRunnerImportSource, i as afterAll, j as afterEach, k as beforeAll, l as beforeEach, o as onTestFailed, m as onTestFinished } from './tasks.d-CLPU8HE4.js';
3
3
  import { Awaitable } from '@vitest/utils';
4
- import { FileSpecification, VitestRunner } from './types.js';
5
- export { CancelReason, VitestRunnerConfig, VitestRunnerConstructor, VitestRunnerImportSource } from './types.js';
6
4
  import '@vitest/utils/diff';
7
5
 
8
6
  /**
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { processError } from '@vitest/utils/error';
2
- import { isObject, createDefer, assertTypes, toArray, isNegativeNaN, objectAttr, shuffle } from '@vitest/utils/helpers';
2
+ import { isObject, filterOutComments, createDefer, assertTypes, toArray, isNegativeNaN, unique, objectAttr, shuffle } from '@vitest/utils/helpers';
3
3
  import { getSafeTimers } from '@vitest/utils/timers';
4
4
  import { format, formatRegExp, objDisplay } from '@vitest/utils/display';
5
- import { c as createChainable, e as createTaskName, f as findTestFileStackTrace, b as createFileTask, a as calculateSuiteHash, s as someTasksAreOnly, i as interpretTaskModes, l as limitConcurrency, p as partitionSuiteChildren, r as hasTests, q as hasFailed } from './chunk-tasks.js';
5
+ import { c as createChainable, v as validateTags, h as createTaskName, w as createNoTagsError, f as findTestFileStackTrace, e as createTagsFilter, b as createFileTask, a as calculateSuiteHash, s as someTasksAreOnly, i as interpretTaskModes, l as limitConcurrency, p as partitionSuiteChildren, t as hasTests, r as hasFailed } from './chunk-tasks.js';
6
6
  import '@vitest/utils/source-map';
7
7
  import 'pathe';
8
8
 
@@ -311,31 +311,6 @@ function getUsedProps(fn) {
311
311
  }
312
312
  return props;
313
313
  }
314
- function filterOutComments(s) {
315
- const result = [];
316
- let commentState = "none";
317
- for (let i = 0; i < s.length; ++i) {
318
- if (commentState === "singleline") {
319
- if (s[i] === "\n") {
320
- commentState = "none";
321
- }
322
- } else if (commentState === "multiline") {
323
- if (s[i - 1] === "*" && s[i] === "/") {
324
- commentState = "none";
325
- }
326
- } else if (commentState === "none") {
327
- if (s[i] === "/" && s[i + 1] === "/") {
328
- commentState = "singleline";
329
- } else if (s[i] === "/" && s[i + 1] === "*") {
330
- commentState = "multiline";
331
- i += 2;
332
- } else {
333
- result.push(s[i]);
334
- }
335
- }
336
- }
337
- return result.join("");
338
- }
339
314
  function splitByComma(s) {
340
315
  const result = [];
341
316
  const stack = [];
@@ -693,18 +668,22 @@ function getRunner() {
693
668
  }
694
669
  function createDefaultSuite(runner) {
695
670
  const config = runner.config.sequence;
696
- const collector = suite("", { concurrent: config.concurrent }, () => {});
671
+ const options = {};
672
+ if (config.concurrent != null) {
673
+ options.concurrent = config.concurrent;
674
+ }
675
+ const collector = suite("", options, () => {});
697
676
  // no parent suite for top-level tests
698
677
  delete collector.suite;
699
678
  return collector;
700
679
  }
701
680
  function clearCollectorContext(file, currentRunner) {
681
+ currentTestFilepath = file.filepath;
682
+ runner = currentRunner;
702
683
  if (!defaultSuite) {
703
684
  defaultSuite = createDefaultSuite(currentRunner);
704
685
  }
705
686
  defaultSuite.file = file;
706
- runner = currentRunner;
707
- currentTestFilepath = file.filepath;
708
687
  collectorContext.tasks.length = 0;
709
688
  defaultSuite.clear();
710
689
  collectorContext.currentSuite = defaultSuite;
@@ -722,6 +701,7 @@ function createSuiteHooks() {
722
701
  afterEach: []
723
702
  };
724
703
  }
704
+ const POSITIVE_INFINITY = Number.POSITIVE_INFINITY;
725
705
  function parseArguments(optionsOrFn, timeoutOrTest) {
726
706
  if (timeoutOrTest != null && typeof timeoutOrTest === "object") {
727
707
  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.`);
@@ -753,27 +733,47 @@ function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions
753
733
  let suite;
754
734
  initSuite(true);
755
735
  const task = function(name = "", options = {}) {
756
- var _collectorContext$cur, _collectorContext$cur2, _collectorContext$cur3;
757
- const timeout = (options === null || options === void 0 ? void 0 : options.timeout) ?? runner.config.testTimeout;
736
+ var _collectorContext$cur, _collectorContext$cur2, _collectorContext$cur3, _collectorContext$cur4;
758
737
  const currentSuite = (_collectorContext$cur = collectorContext.currentSuite) === null || _collectorContext$cur === void 0 ? void 0 : _collectorContext$cur.suite;
738
+ const parentTask = currentSuite ?? ((_collectorContext$cur2 = collectorContext.currentSuite) === null || _collectorContext$cur2 === void 0 ? void 0 : _collectorContext$cur2.file);
739
+ const parentTags = (parentTask === null || parentTask === void 0 ? void 0 : parentTask.tags) || [];
740
+ const testTags = unique([...parentTags, ...toArray(options.tags)]);
741
+ const tagsOptions = testTags.map((tag) => {
742
+ var _runner$config$tags;
743
+ const tagDefinition = (_runner$config$tags = runner.config.tags) === null || _runner$config$tags === void 0 ? void 0 : _runner$config$tags.find((t) => t.name === tag);
744
+ if (!tagDefinition && runner.config.strictTags) {
745
+ throw createNoTagsError(runner.config.tags, tag);
746
+ }
747
+ return tagDefinition;
748
+ }).filter((r) => r != null).sort((tag1, tag2) => (tag2.priority ?? POSITIVE_INFINITY) - (tag1.priority ?? POSITIVE_INFINITY)).reduce((acc, tag) => {
749
+ const { name, description, priority, ...options } = tag;
750
+ Object.assign(acc, options);
751
+ return acc;
752
+ }, {});
753
+ options = {
754
+ ...tagsOptions,
755
+ ...options
756
+ };
757
+ const timeout = options.timeout ?? runner.config.testTimeout;
759
758
  const task = {
760
759
  id: "",
761
760
  name,
762
- fullName: createTaskName([(currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullName) ?? ((_collectorContext$cur2 = collectorContext.currentSuite) === null || _collectorContext$cur2 === void 0 || (_collectorContext$cur2 = _collectorContext$cur2.file) === null || _collectorContext$cur2 === void 0 ? void 0 : _collectorContext$cur2.fullName), name]),
761
+ fullName: createTaskName([(currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullName) ?? ((_collectorContext$cur3 = collectorContext.currentSuite) === null || _collectorContext$cur3 === void 0 || (_collectorContext$cur3 = _collectorContext$cur3.file) === null || _collectorContext$cur3 === void 0 ? void 0 : _collectorContext$cur3.fullName), name]),
763
762
  fullTestName: createTaskName([currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullTestName, name]),
764
763
  suite: currentSuite,
765
764
  each: options.each,
766
765
  fails: options.fails,
767
766
  context: undefined,
768
767
  type: "test",
769
- file: (currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.file) ?? ((_collectorContext$cur3 = collectorContext.currentSuite) === null || _collectorContext$cur3 === void 0 ? void 0 : _collectorContext$cur3.file),
768
+ file: (currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.file) ?? ((_collectorContext$cur4 = collectorContext.currentSuite) === null || _collectorContext$cur4 === void 0 ? void 0 : _collectorContext$cur4.file),
770
769
  timeout,
771
770
  retry: options.retry ?? runner.config.retry,
772
771
  repeats: options.repeats,
773
772
  mode: options.only ? "only" : options.skip ? "skip" : options.todo ? "todo" : "run",
774
773
  meta: options.meta ?? Object.create(null),
775
774
  annotations: [],
776
- artifacts: []
775
+ artifacts: [],
776
+ tags: testTags
777
777
  };
778
778
  const handler = options.handler;
779
779
  if (task.mode === "run" && !handler) {
@@ -818,8 +818,14 @@ function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions
818
818
  options = Object.assign({}, suiteOptions, options);
819
819
  }
820
820
  // inherit concurrent / sequential from suite
821
- options.concurrent = this.concurrent || !this.sequential && (options === null || options === void 0 ? void 0 : options.concurrent);
822
- options.sequential = this.sequential || !this.concurrent && (options === null || options === void 0 ? void 0 : options.sequential);
821
+ const concurrent = this.concurrent ?? (!this.sequential && (options === null || options === void 0 ? void 0 : options.concurrent));
822
+ if (options.concurrent != null && concurrent != null) {
823
+ options.concurrent = concurrent;
824
+ }
825
+ const sequential = this.sequential ?? (!this.concurrent && (options === null || options === void 0 ? void 0 : options.sequential));
826
+ if (options.sequential != null && sequential != null) {
827
+ options.sequential = sequential;
828
+ }
823
829
  const test = task(formatName(name), {
824
830
  ...this,
825
831
  ...options,
@@ -854,25 +860,29 @@ function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions
854
860
  getHooks(suite)[name].push(...fn);
855
861
  }
856
862
  function initSuite(includeLocation) {
857
- var _collectorContext$cur4, _collectorContext$cur5, _collectorContext$cur6;
863
+ var _collectorContext$cur5, _collectorContext$cur6, _collectorContext$cur7, _collectorContext$cur8;
858
864
  if (typeof suiteOptions === "number") {
859
865
  suiteOptions = { timeout: suiteOptions };
860
866
  }
861
- const currentSuite = (_collectorContext$cur4 = collectorContext.currentSuite) === null || _collectorContext$cur4 === void 0 ? void 0 : _collectorContext$cur4.suite;
867
+ const currentSuite = (_collectorContext$cur5 = collectorContext.currentSuite) === null || _collectorContext$cur5 === void 0 ? void 0 : _collectorContext$cur5.suite;
868
+ const parentTask = currentSuite ?? ((_collectorContext$cur6 = collectorContext.currentSuite) === null || _collectorContext$cur6 === void 0 ? void 0 : _collectorContext$cur6.file);
869
+ const suiteTags = toArray(suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.tags);
870
+ validateTags(runner.config, suiteTags);
862
871
  suite = {
863
872
  id: "",
864
873
  type: "suite",
865
874
  name,
866
- fullName: createTaskName([(currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullName) ?? ((_collectorContext$cur5 = collectorContext.currentSuite) === null || _collectorContext$cur5 === void 0 || (_collectorContext$cur5 = _collectorContext$cur5.file) === null || _collectorContext$cur5 === void 0 ? void 0 : _collectorContext$cur5.fullName), name]),
875
+ fullName: createTaskName([(currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullName) ?? ((_collectorContext$cur7 = collectorContext.currentSuite) === null || _collectorContext$cur7 === void 0 || (_collectorContext$cur7 = _collectorContext$cur7.file) === null || _collectorContext$cur7 === void 0 ? void 0 : _collectorContext$cur7.fullName), name]),
867
876
  fullTestName: createTaskName([currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullTestName, name]),
868
877
  suite: currentSuite,
869
878
  mode,
870
879
  each,
871
- file: (currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.file) ?? ((_collectorContext$cur6 = collectorContext.currentSuite) === null || _collectorContext$cur6 === void 0 ? void 0 : _collectorContext$cur6.file),
880
+ file: (currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.file) ?? ((_collectorContext$cur8 = collectorContext.currentSuite) === null || _collectorContext$cur8 === void 0 ? void 0 : _collectorContext$cur8.file),
872
881
  shuffle: suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.shuffle,
873
882
  tasks: [],
874
883
  meta: Object.create(null),
875
- concurrent: suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.concurrent
884
+ concurrent: suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.concurrent,
885
+ tags: unique([...(parentTask === null || parentTask === void 0 ? void 0 : parentTask.tags) || [], ...suiteTags])
876
886
  };
877
887
  if (runner && includeLocation && runner.config.includeTaskLocation) {
878
888
  const limit = Error.stackTraceLimit;
@@ -930,25 +940,33 @@ function createSuite() {
930
940
  if (getCurrentTest()) {
931
941
  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.");
932
942
  }
933
- let mode = this.only ? "only" : this.skip ? "skip" : this.todo ? "todo" : "run";
934
943
  const currentSuite = collectorContext.currentSuite || defaultSuite;
935
944
  let { options, handler: factory } = parseArguments(factoryOrOptions, optionsOrFactory);
936
- if (mode === "run" && !factory) {
937
- mode = "todo";
938
- }
939
945
  const isConcurrentSpecified = options.concurrent || this.concurrent || options.sequential === false;
940
946
  const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false;
941
947
  // inherit options from current suite
942
948
  options = {
943
949
  ...currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.options,
944
- ...options,
945
- shuffle: this.shuffle ?? options.shuffle ?? (currentSuite === null || currentSuite === void 0 || (_currentSuite$options = currentSuite.options) === null || _currentSuite$options === void 0 ? void 0 : _currentSuite$options.shuffle) ?? (runner === null || runner === void 0 ? void 0 : runner.config.sequence.shuffle)
950
+ ...options
946
951
  };
952
+ const shuffle = this.shuffle ?? options.shuffle ?? (currentSuite === null || currentSuite === void 0 || (_currentSuite$options = currentSuite.options) === null || _currentSuite$options === void 0 ? void 0 : _currentSuite$options.shuffle) ?? (runner === null || runner === void 0 ? void 0 : runner.config.sequence.shuffle);
953
+ if (shuffle != null) {
954
+ options.shuffle = shuffle;
955
+ }
956
+ let mode = this.only ?? options.only ? "only" : this.skip ?? options.skip ? "skip" : this.todo ?? options.todo ? "todo" : "run";
957
+ // passed as test(name), assume it's a "todo"
958
+ if (mode === "run" && !factory) {
959
+ mode = "todo";
960
+ }
947
961
  // inherit concurrent / sequential from suite
948
962
  const isConcurrent = isConcurrentSpecified || options.concurrent && !isSequentialSpecified;
949
963
  const isSequential = isSequentialSpecified || options.sequential && !isConcurrentSpecified;
950
- options.concurrent = isConcurrent && !isSequential;
951
- options.sequential = isSequential && !isConcurrent;
964
+ if (isConcurrent != null) {
965
+ options.concurrent = isConcurrent && !isSequential;
966
+ }
967
+ if (isSequential != null) {
968
+ options.sequential = isSequential && !isConcurrent;
969
+ }
952
970
  return createSuiteCollector(formatName(name), factory, mode, this.each, options, currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fixtures());
953
971
  }
954
972
  suiteFn.each = function(cases, ...args) {
@@ -1360,18 +1378,24 @@ async function collectTests(specs, runner) {
1360
1378
  const files = [];
1361
1379
  const config = runner.config;
1362
1380
  const $ = runner.trace;
1381
+ let defaultTagsFilter;
1363
1382
  for (const spec of specs) {
1364
1383
  const filepath = typeof spec === "string" ? spec : spec.filepath;
1365
1384
  await $("collect_spec", { "code.file.path": filepath }, async () => {
1366
- var _runner$onCollectStar;
1367
1385
  const testLocations = typeof spec === "string" ? undefined : spec.testLocations;
1386
+ const testNamePattern = typeof spec === "string" ? undefined : spec.testNamePattern;
1387
+ const testIds = typeof spec === "string" ? undefined : spec.testIds;
1388
+ const testTagsFilter = typeof spec === "object" && spec.testTagsFilter ? createTagsFilter(spec.testTagsFilter, config.tags) : undefined;
1389
+ const fileTags = typeof spec === "string" ? [] : spec.fileTags || [];
1368
1390
  const file = createFileTask(filepath, config.root, config.name, runner.pool, runner.viteEnvironment);
1369
1391
  setFileContext(file, Object.create(null));
1392
+ file.tags = fileTags;
1370
1393
  file.shuffle = config.sequence.shuffle;
1371
- (_runner$onCollectStar = runner.onCollectStart) === null || _runner$onCollectStar === void 0 ? void 0 : _runner$onCollectStar.call(runner, file);
1372
- clearCollectorContext(file, runner);
1373
1394
  try {
1374
- var _runner$getImportDura;
1395
+ var _runner$onCollectStar, _runner$getImportDura;
1396
+ validateTags(runner.config, fileTags);
1397
+ (_runner$onCollectStar = runner.onCollectStart) === null || _runner$onCollectStar === void 0 ? void 0 : _runner$onCollectStar.call(runner, file);
1398
+ clearCollectorContext(file, runner);
1375
1399
  const setupFiles = toArray(config.setupFiles);
1376
1400
  if (setupFiles.length) {
1377
1401
  const setupStart = now$1();
@@ -1420,7 +1444,10 @@ async function collectTests(specs, runner) {
1420
1444
  }
1421
1445
  calculateSuiteHash(file);
1422
1446
  const hasOnlyTasks = someTasksAreOnly(file);
1423
- interpretTaskModes(file, config.testNamePattern, testLocations, hasOnlyTasks, false, config.allowOnly);
1447
+ if (!testTagsFilter && !defaultTagsFilter && config.tagsFilter) {
1448
+ defaultTagsFilter = createTagsFilter(config.tagsFilter, config.tags);
1449
+ }
1450
+ interpretTaskModes(file, testNamePattern ?? config.testNamePattern, testLocations, testIds, testTagsFilter ?? defaultTagsFilter, hasOnlyTasks, false, config.allowOnly);
1424
1451
  if (file.mode === "queued") {
1425
1452
  file.mode = "run";
1426
1453
  }
@@ -1440,6 +1467,37 @@ function mergeHooks(baseHooks, hooks) {
1440
1467
  const now = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now;
1441
1468
  const unixNow = Date.now;
1442
1469
  const { clearTimeout, setTimeout } = getSafeTimers();
1470
+ /**
1471
+ * Normalizes retry configuration to extract individual values.
1472
+ * Handles both number and object forms.
1473
+ */
1474
+ function getRetryCount(retry) {
1475
+ if (retry === undefined) {
1476
+ return 0;
1477
+ }
1478
+ if (typeof retry === "number") {
1479
+ return retry;
1480
+ }
1481
+ return retry.count ?? 0;
1482
+ }
1483
+ function getRetryDelay(retry) {
1484
+ if (retry === undefined) {
1485
+ return 0;
1486
+ }
1487
+ if (typeof retry === "number") {
1488
+ return 0;
1489
+ }
1490
+ return retry.delay ?? 0;
1491
+ }
1492
+ function getRetryCondition(retry) {
1493
+ if (retry === undefined) {
1494
+ return undefined;
1495
+ }
1496
+ if (typeof retry === "number") {
1497
+ return undefined;
1498
+ }
1499
+ return retry.condition;
1500
+ }
1443
1501
  function updateSuiteHookState(task, name, state, runner) {
1444
1502
  if (!task.result) {
1445
1503
  task.result = { state: "run" };
@@ -1603,6 +1661,25 @@ async function callCleanupHooks(runner, cleanups) {
1603
1661
  }
1604
1662
  }
1605
1663
  }
1664
+ /**
1665
+ * Determines if a test should be retried based on its retryCondition configuration
1666
+ */
1667
+ function passesRetryCondition(test, errors) {
1668
+ const condition = getRetryCondition(test.retry);
1669
+ if (!errors || errors.length === 0) {
1670
+ return false;
1671
+ }
1672
+ if (!condition) {
1673
+ return true;
1674
+ }
1675
+ const error = errors[errors.length - 1];
1676
+ if (condition instanceof RegExp) {
1677
+ return condition.test(error.message || "");
1678
+ } else if (typeof condition === "function") {
1679
+ return condition(error);
1680
+ }
1681
+ return false;
1682
+ }
1606
1683
  async function runTest(test, runner) {
1607
1684
  var _runner$onBeforeRunTa, _test$result, _runner$onAfterRunTas;
1608
1685
  await ((_runner$onBeforeRunTa = runner.onBeforeRunTask) === null || _runner$onBeforeRunTa === void 0 ? void 0 : _runner$onBeforeRunTa.call(runner, test));
@@ -1631,7 +1708,7 @@ async function runTest(test, runner) {
1631
1708
  const $ = runner.trace;
1632
1709
  const repeats = test.repeats ?? 0;
1633
1710
  for (let repeatCount = 0; repeatCount <= repeats; repeatCount++) {
1634
- const retry = test.retry ?? 0;
1711
+ const retry = getRetryCount(test.retry);
1635
1712
  for (let retryCount = 0; retryCount <= retry; retryCount++) {
1636
1713
  var _test$onFinished, _test$onFailed, _runner$onAfterRetryT, _test$result2, _test$result3;
1637
1714
  let beforeEachCleanups = [];
@@ -1712,9 +1789,16 @@ async function runTest(test, runner) {
1712
1789
  break;
1713
1790
  }
1714
1791
  if (retryCount < retry) {
1715
- // reset state when retry test
1792
+ const shouldRetry = passesRetryCondition(test, test.result.errors);
1793
+ if (!shouldRetry) {
1794
+ break;
1795
+ }
1716
1796
  test.result.state = "run";
1717
1797
  test.result.retryCount = (test.result.retryCount ?? 0) + 1;
1798
+ const delay = getRetryDelay(test.retry);
1799
+ if (delay > 0) {
1800
+ await new Promise((resolve) => setTimeout(resolve, delay));
1801
+ }
1718
1802
  }
1719
1803
  // update retry info
1720
1804
  updateTask("test-retried", test, runner);
@@ -1,5 +1,205 @@
1
+ import { DiffOptions } from '@vitest/utils/diff';
1
2
  import { TestError, Awaitable } from '@vitest/utils';
2
3
 
4
+ /**
5
+ * This is a subset of Vitest config that's required for the runner to work.
6
+ */
7
+ interface VitestRunnerConfig {
8
+ root: string;
9
+ setupFiles: string[];
10
+ name?: string;
11
+ passWithNoTests: boolean;
12
+ testNamePattern?: RegExp;
13
+ allowOnly?: boolean;
14
+ sequence: {
15
+ shuffle?: boolean;
16
+ concurrent?: boolean;
17
+ seed: number;
18
+ hooks: SequenceHooks;
19
+ setupFiles: SequenceSetupFiles;
20
+ };
21
+ chaiConfig?: {
22
+ truncateThreshold?: number;
23
+ };
24
+ maxConcurrency: number;
25
+ testTimeout: number;
26
+ hookTimeout: number;
27
+ retry: SerializableRetry;
28
+ includeTaskLocation?: boolean;
29
+ diffOptions?: DiffOptions;
30
+ tags: TestTagDefinition[];
31
+ tagsFilter?: string[];
32
+ strictTags: boolean;
33
+ }
34
+ /**
35
+ * Possible options to run a single file in a test.
36
+ */
37
+ interface FileSpecification {
38
+ filepath: string;
39
+ fileTags?: string[];
40
+ testLocations: number[] | undefined;
41
+ testNamePattern: RegExp | undefined;
42
+ testTagsFilter: string[] | undefined;
43
+ testIds: string[] | undefined;
44
+ }
45
+ interface TestTagDefinition extends Omit<TestOptions, "tags" | "shuffle"> {
46
+ /**
47
+ * The name of the tag. This is what you use in the `tags` array in tests.
48
+ */
49
+ name: keyof TestTags extends never ? string : TestTags[keyof TestTags];
50
+ /**
51
+ * A description for the tag. This will be shown in the CLI help and UI.
52
+ */
53
+ description?: string;
54
+ /**
55
+ * Priority for merging options when multiple tags with the same options are applied to a test.
56
+ *
57
+ * Lower number means higher priority. E.g., priority 1 takes precedence over priority 3.
58
+ */
59
+ priority?: number;
60
+ }
61
+ type VitestRunnerImportSource = "collect" | "setup";
62
+ interface VitestRunnerConstructor {
63
+ new (config: VitestRunnerConfig): VitestRunner;
64
+ }
65
+ type CancelReason = "keyboard-input" | "test-failure" | (string & Record<string, never>);
66
+ interface VitestRunner {
67
+ /**
68
+ * First thing that's getting called before actually collecting and running tests.
69
+ */
70
+ onBeforeCollect?: (paths: string[]) => unknown;
71
+ /**
72
+ * Called after the file task was created but not collected yet.
73
+ */
74
+ onCollectStart?: (file: File) => unknown;
75
+ /**
76
+ * Called after collecting tests and before "onBeforeRun".
77
+ */
78
+ onCollected?: (files: File[]) => unknown;
79
+ /**
80
+ * Called when test runner should cancel next test runs.
81
+ * Runner should listen for this method and mark tests and suites as skipped in
82
+ * "onBeforeRunSuite" and "onBeforeRunTask" when called.
83
+ */
84
+ cancel?: (reason: CancelReason) => unknown;
85
+ /**
86
+ * Called before running a single test. Doesn't have "result" yet.
87
+ */
88
+ onBeforeRunTask?: (test: Test) => unknown;
89
+ /**
90
+ * Called before actually running the test function. Already has "result" with "state" and "startTime".
91
+ */
92
+ onBeforeTryTask?: (test: Test, options: {
93
+ retry: number;
94
+ repeats: number;
95
+ }) => unknown;
96
+ /**
97
+ * When the task has finished running, but before cleanup hooks are called
98
+ */
99
+ onTaskFinished?: (test: Test) => unknown;
100
+ /**
101
+ * Called after result and state are set.
102
+ */
103
+ onAfterRunTask?: (test: Test) => unknown;
104
+ /**
105
+ * Called right after running the test function. Doesn't have new state yet. Will not be called, if the test function throws.
106
+ */
107
+ onAfterTryTask?: (test: Test, options: {
108
+ retry: number;
109
+ repeats: number;
110
+ }) => unknown;
111
+ /**
112
+ * Called after the retry resolution happend. Unlike `onAfterTryTask`, the test now has a new state.
113
+ * All `after` hooks were also called by this point.
114
+ */
115
+ onAfterRetryTask?: (test: Test, options: {
116
+ retry: number;
117
+ repeats: number;
118
+ }) => unknown;
119
+ /**
120
+ * Called before running a single suite. Doesn't have "result" yet.
121
+ */
122
+ onBeforeRunSuite?: (suite: Suite) => unknown;
123
+ /**
124
+ * Called after running a single suite. Has state and result.
125
+ */
126
+ onAfterRunSuite?: (suite: Suite) => unknown;
127
+ /**
128
+ * If defined, will be called instead of usual Vitest suite partition and handling.
129
+ * "before" and "after" hooks will not be ignored.
130
+ */
131
+ runSuite?: (suite: Suite) => Promise<void>;
132
+ /**
133
+ * If defined, will be called instead of usual Vitest handling. Useful, if you have your custom test function.
134
+ * "before" and "after" hooks will not be ignored.
135
+ */
136
+ runTask?: (test: Test) => Promise<void>;
137
+ /**
138
+ * Called, when a task is updated. The same as "onTaskUpdate" in a reporter, but this is running in the same thread as tests.
139
+ */
140
+ onTaskUpdate?: (task: TaskResultPack[], events: TaskEventPack[]) => Promise<void>;
141
+ /**
142
+ * Called when annotation is added via the `context.annotate` method.
143
+ */
144
+ onTestAnnotate?: (test: Test, annotation: TestAnnotation) => Promise<TestAnnotation>;
145
+ /**
146
+ * @experimental
147
+ *
148
+ * Called when artifacts are recorded on tests via the `recordArtifact` utility.
149
+ */
150
+ onTestArtifactRecord?: <Artifact extends TestArtifact>(test: Test, artifact: Artifact) => Promise<Artifact>;
151
+ /**
152
+ * Called before running all tests in collected paths.
153
+ */
154
+ onBeforeRunFiles?: (files: File[]) => unknown;
155
+ /**
156
+ * Called right after running all tests in collected paths.
157
+ */
158
+ onAfterRunFiles?: (files: File[]) => unknown;
159
+ /**
160
+ * Called when new context for a test is defined. Useful if you want to add custom properties to the context.
161
+ * If you only want to define custom context, consider using "beforeAll" in "setupFiles" instead.
162
+ *
163
+ * @see https://vitest.dev/advanced/runner#your-task-function
164
+ */
165
+ extendTaskContext?: (context: TestContext) => TestContext;
166
+ /**
167
+ * Called when test and setup files are imported. Can be called in two situations: when collecting tests and when importing setup files.
168
+ */
169
+ importFile: (filepath: string, source: VitestRunnerImportSource) => unknown;
170
+ /**
171
+ * Function that is called when the runner attempts to get the value when `test.extend` is used with `{ injected: true }`
172
+ */
173
+ injectValue?: (key: string) => unknown;
174
+ /**
175
+ * Gets the time spent importing each individual non-externalized file that Vitest collected.
176
+ */
177
+ getImportDurations?: () => Record<string, ImportDuration>;
178
+ /**
179
+ * Publicly available configuration.
180
+ */
181
+ config: VitestRunnerConfig;
182
+ /**
183
+ * The name of the current pool. Can affect how stack trace is inferred on the server side.
184
+ */
185
+ pool?: string;
186
+ /**
187
+ * The current Vite environment that processes the files on the server.
188
+ */
189
+ viteEnvironment?: string;
190
+ /**
191
+ * Return the worker context for fixtures specified with `scope: 'worker'`
192
+ */
193
+ getWorkerContext?: () => Record<string, unknown>;
194
+ onCleanupWorkerContext?: (cleanup: () => unknown) => void;
195
+ trace?<T>(name: string, cb: () => T): T;
196
+ trace?<T>(name: string, attributes: Record<string, any>, cb: () => T): T;
197
+ /** @private */
198
+ _currentTaskStartTime?: number;
199
+ /** @private */
200
+ _currentTaskTimeout?: number;
201
+ }
202
+
3
203
  interface FixtureItem extends FixtureOptions {
4
204
  prop: string;
5
205
  value: any;
@@ -226,10 +426,12 @@ interface TaskBase {
226
426
  */
227
427
  result?: TaskResult;
228
428
  /**
229
- * The amount of times the task should be retried if it fails.
429
+ * Retry configuration for the task.
430
+ * - If a number, specifies how many times to retry
431
+ * - If an object, allows fine-grained retry control
230
432
  * @default 0
231
433
  */
232
- retry?: number;
434
+ retry?: Retry;
233
435
  /**
234
436
  * The amount of times the task should be repeated after the successful run.
235
437
  * If the task fails, it will not be retried unless `retry` is specified.
@@ -251,6 +453,10 @@ interface TaskBase {
251
453
  * @experimental
252
454
  */
253
455
  dynamic?: boolean;
456
+ /**
457
+ * Custom tags of the task. Useful for filtering tasks.
458
+ */
459
+ tags?: string[];
254
460
  }
255
461
  interface TaskPopulated extends TaskBase {
256
462
  /**
@@ -448,18 +654,68 @@ type ChainableTestAPI<ExtraContext = object> = ChainableFunction<"concurrent" |
448
654
  for: TestForFunction<ExtraContext>;
449
655
  }>;
450
656
  type TestCollectorOptions = Omit<TestOptions, "shuffle">;
657
+ /**
658
+ * Retry configuration for tests.
659
+ * Can be a number for simple retry count, or an object for advanced retry control.
660
+ */
661
+ type Retry = number | {
662
+ /**
663
+ * The number of times to retry the test if it fails.
664
+ * @default 0
665
+ */
666
+ count?: number;
667
+ /**
668
+ * Delay in milliseconds between retry attempts.
669
+ * @default 0
670
+ */
671
+ delay?: number;
672
+ /**
673
+ * Condition to determine if a test should be retried based on the error.
674
+ * - If a RegExp, it is tested against the error message
675
+ * - If a function, called with the TestError object; return true to retry
676
+ *
677
+ * NOTE: Functions can only be used in test files, not in vitest.config.ts,
678
+ * because the configuration is serialized when passed to worker threads.
679
+ *
680
+ * @default undefined (retry on all errors)
681
+ */
682
+ condition?: RegExp | ((error: TestError) => boolean);
683
+ };
684
+ /**
685
+ * Serializable retry configuration (used in config files).
686
+ * Functions cannot be serialized, so only string conditions are allowed.
687
+ */
688
+ type SerializableRetry = number | {
689
+ /**
690
+ * The number of times to retry the test if it fails.
691
+ * @default 0
692
+ */
693
+ count?: number;
694
+ /**
695
+ * Delay in milliseconds between retry attempts.
696
+ * @default 0
697
+ */
698
+ delay?: number;
699
+ /**
700
+ * Condition to determine if a test should be retried based on the error.
701
+ * Must be a RegExp tested against the error message.
702
+ *
703
+ * @default undefined (retry on all errors)
704
+ */
705
+ condition?: RegExp;
706
+ };
451
707
  interface TestOptions {
452
708
  /**
453
709
  * Test timeout.
454
710
  */
455
711
  timeout?: number;
456
712
  /**
457
- * Times to retry the test if fails. Useful for making flaky tests more stable.
458
- * When retries is up, the last test error will be thrown.
459
- *
713
+ * Retry configuration for the test.
714
+ * - If a number, specifies how many times to retry
715
+ * - If an object, allows fine-grained retry control
460
716
  * @default 0
461
717
  */
462
- retry?: number;
718
+ retry?: Retry;
463
719
  /**
464
720
  * How many times the test will run again.
465
721
  * Only inner tests will repeat if set on `describe()`, nested `describe()` will inherit parent's repeat by default.
@@ -478,10 +734,6 @@ interface TestOptions {
478
734
  */
479
735
  sequential?: boolean;
480
736
  /**
481
- * Whether the tasks of the suite run in a random order.
482
- */
483
- shuffle?: boolean;
484
- /**
485
737
  * Whether the test should be skipped.
486
738
  */
487
739
  skip?: boolean;
@@ -497,6 +749,17 @@ interface TestOptions {
497
749
  * Whether the test is expected to fail. If it does, the test will pass, otherwise it will fail.
498
750
  */
499
751
  fails?: boolean;
752
+ /**
753
+ * Custom tags of the test. Useful for filtering tests.
754
+ */
755
+ tags?: keyof TestTags extends never ? string[] | string : TestTags[keyof TestTags] | TestTags[keyof TestTags][];
756
+ }
757
+ interface TestTags {}
758
+ interface SuiteOptions extends TestOptions {
759
+ /**
760
+ * Whether the tasks of the suite run in a random order.
761
+ */
762
+ shuffle?: boolean;
500
763
  }
501
764
  interface ExtendedAPI<ExtraContext> {
502
765
  skipIf: (condition: any) => ChainableTestAPI<ExtraContext>;
@@ -551,7 +814,7 @@ type Fixtures<
551
814
  type InferFixturesTypes<T> = T extends TestAPI<infer C> ? C : T;
552
815
  interface SuiteCollectorCallable<ExtraContext = object> {
553
816
  <OverrideExtraContext extends ExtraContext = ExtraContext>(name: string | Function, fn?: SuiteFactory<OverrideExtraContext>, options?: number): SuiteCollector<OverrideExtraContext>;
554
- <OverrideExtraContext extends ExtraContext = ExtraContext>(name: string | Function, options: TestOptions, fn?: SuiteFactory<OverrideExtraContext>): SuiteCollector<OverrideExtraContext>;
817
+ <OverrideExtraContext extends ExtraContext = ExtraContext>(name: string | Function, options: SuiteOptions, fn?: SuiteFactory<OverrideExtraContext>): SuiteCollector<OverrideExtraContext>;
555
818
  }
556
819
  type ChainableSuiteAPI<ExtraContext = object> = ChainableFunction<"concurrent" | "sequential" | "only" | "skip" | "todo" | "shuffle", SuiteCollectorCallable<ExtraContext>, {
557
820
  each: TestEachFunction;
@@ -602,7 +865,7 @@ interface TaskCustomOptions extends TestOptions {
602
865
  interface SuiteCollector<ExtraContext = object> {
603
866
  readonly name: string;
604
867
  readonly mode: RunMode;
605
- options?: TestOptions;
868
+ options?: SuiteOptions;
606
869
  type: "collector";
607
870
  test: TestAPI<ExtraContext>;
608
871
  tasks: (Suite | Test<ExtraContext> | SuiteCollector<ExtraContext>)[];
@@ -830,5 +1093,5 @@ interface TestArtifactRegistry {}
830
1093
  */
831
1094
  type TestArtifact = TestAnnotationArtifact | VisualRegressionArtifact | TestArtifactRegistry[keyof TestArtifactRegistry];
832
1095
 
833
- export { createChainable as c, afterAll as i, afterEach as j, beforeAll as k, beforeEach as l, onTestFinished as m, onTestFailed as o };
834
- export type { TestOptions as $, AfterAllListener as A, BeforeAllListener as B, ChainableFunction as C, TaskBase as D, TaskCustomOptions as E, File as F, TaskEventPack as G, TaskHook as H, ImportDuration as I, TaskMeta as J, TaskPopulated as K, TaskResult as L, TaskResultPack as M, TaskState as N, OnTestFailedHandler as O, TestAnnotation as P, TestAnnotationArtifact as Q, RunMode as R, Suite as S, Task as T, TestAnnotationLocation as U, TestArtifactBase as V, TestArtifactLocation as W, TestArtifactRegistry as X, TestAttachment as Y, TestContext as Z, TestFunction as _, Test as a, Use as a0, VisualRegressionArtifact as a1, TestArtifact as b, SuiteHooks as d, TaskUpdateEvent as e, TestAPI as f, SuiteAPI as g, SuiteCollector as h, AfterEachListener as n, BeforeEachListener as p, Fixture as q, FixtureFn as r, FixtureOptions as s, Fixtures as t, InferFixturesTypes as u, OnTestFinishedHandler as v, RuntimeContext as w, SequenceHooks as x, SequenceSetupFiles as y, SuiteFactory as z };
1096
+ export { createChainable as ab, afterAll as i, afterEach as j, beforeAll as k, beforeEach as l, onTestFinished as m, onTestFailed as o };
1097
+ export type { TestAnnotationLocation as $, AfterAllListener as A, BeforeAllListener as B, CancelReason as C, RunMode as D, RuntimeContext as E, FileSpecification as F, SequenceHooks as G, SequenceSetupFiles as H, ImportDuration as I, SerializableRetry as J, SuiteFactory as K, TaskBase as L, TaskCustomOptions as M, TaskEventPack as N, OnTestFailedHandler as O, TaskHook as P, TaskMeta as Q, Retry as R, Suite as S, TestArtifact as T, TaskPopulated as U, VitestRunner as V, TaskResult as W, TaskResultPack as X, TaskState as Y, TestAnnotation as Z, TestAnnotationArtifact as _, Test as a, TestArtifactBase as a0, TestArtifactLocation as a1, TestArtifactRegistry as a2, TestAttachment as a3, TestContext as a4, TestFunction as a5, TestOptions as a6, TestTags as a7, Use as a8, VisualRegressionArtifact as a9, ChainableFunction as aa, SuiteHooks as b, File as c, TaskUpdateEvent as d, Task as e, TestAPI as f, SuiteAPI as g, SuiteCollector as h, TestTagDefinition as n, VitestRunnerConfig as p, VitestRunnerConstructor as q, VitestRunnerImportSource as r, AfterEachListener as s, BeforeEachListener as t, Fixture as u, FixtureFn as v, FixtureOptions as w, Fixtures as x, InferFixturesTypes as y, OnTestFinishedHandler as z };
package/dist/types.d.ts CHANGED
@@ -1,183 +1,3 @@
1
- import { DiffOptions } from '@vitest/utils/diff';
2
- import { F as File, a as Test, S as Suite, M as TaskResultPack, G as TaskEventPack, P as TestAnnotation, b as TestArtifact, Z as TestContext, I as ImportDuration, x as SequenceHooks, y as SequenceSetupFiles } from './tasks.d-C7UxawJ9.js';
3
- export { A as AfterAllListener, n as AfterEachListener, B as BeforeAllListener, p as BeforeEachListener, q as Fixture, r as FixtureFn, s as FixtureOptions, t as Fixtures, u as InferFixturesTypes, O as OnTestFailedHandler, v as OnTestFinishedHandler, R as RunMode, w as RuntimeContext, g as SuiteAPI, h as SuiteCollector, z as SuiteFactory, d as SuiteHooks, T as Task, D as TaskBase, E as TaskCustomOptions, H as TaskHook, J as TaskMeta, K as TaskPopulated, L as TaskResult, N as TaskState, e as TaskUpdateEvent, f as TestAPI, Q as TestAnnotationArtifact, U as TestAnnotationLocation, V as TestArtifactBase, W as TestArtifactLocation, X as TestArtifactRegistry, Y as TestAttachment, _ as TestFunction, $ as TestOptions, a0 as Use, a1 as VisualRegressionArtifact } from './tasks.d-C7UxawJ9.js';
1
+ export { A as AfterAllListener, s as AfterEachListener, B as BeforeAllListener, t as BeforeEachListener, C as CancelReason, c as File, F as FileSpecification, u as Fixture, v as FixtureFn, w as FixtureOptions, x as Fixtures, I as ImportDuration, y as InferFixturesTypes, O as OnTestFailedHandler, z as OnTestFinishedHandler, R as Retry, D as RunMode, E as RuntimeContext, G as SequenceHooks, H as SequenceSetupFiles, J as SerializableRetry, S as Suite, g as SuiteAPI, h as SuiteCollector, K as SuiteFactory, b as SuiteHooks, e as Task, L as TaskBase, M as TaskCustomOptions, N as TaskEventPack, P as TaskHook, Q as TaskMeta, U as TaskPopulated, W as TaskResult, X as TaskResultPack, Y as TaskState, d as TaskUpdateEvent, a as Test, f as TestAPI, Z as TestAnnotation, _ as TestAnnotationArtifact, $ as TestAnnotationLocation, T as TestArtifact, a0 as TestArtifactBase, a1 as TestArtifactLocation, a2 as TestArtifactRegistry, a3 as TestAttachment, a4 as TestContext, a5 as TestFunction, a6 as TestOptions, n as TestTagDefinition, a7 as TestTags, a8 as Use, a9 as VisualRegressionArtifact, V as VitestRunner, p as VitestRunnerConfig, q as VitestRunnerConstructor, r as VitestRunnerImportSource } from './tasks.d-CLPU8HE4.js';
2
+ import '@vitest/utils/diff';
4
3
  import '@vitest/utils';
5
-
6
- /**
7
- * This is a subset of Vitest config that's required for the runner to work.
8
- */
9
- interface VitestRunnerConfig {
10
- root: string;
11
- setupFiles: string[];
12
- name?: string;
13
- passWithNoTests: boolean;
14
- testNamePattern?: RegExp;
15
- allowOnly?: boolean;
16
- sequence: {
17
- shuffle?: boolean;
18
- concurrent?: boolean;
19
- seed: number;
20
- hooks: SequenceHooks;
21
- setupFiles: SequenceSetupFiles;
22
- };
23
- chaiConfig?: {
24
- truncateThreshold?: number;
25
- };
26
- maxConcurrency: number;
27
- testTimeout: number;
28
- hookTimeout: number;
29
- retry: number;
30
- includeTaskLocation?: boolean;
31
- diffOptions?: DiffOptions;
32
- }
33
- /**
34
- * Possible options to run a single file in a test.
35
- */
36
- interface FileSpecification {
37
- filepath: string;
38
- testLocations: number[] | undefined;
39
- }
40
- type VitestRunnerImportSource = "collect" | "setup";
41
- interface VitestRunnerConstructor {
42
- new (config: VitestRunnerConfig): VitestRunner;
43
- }
44
- type CancelReason = "keyboard-input" | "test-failure" | (string & Record<string, never>);
45
- interface VitestRunner {
46
- /**
47
- * First thing that's getting called before actually collecting and running tests.
48
- */
49
- onBeforeCollect?: (paths: string[]) => unknown;
50
- /**
51
- * Called after the file task was created but not collected yet.
52
- */
53
- onCollectStart?: (file: File) => unknown;
54
- /**
55
- * Called after collecting tests and before "onBeforeRun".
56
- */
57
- onCollected?: (files: File[]) => unknown;
58
- /**
59
- * Called when test runner should cancel next test runs.
60
- * Runner should listen for this method and mark tests and suites as skipped in
61
- * "onBeforeRunSuite" and "onBeforeRunTask" when called.
62
- */
63
- cancel?: (reason: CancelReason) => unknown;
64
- /**
65
- * Called before running a single test. Doesn't have "result" yet.
66
- */
67
- onBeforeRunTask?: (test: Test) => unknown;
68
- /**
69
- * Called before actually running the test function. Already has "result" with "state" and "startTime".
70
- */
71
- onBeforeTryTask?: (test: Test, options: {
72
- retry: number;
73
- repeats: number;
74
- }) => unknown;
75
- /**
76
- * When the task has finished running, but before cleanup hooks are called
77
- */
78
- onTaskFinished?: (test: Test) => unknown;
79
- /**
80
- * Called after result and state are set.
81
- */
82
- onAfterRunTask?: (test: Test) => unknown;
83
- /**
84
- * Called right after running the test function. Doesn't have new state yet. Will not be called, if the test function throws.
85
- */
86
- onAfterTryTask?: (test: Test, options: {
87
- retry: number;
88
- repeats: number;
89
- }) => unknown;
90
- /**
91
- * Called after the retry resolution happend. Unlike `onAfterTryTask`, the test now has a new state.
92
- * All `after` hooks were also called by this point.
93
- */
94
- onAfterRetryTask?: (test: Test, options: {
95
- retry: number;
96
- repeats: number;
97
- }) => unknown;
98
- /**
99
- * Called before running a single suite. Doesn't have "result" yet.
100
- */
101
- onBeforeRunSuite?: (suite: Suite) => unknown;
102
- /**
103
- * Called after running a single suite. Has state and result.
104
- */
105
- onAfterRunSuite?: (suite: Suite) => unknown;
106
- /**
107
- * If defined, will be called instead of usual Vitest suite partition and handling.
108
- * "before" and "after" hooks will not be ignored.
109
- */
110
- runSuite?: (suite: Suite) => Promise<void>;
111
- /**
112
- * If defined, will be called instead of usual Vitest handling. Useful, if you have your custom test function.
113
- * "before" and "after" hooks will not be ignored.
114
- */
115
- runTask?: (test: Test) => Promise<void>;
116
- /**
117
- * Called, when a task is updated. The same as "onTaskUpdate" in a reporter, but this is running in the same thread as tests.
118
- */
119
- onTaskUpdate?: (task: TaskResultPack[], events: TaskEventPack[]) => Promise<void>;
120
- /**
121
- * Called when annotation is added via the `context.annotate` method.
122
- */
123
- onTestAnnotate?: (test: Test, annotation: TestAnnotation) => Promise<TestAnnotation>;
124
- /**
125
- * @experimental
126
- *
127
- * Called when artifacts are recorded on tests via the `recordArtifact` utility.
128
- */
129
- onTestArtifactRecord?: <Artifact extends TestArtifact>(test: Test, artifact: Artifact) => Promise<Artifact>;
130
- /**
131
- * Called before running all tests in collected paths.
132
- */
133
- onBeforeRunFiles?: (files: File[]) => unknown;
134
- /**
135
- * Called right after running all tests in collected paths.
136
- */
137
- onAfterRunFiles?: (files: File[]) => unknown;
138
- /**
139
- * Called when new context for a test is defined. Useful if you want to add custom properties to the context.
140
- * If you only want to define custom context, consider using "beforeAll" in "setupFiles" instead.
141
- *
142
- * @see https://vitest.dev/advanced/runner#your-task-function
143
- */
144
- extendTaskContext?: (context: TestContext) => TestContext;
145
- /**
146
- * Called when test and setup files are imported. Can be called in two situations: when collecting tests and when importing setup files.
147
- */
148
- importFile: (filepath: string, source: VitestRunnerImportSource) => unknown;
149
- /**
150
- * Function that is called when the runner attempts to get the value when `test.extend` is used with `{ injected: true }`
151
- */
152
- injectValue?: (key: string) => unknown;
153
- /**
154
- * Gets the time spent importing each individual non-externalized file that Vitest collected.
155
- */
156
- getImportDurations?: () => Record<string, ImportDuration>;
157
- /**
158
- * Publicly available configuration.
159
- */
160
- config: VitestRunnerConfig;
161
- /**
162
- * The name of the current pool. Can affect how stack trace is inferred on the server side.
163
- */
164
- pool?: string;
165
- /**
166
- * The current Vite environment that processes the files on the server.
167
- */
168
- viteEnvironment?: string;
169
- /**
170
- * Return the worker context for fixtures specified with `scope: 'worker'`
171
- */
172
- getWorkerContext?: () => Record<string, unknown>;
173
- onCleanupWorkerContext?: (cleanup: () => unknown) => void;
174
- trace?<T>(name: string, cb: () => T): T;
175
- trace?<T>(name: string, attributes: Record<string, any>, cb: () => T): T;
176
- /** @private */
177
- _currentTaskStartTime?: number;
178
- /** @private */
179
- _currentTaskTimeout?: number;
180
- }
181
-
182
- export { File, ImportDuration, SequenceHooks, SequenceSetupFiles, Suite, TaskEventPack, TaskResultPack, Test, TestAnnotation, TestArtifact, TestContext };
183
- export type { CancelReason, FileSpecification, VitestRunner, VitestRunnerConfig, VitestRunnerConstructor, VitestRunnerImportSource };
package/dist/utils.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- import { S as Suite, F as File, T as Task, a as Test } from './tasks.d-C7UxawJ9.js';
2
- export { C as ChainableFunction, c as createChainable } from './tasks.d-C7UxawJ9.js';
1
+ import { S as Suite, c as File, e as Task, n as TestTagDefinition, p as VitestRunnerConfig, a as Test } from './tasks.d-CLPU8HE4.js';
2
+ export { aa as ChainableFunction, ab as createChainable } from './tasks.d-CLPU8HE4.js';
3
3
  import { ParsedStack, Arrayable } from '@vitest/utils';
4
+ import '@vitest/utils/diff';
4
5
 
5
6
  /**
6
7
  * If any tasks been marked as `only`, mark all other tasks as `skip`.
7
8
  */
8
- declare function interpretTaskModes(file: Suite, namePattern?: string | RegExp, testLocations?: number[] | undefined, onlyMode?: boolean, parentIsOnly?: boolean, allowOnly?: boolean): void;
9
+ declare function interpretTaskModes(file: Suite, namePattern?: string | RegExp, testLocations?: number[] | undefined, testIds?: string[] | undefined, testTagsFilter?: ((testTags: string[]) => boolean) | undefined, onlyMode?: boolean, parentIsOnly?: boolean, allowOnly?: boolean): void;
9
10
  declare function someTasksAreOnly(suite: Suite): boolean;
10
11
  declare function generateHash(str: string): string;
11
12
  declare function calculateSuiteHash(parent: Suite): void;
@@ -31,6 +32,9 @@ declare function limitConcurrency(concurrency?: number): <
31
32
  */
32
33
  declare function partitionSuiteChildren(suite: Suite): Task[][];
33
34
 
35
+ declare function validateTags(config: VitestRunnerConfig, tags: string[]): void;
36
+ declare function createTagsFilter(tagsExpr: string[], availableTags: TestTagDefinition[]): (testTags: string[]) => boolean;
37
+
34
38
  declare function isTestCase(s: Task): s is Test;
35
39
  declare function getTests(suite: Arrayable<Task>): Test[];
36
40
  declare function getTasks(tasks?: Arrayable<Task>): Task[];
@@ -42,4 +46,4 @@ declare function getFullName(task: Task, separator?: string): string;
42
46
  declare function getTestName(task: Task, separator?: string): string;
43
47
  declare function createTaskName(names: readonly (string | undefined)[], separator?: string): string;
44
48
 
45
- export { calculateSuiteHash, createFileTask, createTaskName, findTestFileStackTrace, generateFileHash, generateHash, getFullName, getNames, getSuites, getTasks, getTestName, getTests, hasFailed, hasTests, interpretTaskModes, isTestCase, limitConcurrency, partitionSuiteChildren, someTasksAreOnly };
49
+ export { calculateSuiteHash, createFileTask, createTagsFilter, createTaskName, findTestFileStackTrace, generateFileHash, generateHash, getFullName, getNames, getSuites, getTasks, getTestName, getTests, hasFailed, hasTests, interpretTaskModes, isTestCase, limitConcurrency, partitionSuiteChildren, someTasksAreOnly, validateTags };
package/dist/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- export { a as calculateSuiteHash, c as createChainable, b as createFileTask, e as createTaskName, f as findTestFileStackTrace, g as generateFileHash, d as generateHash, h as getFullName, j as getNames, k as getSuites, m as getTasks, n as getTestName, o as getTests, q as hasFailed, r as hasTests, i as interpretTaskModes, t as isTestCase, l as limitConcurrency, p as partitionSuiteChildren, s as someTasksAreOnly } from './chunk-tasks.js';
1
+ export { a as calculateSuiteHash, c as createChainable, b as createFileTask, e as createTagsFilter, h as createTaskName, f as findTestFileStackTrace, g as generateFileHash, d as generateHash, j as getFullName, k as getNames, m as getSuites, n as getTasks, o as getTestName, q as getTests, r as hasFailed, t as hasTests, i as interpretTaskModes, u as isTestCase, l as limitConcurrency, p as partitionSuiteChildren, s as someTasksAreOnly, v as validateTags } from './chunk-tasks.js';
2
2
  import '@vitest/utils/error';
3
3
  import '@vitest/utils/source-map';
4
4
  import 'pathe';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/runner",
3
3
  "type": "module",
4
- "version": "4.0.17",
4
+ "version": "4.1.0-beta.2",
5
5
  "description": "Vitest test runner",
6
6
  "license": "MIT",
7
7
  "funding": "https://opencollective.com/vitest",
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "dependencies": {
41
41
  "pathe": "^2.0.3",
42
- "@vitest/utils": "4.0.17"
42
+ "@vitest/utils": "4.1.0-beta.2"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "premove dist && rollup -c",