diff --git a/docs/api/index.md b/docs/api/index.md index 50dc8555bbca..8119b974d173 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -654,7 +654,7 @@ You cannot use this syntax, when using Vitest as [type checker](/guide/testing-t - **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void` -`describe.sequential` in a suite marks every test as sequential. This is useful if you want to run tests in sequential within `describe.concurrent` or with the `--sequence.concurrent` command option. +`describe.sequential` in a suite marks every test as sequential. This is useful if you want to run tests in sequence within `describe.concurrent` or with the `--sequence.concurrent` command option. ```ts describe.concurrent('suite', () => { diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 4bc8f14d97b7..7bbba77a2b86 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -80,7 +80,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m meta: options.meta ?? Object.create(null), } const handler = options.handler - if (options.concurrent || (!sequential && (concurrent || runner.config.sequence.concurrent))) + if (options.concurrent || (!options.sequential && runner.config.sequence.concurrent)) task.concurrent = true if (shuffle) task.shuffle = true @@ -104,7 +104,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m return task } - const test = createTest(function (name: string | Function, fn = noop, options) { + const test = createTest(function (name: string | Function, fn = noop, options = {}) { if (typeof options === 'number') options = { timeout: options } @@ -112,6 +112,10 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m if (typeof suiteOptions === 'object') options = Object.assign({}, suiteOptions, options) + // inherit concurrent / sequential from suite + options.concurrent = this.concurrent || (!this.sequential && options?.concurrent) + options.sequential = this.sequential || (!this.concurrent && options?.sequential) + const test = task( formatName(name), { ...this, ...options, handler: fn as any }, @@ -188,7 +192,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m } function createSuite() { - function suiteFn(this: Record, name: string | Function, factory?: SuiteFactory, options?: number | TestOptions) { + function suiteFn(this: Record, name: string | Function, factory?: SuiteFactory, options: number | TestOptions = {}) { const mode: RunMode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run' const currentSuite = getCurrentSuite() @@ -199,7 +203,11 @@ function createSuite() { if (currentSuite?.options) options = { ...currentSuite.options, ...options } - return createSuiteCollector(formatName(name), factory, mode, this.concurrent, this.sequence, this.shuffle, this.each, options) + // inherit concurrent / sequential from current suite + options.concurrent = this.concurrent || (!this.sequential && options?.concurrent) + options.sequential = this.sequential || (!this.concurrent && options?.sequential) + + return createSuiteCollector(formatName(name), factory, mode, this.concurrent, this.sequential, this.shuffle, this.each, options) } suiteFn.each = function(this: { withContext: () => SuiteAPI; setContext: (key: string, value: boolean | undefined) => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 275d109138d7..dcc2d66725c1 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -178,6 +178,16 @@ export interface TestOptions { * @default 0 */ repeats?: number + /** + * Whether tests run concurrently. + * Tests inherit `concurrent` from `describe()` and nested `describe()` will inherit from parent's `concurrent`. + */ + concurrent?: boolean + /** + * Whether tests run sequentially. + * Tests inherit `sequential` from `describe()` and nested `describe()` will inherit from parent's `sequential`. + */ + sequential?: boolean } interface ExtendedAPI { diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index a2c9fb4247fa..aaf2c210a585 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -813,6 +813,11 @@ export type RuntimeConfig = Pick< | 'restoreMocks' | 'fakeTimers' | 'maxConcurrency' -> & { sequence?: { hooks?: SequenceHooks } } +> & { + sequence?: { + concurrent?: boolean + hooks?: SequenceHooks + } +} export type { UserWorkspaceConfig } from '../config' diff --git a/test/core/test/sequential-sequence-concurrent.test.ts b/test/core/test/sequential-sequence-concurrent.test.ts new file mode 100644 index 000000000000..1de8a482aada --- /dev/null +++ b/test/core/test/sequential-sequence-concurrent.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test, vi } from 'vitest' + +vi.setConfig({ + sequence: { + concurrent: true, + }, +}) + +const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)) + +let count = 0 + +describe.sequential('running sequential suite when sequence.concurrent is true', () => { + test('first test completes first', async ({ task }) => { + await delay(50) + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(1) + }) + + test('second test completes second', ({ task }) => { + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(2) + }) +}) diff --git a/test/core/test/sequential.test.ts b/test/core/test/sequential.test.ts index d134e9643680..502943e9fa68 100644 --- a/test/core/test/sequential.test.ts +++ b/test/core/test/sequential.test.ts @@ -2,19 +2,59 @@ import { describe, expect, test } from 'vitest' const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)) -let count = 0 - -describe.concurrent('', () => { - describe.sequential('', () => { - test('should pass', async ({ task }) => { - await delay(50) - expect(task.concurrent).toBeFalsy() - expect(++count).toBe(1) - }) - - test('should pass', ({ task }) => { - expect(task.concurrent).toBeFalsy() - expect(++count).toBe(2) - }) +function assertSequential() { + let count = 0 + + test('first test completes first', async ({ task }) => { + await delay(50) + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(1) + }) + + test('second test completes second', ({ task }) => { + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(2) + }) + + test.concurrent('third test completes fourth', async ({ task }) => { + await delay(50) + expect(task.concurrent).toBe(true) + expect(++count).toBe(4) + }) + + test.concurrent('fourth test completes third', ({ task }) => { + expect(task.concurrent).toBe(true) + expect(++count).toBe(3) + }) +} + +function assertConcurrent() { + let count = 0 + + test('first test completes second', async ({ task }) => { + await delay(50) + expect(task.concurrent).toBe(true) + expect(++count).toBe(2) + }) + + test('second test completes first', ({ task }) => { + expect(task.concurrent).toBe(true) + expect(++count).toBe(1) + }) +} + +assertSequential() + +describe.concurrent('describe.concurrent', () => { + assertConcurrent() + + describe('describe', assertConcurrent) + + describe.sequential('describe.sequential', () => { + assertSequential() + + describe('describe', assertSequential) + + describe.concurrent('describe.concurrent', assertConcurrent) }) })