From ee48b959eef46857e7bcc65691c72e4e8a2d32de Mon Sep 17 00:00:00 2001 From: Todor Andonov <45210624+todor-a@users.noreply.github.com> Date: Sun, 7 Jun 2026 17:53:14 +0300 Subject: [PATCH] feat(cli): add `--repeats` CLI option (#10504) --- docs/.vitepress/config.ts | 4 +++ docs/config/repeats.md | 14 ++++++++++ docs/guide/cli-generated.md | 7 +++++ packages/vitest/src/node/cli/cli-config.ts | 5 ++++ .../vitest/src/node/config/serializeConfig.ts | 1 + .../src/node/projects/resolveProjects.ts | 1 + packages/vitest/src/node/types/config.ts | 7 +++++ packages/vitest/src/runtime/config.ts | 1 + packages/vitest/src/runtime/runner/suite.ts | 2 +- .../fixtures/repeats/repeats-config.test.ts | 9 +++++++ test/e2e/test/cli-config.test.ts | 2 ++ test/e2e/test/repeats.test.ts | 26 +++++++++++++++++++ 12 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 docs/config/repeats.md create mode 100644 test/e2e/fixtures/repeats/repeats-config.test.ts create mode 100644 test/e2e/test/repeats.test.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 6a57443d8..c42839e96 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -507,6 +507,10 @@ export default ({ mode }: { mode: string }) => { text: 'retry', link: '/config/retry', }, + { + text: 'repeats', + link: '/config/repeats', + }, { text: 'onConsoleLog', link: '/config/onconsolelog', diff --git a/docs/config/repeats.md b/docs/config/repeats.md new file mode 100644 index 000000000..733644c5f --- /dev/null +++ b/docs/config/repeats.md @@ -0,0 +1,14 @@ +--- +title: repeats | Config +outline: deep +--- + +# repeats + +- **Type:** `number` +- **Default:** `0` +- **CLI:** `--repeats=` + +Repeat every test a specific number of times regardless of the result. A test that uses the [`repeats`](/api/test#repeats) test option takes precedence over this value. + +This is useful for verifying that tests are stable across multiple runs. If a test fails on any repetition, the whole test is reported as failed. diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index 7538e4f2e..9fe528713 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -637,6 +637,13 @@ Delay in milliseconds between retry attempts (default: `0`) Regex pattern to match error messages that should trigger a retry. Only errors matching this pattern will cause a retry (default: retry on all errors) +### repeats + +- **CLI:** `--repeats ` +- **Config:** [repeats](/config/repeats) + +Repeat every test a specific number of times regardless of the result (default: `0`) + ### diff.aAnnotation - **CLI:** `--diff.aAnnotation ` diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index e41c744d2..1348bea2a 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -630,6 +630,11 @@ export const cliOptionsConfig: VitestCLIOptions = { }, }, }, + repeats: { + description: + 'Repeat every test a specific number of times regardless of the result (default: `0`)', + argument: '', + }, diff: { description: 'DiffOptions object or a path to a module which exports DiffOptions object', diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts index 0ce56e9ad..b09e7fb39 100644 --- a/packages/vitest/src/node/config/serializeConfig.ts +++ b/packages/vitest/src/node/config/serializeConfig.ts @@ -45,6 +45,7 @@ export function serializeConfig(project: TestProject): SerializedConfig { // TODO: non serializable function? diff: config.diff, retry: config.retry, + repeats: config.repeats, disableConsoleIntercept: config.disableConsoleIntercept, root: config.root, name: config.name, diff --git a/packages/vitest/src/node/projects/resolveProjects.ts b/packages/vitest/src/node/projects/resolveProjects.ts index c739917ff..dccb6d191 100644 --- a/packages/vitest/src/node/projects/resolveProjects.ts +++ b/packages/vitest/src/node/projects/resolveProjects.ts @@ -52,6 +52,7 @@ export async function resolveProjects( 'expandSnapshotDiff', 'disableConsoleIntercept', 'retry', + 'repeats', 'testNamePattern', 'passWithNoTests', 'bail', diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 4f96e1092..307279a18 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -830,6 +830,13 @@ export interface InlineConfig { */ retry?: SerializableRetry + /** + * Repeat every test a specific number of times regardless of the result. + * + * @default 0 // Don't repeat + */ + repeats?: number + /** * Show full diff when snapshot fails instead of a patch. */ diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index 3493b2147..f7a348b37 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -31,6 +31,7 @@ export interface SerializedConfig { testTimeout: number hookTimeout: number retry: SerializableRetry + repeats?: number includeTaskLocation: boolean | undefined tags: TestTagDefinition[] tagsFilter: string[] | undefined diff --git a/packages/vitest/src/runtime/runner/suite.ts b/packages/vitest/src/runtime/runner/suite.ts index a616d46bd..e4670f00a 100644 --- a/packages/vitest/src/runtime/runner/suite.ts +++ b/packages/vitest/src/runtime/runner/suite.ts @@ -387,7 +387,7 @@ function createSuiteCollector( file: (currentSuite?.file ?? collectorContext.currentSuite?.file)!, timeout, retry: options.retry ?? runner.config.retry, - repeats: options.repeats, + repeats: options.repeats ?? runner.config.repeats, mode: options.only ? 'only' : options.skip diff --git a/test/e2e/fixtures/repeats/repeats-config.test.ts b/test/e2e/fixtures/repeats/repeats-config.test.ts new file mode 100644 index 000000000..a19bbcaeb --- /dev/null +++ b/test/e2e/fixtures/repeats/repeats-config.test.ts @@ -0,0 +1,9 @@ +import { expect, test } from 'vitest' + +test('uses repeats from config', () => { + expect(1 + 1).toBe(2) +}) + +test('test option overrides config', { repeats: 1 }, () => { + expect(1 + 1).toBe(2) +}) diff --git a/test/e2e/test/cli-config.test.ts b/test/e2e/test/cli-config.test.ts index 1f228d428..0311ea79c 100644 --- a/test/e2e/test/cli-config.test.ts +++ b/test/e2e/test/cli-config.test.ts @@ -27,6 +27,7 @@ it('correctly inherit from the cli', async () => { globals: true, expandSnapshotDiff: true, retry: 6, + repeats: 3, testNamePattern: 'math', passWithNoTests: true, bail: 100, @@ -50,6 +51,7 @@ it('correctly inherit from the cli', async () => { globals: true, expandSnapshotDiff: true, retry: 6, + repeats: 3, passWithNoTests: true, bail: 100, experimental: { diff --git a/test/e2e/test/repeats.test.ts b/test/e2e/test/repeats.test.ts new file mode 100644 index 000000000..7e11c28ec --- /dev/null +++ b/test/e2e/test/repeats.test.ts @@ -0,0 +1,26 @@ +import type { TestModule } from 'vitest/node' +import { resolve } from 'pathe' +import { expect, test } from 'vitest' +import { runVitest } from '../../test-utils' + +const root = resolve(__dirname, '..', 'fixtures', 'repeats') + +test('repeats config option is exposed to tests and repeats execution', async () => { + const { ctx } = await runVitest({ + root, + include: ['repeats-config.test.ts'], + repeats: 3, + }) + + const file = ctx!.state.getFiles()[0] + const testModule = ctx!.state.getReportedEntity(file)! as TestModule + const tests = [...testModule.children.allTests()] + + const fromConfig = tests.find(t => t.name === 'uses repeats from config')! + expect(fromConfig.options.repeats).toBe(3) + expect(fromConfig.diagnostic()!.repeatCount).toBe(3) + + const overridden = tests.find(t => t.name === 'test option overrides config')! + expect(overridden.options.repeats).toBe(1) + expect(overridden.diagnostic()!.repeatCount).toBe(1) +}) -- 2.51.2