Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 13 additions & 24 deletions docs/api/describe.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,19 @@ describe.concurrent('suite', () => {
})
```

Set `concurrent` to `false` to opt out of concurrency inherited from a parent suite or [`sequence.concurrent`](/config/sequence#sequence-concurrent):

```ts
describe.concurrent('suite', () => {
test('concurrent test', async () => { /* ... */ })

describe('sequential suite', { concurrent: false }, () => {
test('sequential test 1', async () => { /* ... */ })
test('sequential test 2', async () => { /* ... */ })
})
})
```

`.skip`, `.only`, and `.todo` works with concurrent suites. All the following combinations are valid:

```ts
Expand All @@ -230,30 +243,6 @@ describe.concurrent('suite', () => {
})
```

## describe.sequential <Deprecated /> {#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
import { describe, test } from 'vitest'

describe.concurrent('suite', () => {
test('concurrent test 1', async () => { /* ... */ })
test('concurrent test 2', async () => { /* ... */ })

describe.sequential('', () => {
test('sequential test 1', async () => { /* ... */ })
test('sequential test 2', async () => { /* ... */ })
})
})
```

## describe.shuffle

- **Alias:** `suite.shuffle`
Expand Down
46 changes: 6 additions & 40 deletions docs/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,17 +217,13 @@ Prefer using non-nested meta, if possible.

Whether this test run concurrently with other concurrent tests in the suite.

