From 9387f57cfabb222fa8b98526b75b90d4ddb61f75 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sat, 25 Apr 2026 20:01:43 +0900 Subject: [PATCH] fix: global `sequence.concurrent: true` with top-level `test(..., { concurrent: false })` + depreacte `sequential` test API and options (#10194) Co-authored-by: Codex Co-authored-by: Vladimir --- docs/api/describe.md | 6 +- docs/api/test.md | 10 ++- packages/runner/src/suite.ts | 2 +- packages/runner/src/types/tasks.ts | 34 ++++++--- packages/runner/src/utils/chain.ts | 11 +++ ...e-concurrent-true-concurrent-false.test.ts | 31 ++++++++ test/config/test/sequence-concurrent.test.ts | 72 +++++++++++++------ test/core/test/sequential.test.ts | 19 +++++ 8 files changed, 151 insertions(+), 34 deletions(-) create mode 100644 test/config/fixtures/sequence-concurrent/sequence-concurrent-true-concurrent-false.test.ts diff --git a/docs/api/describe.md b/docs/api/describe.md index 5444a50b0..2d37a4c2f 100644 --- a/docs/api/describe.md +++ b/docs/api/describe.md @@ -230,10 +230,14 @@ describe.concurrent('suite', () => { }) ``` -## describe.sequential +## describe.sequential {#describe-sequential} - **Alias:** `suite.sequential` +::: warning DEPRECATED +Use [`concurrent: false`](/api/test#concurrent) instead when you need to override inherited or configured concurrency. +::: + `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 diff --git a/docs/api/test.md b/docs/api/test.md index 18af801f2..7618fa19a 100644 --- a/docs/api/test.md +++ b/docs/api/test.md @@ -223,6 +223,10 @@ Whether this test run concurrently with other concurrent tests in the suite. - **Default:** `true` - **Alias:** [`test.sequential`](#test-sequential) +::: warning DEPRECATED +Use [`concurrent: false`](#concurrent) instead when you need to override inherited or configured concurrency. +::: + Whether tests run sequentially. When both `concurrent` and `sequential` are specified, `concurrent` takes precedence. ### skip @@ -453,10 +457,14 @@ test.concurrent('test 2', async ({ expect }) => { Note that if tests are synchronous, Vitest will still run them sequentially. -## test.sequential +## test.sequential {#test-sequential} - **Alias:** `it.sequential` +::: warning DEPRECATED +Use [`concurrent: false`](#concurrent) instead when you need to override inherited or configured concurrency. +::: + `test.sequential` marks a 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 diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 46cd48713..cfc033157 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -392,7 +392,7 @@ function createSuiteCollector( } if ( options.concurrent - || (!options.sequential && runner.config.sequence.concurrent) + ?? (!options.sequential && runner.config.sequence.concurrent) ) { task.concurrent = true } diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 982186230..d2931ea7b 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -1,7 +1,7 @@ import type { Awaitable, TestError } from '@vitest/utils' import type { TestFixtures } from '../fixture' import type { afterAll, afterEach, aroundAll, aroundEach, beforeAll, beforeEach } from '../hooks' -import type { ChainableFunction, kChainableContext } from '../utils/chain' +import type { kChainableContext, TypedChainableFunction } from '../utils/chain' export type RunMode = 'run' | 'skip' | 'only' | 'todo' | 'queued' export type TaskState = RunMode | 'pass' | 'fail' @@ -464,8 +464,14 @@ export interface InternalChainableContext { /** @internal */ getFixtures: () => TestFixtures } -type ChainableTestAPI = ChainableFunction< - 'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'fails', + +type ChainableTestContextMap = Pick< + Required, + 'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'fails' +> + +type ChainableTestAPI = TypedChainableFunction< + ChainableTestContextMap, TestCollectorCallable, { each: TestEachFunction @@ -554,6 +560,8 @@ export interface TestOptions { /** * Whether tests run sequentially. * Tests inherit `sequential` from `describe()` and nested `describe()` will inherit from parent's `sequential`. + * + * @deprecated Use `concurrent: false` instead. */ sequential?: boolean /** @@ -799,10 +807,13 @@ export type TestAPI = ChainableTestAPI suite: SuiteAPI } -export interface InternalTestContext extends Record< - 'concurrent' | 'sequential' | 'skip' | 'only' | 'todo' | 'fails' | 'each', - boolean | undefined -> { +// use mapped type to preserve TestOptions references +type InternalTestChainableContext = { + [K in keyof ChainableTestContextMap]: boolean | undefined +} + +export interface InternalTestContext extends InternalTestChainableContext { + each: boolean | undefined fixtures: TestFixtures } @@ -1061,8 +1072,13 @@ interface SuiteCollectorCallable { ): SuiteCollector } -type ChainableSuiteAPI = ChainableFunction< - 'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'shuffle', +type ChainableSuiteContextMap = Pick< + Required, + 'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'shuffle' +> + +type ChainableSuiteAPI = TypedChainableFunction< + ChainableSuiteContextMap, SuiteCollectorCallable, { each: TestEachFunction diff --git a/packages/runner/src/utils/chain.ts b/packages/runner/src/utils/chain.ts index befadd5c7..3309ebede 100644 --- a/packages/runner/src/utils/chain.ts +++ b/packages/runner/src/utils/chain.ts @@ -10,6 +10,17 @@ export type ChainableFunction< fn: (this: Record, ...args: Parameters) => ReturnType } & C +// this uses mapped type technique to preserve T's jsdoc for chained property function +export type TypedChainableFunction< + T, + F extends (...args: any) => any, + C = object, +> = F & { + [x in keyof T]: TypedChainableFunction; +} & { + fn: (this: Record, ...args: Parameters) => ReturnType +} & C + export const kChainableContext: unique symbol = Symbol('kChainableContext') export function getChainableContext(chainable: SuiteAPI): InternalChainableContext diff --git a/test/config/fixtures/sequence-concurrent/sequence-concurrent-true-concurrent-false.test.ts b/test/config/fixtures/sequence-concurrent/sequence-concurrent-true-concurrent-false.test.ts new file mode 100644 index 000000000..9f95c9726 --- /dev/null +++ b/test/config/fixtures/sequence-concurrent/sequence-concurrent-true-concurrent-false.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from 'vitest' + +const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)) + +let count = 0 + +describe('sequential suite', { concurrent: false }, () => { + test('first test completes first', async ({ task }) => { + await delay(40) + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(1) + }) + + test('second test completes second', async ({ task }) => { + await delay(30) + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(2) + }) +}) + +test('third test completes third', { concurrent: false }, async ({ task }) => { + await delay(20) + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(3) +}) + +test('last test completes last', { concurrent: false }, async ({ task }) => { + await delay(10) + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(4) +}) diff --git a/test/config/test/sequence-concurrent.test.ts b/test/config/test/sequence-concurrent.test.ts index a665e8f71..8fff8cdda 100644 --- a/test/config/test/sequence-concurrent.test.ts +++ b/test/config/test/sequence-concurrent.test.ts @@ -3,7 +3,7 @@ import { expect, test } from 'vitest' import { runVitest } from '../../test-utils' test('should run suites and tests concurrently unless sequential specified when sequence.concurrent is true', async () => { - const { stderr, stdout } = await runVitest({ + const { stderr, errorTree } = await runVitest({ root: './fixtures/sequence-concurrent', include: ['sequence-concurrent-true-*.test.ts'], sequence: { @@ -12,20 +12,38 @@ test('should run suites and tests concurrently unless sequential specified when }) expect(stderr).toBe('') - - expect(stdout).toContain('✓ sequence-concurrent-true-sequential.test.ts > sequential suite > first test completes first') - expect(stdout).toContain('✓ sequence-concurrent-true-sequential.test.ts > sequential suite > second test completes second') - expect(stdout).toContain('✓ sequence-concurrent-true-sequential.test.ts > third test completes third') - expect(stdout).toContain('✓ sequence-concurrent-true-sequential.test.ts > last test completes last') - expect(stdout).toContain('✓ sequence-concurrent-true-concurrent.test.ts > concurrent suite > first test completes last') - expect(stdout).toContain('✓ sequence-concurrent-true-concurrent.test.ts > concurrent suite > second test completes third') - expect(stdout).toContain('✓ sequence-concurrent-true-concurrent.test.ts > third test completes second') - expect(stdout).toContain('✓ sequence-concurrent-true-concurrent.test.ts > last test completes first') - expect(stdout).toContain('Test Files 2 passed (2)') + expect(errorTree()).toMatchInlineSnapshot(` + { + "sequence-concurrent-true-concurrent-false.test.ts": { + "last test completes last": "passed", + "sequential suite": { + "first test completes first": "passed", + "second test completes second": "passed", + }, + "third test completes third": "passed", + }, + "sequence-concurrent-true-concurrent.test.ts": { + "concurrent suite": { + "first test completes last": "passed", + "second test completes third": "passed", + }, + "last test completes first": "passed", + "third test completes second": "passed", + }, + "sequence-concurrent-true-sequential.test.ts": { + "last test completes last": "passed", + "sequential suite": { + "first test completes first": "passed", + "second test completes second": "passed", + }, + "third test completes third": "passed", + }, + } + `) }) test('should run suites and tests sequentially unless concurrent specified when sequence.concurrent is false', async () => { - const { stderr, stdout } = await runVitest({ + const { stderr, errorTree } = await runVitest({ root: './fixtures/sequence-concurrent', include: ['sequence-concurrent-false-*.test.ts'], sequence: { @@ -34,14 +52,24 @@ test('should run suites and tests sequentially unless concurrent specified when }) expect(stderr).toBe('') - - expect(stdout).toContain('✓ sequence-concurrent-false-sequential.test.ts > sequential suite > first test completes first') - expect(stdout).toContain('✓ sequence-concurrent-false-sequential.test.ts > sequential suite > second test completes second') - expect(stdout).toContain('✓ sequence-concurrent-false-sequential.test.ts > third test completes third') - expect(stdout).toContain('✓ sequence-concurrent-false-sequential.test.ts > last test completes last') - expect(stdout).toContain('✓ sequence-concurrent-false-concurrent.test.ts > concurrent suite > first test completes last') - expect(stdout).toContain('✓ sequence-concurrent-false-concurrent.test.ts > concurrent suite > second test completes third') - expect(stdout).toContain('✓ sequence-concurrent-false-concurrent.test.ts > third test completes second') - expect(stdout).toContain('✓ sequence-concurrent-false-concurrent.test.ts > last test completes first') - expect(stdout).toContain('Test Files 2 passed (2)') + expect(errorTree()).toMatchInlineSnapshot(` + { + "sequence-concurrent-false-concurrent.test.ts": { + "concurrent suite": { + "first test completes last": "passed", + "second test completes third": "passed", + }, + "last test completes first": "passed", + "third test completes second": "passed", + }, + "sequence-concurrent-false-sequential.test.ts": { + "last test completes last": "passed", + "sequential suite": { + "first test completes first": "passed", + "second test completes second": "passed", + }, + "third test completes third": "passed", + }, + } + `) }) diff --git a/test/core/test/sequential.test.ts b/test/core/test/sequential.test.ts index eef46286b..a6d6d8ce0 100644 --- a/test/core/test/sequential.test.ts +++ b/test/core/test/sequential.test.ts @@ -52,6 +52,17 @@ function assertConcurrent() { expect(task.concurrent).toBeFalsy() expect(++count).toBe(4) }) + + test('fifth test completes fifth', { concurrent: false }, async ({ task }) => { + await delay(50) + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(5) + }) + + test('sixth test completes sixth', { concurrent: false }, ({ task }) => { + expect(task.concurrent).toBeFalsy() + expect(++count).toBe(6) + }) } assertSequential() @@ -68,4 +79,12 @@ describe.concurrent('describe.concurrent', () => { describe.concurrent('describe.concurrent', assertConcurrent) }) + + describe('describe concurrent false', { concurrent: false }, () => { + assertSequential() + + describe('describe', assertSequential) + + describe.concurrent('describe.concurrent', assertConcurrent) + }) }) -- 2.51.2