### sequential
Set `concurrent` to `false` to opt out of concurrency inherited from [`describe.concurrent`](/api/describe#describe-concurrent) or [`sequence.concurrent`](/config/sequence#sequence-concurrent):

- **Type:** `boolean`
- **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.
```ts
test('runs sequentially', { concurrent: false }, async () => {
// ...
})
```

### skip

Expand Down Expand Up @@ -457,36 +453,6 @@ test.concurrent('test 2', async ({ expect }) => {

Note that if tests are synchronous, Vitest will still run them sequentially.

## test.sequential <Deprecated /> {#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
import { describe, test } from 'vitest'

// with config option { sequence: { concurrent: true } }
test('concurrent test 1', async () => { /* ... */ })
test('concurrent test 2', async () => { /* ... */ })

test.sequential('sequential test 1', async () => { /* ... */ })
test.sequential('sequential test 2', async () => { /* ... */ })

// within concurrent suite
describe.concurrent('suite', () => {
test('concurrent test 1', async () => { /* ... */ })
test('concurrent test 2', async () => { /* ... */ })

test.sequential('sequential test 1', async () => { /* ... */ })
test.sequential('sequential test 2', async () => { /* ... */ })
})
```

## test.todo

- **Alias:** `it.todo`
Expand Down
41 changes: 10 additions & 31 deletions docs/guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,27 @@ outline: deep
Vitest 5.0 is currently in beta. This section tracks breaking changes as they are merged and may change before the stable release.
:::

### String Values in `$` Test Titles Are No Longer Quoted
### Removed `test.sequential`, `describe.sequential`, and `sequential` Options

When interpolating string values in `test.each`, `test.for`, `describe.each`, or `describe.for` titles with the `$` syntax, Vitest no longer wraps those string values in quotes.

This affects generated task names in reporter output, snapshots, and any tooling that matches tests by their generated title.
Vitest 5.0 removes the deprecated `test.sequential`, `describe.sequential`, and `sequential` test options. Use `concurrent: false` when you need a test or suite to opt out of inherited or globally configured concurrency.

```ts
test.for([{ name: 'Alice' }])('I am $name', () => {})
// Vitest 4 → I am 'Alice'
// Vitest 5 → I am Alice
test.sequential('example', async () => { /* ... */ }) // [!code --]
test('example', { concurrent: false }, async () => { /* ... */ }) // [!code ++]
```

If you need quotes in the generated title, add them to the title template:

```ts
test.for([{ name: 'Alice' }])('I am "$name"', () => {})
// → I am "Alice"
describe.sequential('suite', () => { /* ... */ }) // [!code --]
describe('suite', { concurrent: false }, () => { /* ... */ }) // [!code ++]
```

### `chaiConfig.truncateThreshold` No Longer Controls Test Title Value Truncation

Vitest now formats interpolated task title values with its display formatter based on `@vitest/pretty-format`, instead of Chai/loupe formatting.

Most output should stay similar, but generated titles or assertion output involving formatted values may have small formatting differences.

If you used `chaiConfig.truncateThreshold` to control truncation in `test.each`, `test.for`, `describe.each`, or `describe.for` titles, use `taskTitleValueFormatTruncate` instead:

```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
The same replacement applies to option objects:

export default defineConfig({
test: {
chaiConfig: { // [!code --]
truncateThreshold: 120, // [!code --]
}, // [!code --]
taskTitleValueFormatTruncate: 120, // [!code ++]
},
})
```ts
test('example', { sequential: true }, async () => { /* ... */ }) // [!code --]
test('example', { concurrent: false }, async () => { /* ... */ }) // [!code ++]
```

`chaiConfig.truncateThreshold` still controls truncation in assertion error messages.

## Migrating to Vitest 4.0 {#vitest-4}

::: warning Prerequisites
Expand Down
13 changes: 13 additions & 0 deletions docs/guide/parallelism.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ describe.concurrent('user API', () => {

If you want *all* tests in your project to run concurrently by default, set [`sequence.concurrent`](/config/sequence#sequence-concurrent) to `true` in your config.

You can opt individual tests or suites out of inherited concurrency with `concurrent: false`:

```ts
test('uses a shared resource', { concurrent: false }, async () => {
// ...
})

describe('shared resource suite', { concurrent: false }, () => {
test('step 1', async () => { /* ... */ })
test('step 2', async () => { /* ... */ })
})
```

### Hooks with Concurrent Tests

When tests run concurrently, lifecycle hooks behave differently. `beforeAll` and `afterAll` still run once for the group, but `beforeEach` and `afterEach` run for each test — potentially at the same time, since the tests themselves overlap.
Expand Down
33 changes: 8 additions & 25 deletions packages/runner/src/suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,7 @@ function createSuiteCollector(
if (task.mode === 'run' && !handler) {
task.mode = 'todo'
}
if (
options.concurrent
?? (!options.sequential && runner.config.sequence.concurrent)
) {
if (options.concurrent ?? runner.config.sequence.concurrent) {
task.concurrent = true
}
task.shuffle = suiteOptions?.shuffle
Expand Down Expand Up @@ -450,17 +447,11 @@ function createSuiteCollector(
options = Object.assign({}, suiteOptions, options)
}

// inherit concurrent / sequential from suite
const concurrent = this.concurrent ?? (!this.sequential && options?.concurrent)
if (options.concurrent != null && concurrent != null) {
const concurrent = this.concurrent ?? options?.concurrent
if (concurrent != null) {
options.concurrent = concurrent
}

const sequential = this.sequential ?? (!this.concurrent && options?.sequential)
if (options.sequential != null && sequential != null) {
options.sequential = sequential
}

const test = task(formatName(name), {
...this,
...options,
Expand Down Expand Up @@ -602,9 +593,6 @@ function createSuite() {
optionsOrFactory,
) as { options: SuiteOptions; handler: SuiteFactory | undefined }

const isConcurrentSpecified = options.concurrent || this.concurrent || options.sequential === false
const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false

const { meta: parentMeta, ...parentOptions } = currentSuite?.options || {}
// inherit options from current suite
options = {
Expand All @@ -630,14 +618,9 @@ function createSuite() {
mode = 'todo'
}

// inherit concurrent / sequential from suite
const isConcurrent = isConcurrentSpecified || (options.concurrent && !isSequentialSpecified)
const isSequential = isSequentialSpecified || (options.sequential && !isConcurrentSpecified)
if (isConcurrent != null) {
options.concurrent = isConcurrent && !isSequential
}
if (isSequential != null) {
options.sequential = isSequential && !isConcurrent
const concurrent = this.concurrent ?? options.concurrent
if (concurrent != null) {
options.concurrent = concurrent
}

if (parentMeta) {
Expand Down Expand Up @@ -737,7 +720,7 @@ function createSuite() {
(condition ? suite : suite.skip) as SuiteAPI

return createChainable(
['concurrent', 'sequential', 'shuffle', 'skip', 'only', 'todo'],
['concurrent', 'shuffle', 'skip', 'only', 'todo'],
suiteFn,
) as unknown as SuiteAPI
}
Expand Down Expand Up @@ -968,7 +951,7 @@ export function createTaskCollector(
taskFn.aroundAll = aroundAll

const _test = createChainable(
['concurrent', 'sequential', 'skip', 'only', 'todo', 'fails'],
['concurrent', 'skip', 'only', 'todo', 'fails'],
taskFn,
{ fixtures: new TestFixtures() },
) as TestAPI
Expand Down
11 changes: 2 additions & 9 deletions packages/runner/src/types/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ export interface InternalChainableContext<API = TestAPI> {

type ChainableTestContextMap = Pick<
Required<TestOptions>,
'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'fails'
'concurrent' | 'only' | 'skip' | 'todo' | 'fails'
>

type ChainableTestAPI<ExtraContext = object> = TypedChainableFunction<
Expand Down Expand Up @@ -557,13 +557,6 @@ export interface TestOptions {
* 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`.
*
* @deprecated Use `concurrent: false` instead.
*/
sequential?: boolean
/**
* Whether the test should be skipped.
*/
Expand Down Expand Up @@ -1074,7 +1067,7 @@ interface SuiteCollectorCallable<ExtraContext = object> {

type ChainableSuiteContextMap = Pick<
Required<SuiteOptions>,
'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'shuffle'
'concurrent' | 'only' | 'skip' | 'todo' | 'shuffle'
>

type ChainableSuiteAPI<ExtraContext = object> = TypedChainableFunction<
Expand Down
25 changes: 8 additions & 17 deletions packages/vitest/src/node/ast-collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ interface LocalCallDefinition {
mode: 'run' | 'skip' | 'only' | 'todo' | 'queued'
task: ParsedSuite | ParsedFile | ParsedTest
dynamic: boolean
concurrent: boolean
sequential: boolean
concurrent: boolean | undefined
tags: string[]
}

Expand Down Expand Up @@ -172,8 +171,7 @@ function astParseFile(filepath: string, code: string) {
mode = 'skip'
}
}
let isConcurrent = properties.includes('concurrent')
let isSequential = properties.includes('sequential')
let concurrent = properties.includes('concurrent') || undefined

let start: number
const end = node.end
Expand Down Expand Up @@ -249,15 +247,12 @@ function astParseFile(filepath: string, code: string) {
}
}
}
else if (prop.value?.type === 'Literal' && prop.value.value === true) {
if (keyName === 'skip' || keyName === 'only' || keyName === 'todo') {
else if (prop.value?.type === 'Literal') {
if ((keyName === 'skip' || keyName === 'only' || keyName === 'todo') && prop.value.value === true) {
mode = keyName
}
else if (keyName === 'concurrent') {
isConcurrent = true
}
else if (keyName === 'sequential') {
isSequential = true
else if (keyName === 'concurrent' && typeof prop.value.value === 'boolean') {
concurrent = prop.value.value
Comment thread
hi-ogawa marked this conversation as resolved.
Resolved
}
}
}
Expand All @@ -272,8 +267,7 @@ function astParseFile(filepath: string, code: string) {
mode,
task: null as any,
dynamic: isDynamicEach,
concurrent: isConcurrent,
sequential: isSequential,
concurrent,
tags,
} satisfies LocalCallDefinition)
},
Expand Down Expand Up @@ -416,10 +410,7 @@ function createFileTask(
// Inherit tags from parent suite and merge with own tags
const parentTags = latestSuite.tags || []
const taskTags = unique([...parentTags, ...definition.tags])
// resolve concurrent/sequential: sequential cancels inherited concurrent
const concurrent = definition.sequential
? undefined
: (definition.concurrent || latestSuite.concurrent || undefined)
const concurrent = definition.concurrent ?? latestSuite.concurrent

if (definition.type === 'suite') {
const task: ParsedSuite = {
Expand Down
Loading
Loading