diff --git a/docs/api/describe.md b/docs/api/describe.md index 2d37a4c2f..f1a95f9ed 100644 --- a/docs/api/describe.md +++ b/docs/api/describe.md @@ -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 @@ -230,30 +243,6 @@ describe.concurrent('suite', () => { }) ``` -## 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 -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` diff --git a/docs/api/test.md b/docs/api/test.md index 7618fa19a..670f40067 100644 --- a/docs/api/test.md +++ b/docs/api/test.md @@ -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 @@ -457,36 +453,6 @@ test.concurrent('test 2', async ({ expect }) => { Note that if tests are synchronous, Vitest will still run them sequentially. -## 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 -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` diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 9047317d6..788cf0ab8 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -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 diff --git a/docs/guide/parallelism.md b/docs/guide/parallelism.md index 4c694c3b8..c273eeefb 100644 --- a/docs/guide/parallelism.md +++ b/docs/guide/parallelism.md @@ -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. diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index cfc033157..52ef60388 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -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 @@ -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, @@ -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 = { @@ -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) { @@ -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 } @@ -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 diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index d2931ea7b..35ee6d841 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -467,7 +467,7 @@ export interface InternalChainableContext { type ChainableTestContextMap = Pick< Required, - 'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'fails' + 'concurrent' | 'only' | 'skip' | 'todo' | 'fails' > type ChainableTestAPI = TypedChainableFunction< @@ -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. */ @@ -1074,7 +1067,7 @@ interface SuiteCollectorCallable { type ChainableSuiteContextMap = Pick< Required, - 'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'shuffle' + 'concurrent' | 'only' | 'skip' | 'todo' | 'shuffle' > type ChainableSuiteAPI = TypedChainableFunction< diff --git a/packages/vitest/src/node/ast-collect.ts b/packages/vitest/src/node/ast-collect.ts index 4e12e8a1b..209955127 100644 --- a/packages/vitest/src/node/ast-collect.ts +++ b/packages/vitest/src/node/ast-collect.ts @@ -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[] } @@ -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 @@ -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 } } } @@ -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) }, @@ -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 = { diff --git a/test/cli/test/static-collect.test.ts b/test/cli/test/static-collect.test.ts index c245d2674..7f5fc43a5 100644 --- a/test/cli/test/static-collect.test.ts +++ b/test/cli/test/static-collect.test.ts @@ -1137,14 +1137,15 @@ test('collects tags with other options', async () => { `) }) -test('sequential cancels inherited concurrent', async () => { +test('concurrent false cancels inherited concurrent', async () => { const testModule = await collectTests(` import { test, describe } from 'vitest' describe.concurrent('concurrent suite', () => { test('inherits concurrent', () => {}) + test('not concurrent via options', { concurrent: false }, () => {}) - describe.sequential('sequential nested', () => { + describe('non-concurrent nested', { concurrent: false }, () => { test('not concurrent', () => {}) }) @@ -1165,23 +1166,31 @@ test('sequential cancels inherited concurrent', async () => { "mode": "run", "state": "pending", }, - "regular nested": { - "still concurrent": { - "concurrent": true, + "non-concurrent nested": { + "not concurrent": { "errors": [], - "fullName": "concurrent suite > regular nested > still concurrent", + "fullName": "concurrent suite > non-concurrent nested > not concurrent", "id": "-1732721377_0_2_0", - "location": "12:8", + "location": "9:8", "mode": "run", "state": "pending", }, }, - "sequential nested": { - "not concurrent": { + "not concurrent via options": { + "errors": [], + "fullName": "concurrent suite > not concurrent via options", + "id": "-1732721377_0_1", + "location": "6:6", + "mode": "run", + "state": "pending", + }, + "regular nested": { + "still concurrent": { + "concurrent": true, "errors": [], - "fullName": "concurrent suite > sequential nested > not concurrent", - "id": "-1732721377_0_1_0", - "location": "8:8", + "fullName": "concurrent suite > regular nested > still concurrent", + "id": "-1732721377_0_3_0", + "location": "13:8", "mode": "run", "state": "pending", }, @@ -1191,40 +1200,6 @@ test('sequential cancels inherited concurrent', async () => { `) }) -test('collects tests with sequential modifier', async () => { - const testModule = await collectTests(` - import { test, describe } from 'vitest' - - describe.sequential('sequential suite', () => { - test('test in sequential suite', () => {}) - }) - - test.sequential('sequential test', () => {}) -`) - expect(testModule).toMatchInlineSnapshot(` - { - "sequential suite": { - "test in sequential suite": { - "errors": [], - "fullName": "sequential suite > test in sequential suite", - "id": "-1732721377_0_0", - "location": "5:6", - "mode": "run", - "state": "pending", - }, - }, - "sequential test": { - "errors": [], - "fullName": "sequential test", - "id": "-1732721377_1", - "location": "8:4", - "mode": "run", - "state": "pending", - }, - } - `) -}) - test('collects tests with concurrent modifier in different order', async () => { const testModule = await collectTests(` import { test, describe } from 'vitest' @@ -1269,6 +1244,7 @@ test('collects tests with options object modifiers', async () => { test('only via options', { only: true }, () => {}) test('todo via options', { todo: true }, () => {}) test('concurrent via options', { concurrent: true }, () => {}) + test('not concurrent via options', { concurrent: false }, () => {}) test('skip and concurrent via options', { skip: true, concurrent: true }, () => {}) }) `) @@ -1284,6 +1260,14 @@ test('collects tests with options object modifiers', async () => { "mode": "skip", "state": "skipped", }, + "not concurrent via options": { + "errors": [], + "fullName": "options tests > not concurrent via options", + "id": "-1732721377_0_4", + "location": "9:6", + "mode": "skip", + "state": "skipped", + }, "only via options": { "errors": [], "fullName": "options tests > only via options", @@ -1296,8 +1280,8 @@ test('collects tests with options object modifiers', async () => { "concurrent": true, "errors": [], "fullName": "options tests > skip and concurrent via options", - "id": "-1732721377_0_4", - "location": "9:6", + "id": "-1732721377_0_5", + "location": "10:6", "mode": "skip", "state": "skipped", }, diff --git a/test/cli/test/test-tags.test.ts b/test/cli/test/test-tags.test.ts index 742c8ae42..9567cb36d 100644 --- a/test/cli/test/test-tags.test.ts +++ b/test/cli/test/test-tags.test.ts @@ -631,37 +631,41 @@ test('@module-tag with strictTags: false allows undefined tags', async () => { `) }) -test('sequential tag option makes tests run sequentially', async () => { +test('concurrent false tag option opts out of sequence.concurrent', async () => { const { stderr, buildTree } = await runInlineTests({ 'basic.test.js': ` - test('test 1', { tags: ['sequential-tag'] }, () => {}) - test('test 2', { tags: ['sequential-tag'] }, () => {}) + test('test 1', { tags: ['non-concurrent-tag'] }, () => {}) + test('test 2', { tags: ['non-concurrent-tag'] }, () => {}) `, 'vitest.config.js': { test: { globals: true, + sequence: { + concurrent: true, + }, tags: [ - { name: 'sequential-tag', sequential: true }, + { name: 'non-concurrent-tag', concurrent: false }, ], }, }, }) expect(stderr).toBe('') - // sequential is not visible in options, it affect "concurrent" only, which is not set if false expect(buildOptionsTree(buildTree)).toMatchInlineSnapshot(` { "basic.test.js": { "test 1": { + "concurrent": true, "mode": "run", "tags": [ - "sequential-tag", + "non-concurrent-tag", ], "timeout": 5000, }, "test 2": { + "concurrent": true, "mode": "run", "tags": [ - "sequential-tag", + "non-concurrent-tag", ], "timeout": 5000, }, diff --git a/test/config/fixtures/sequence-concurrent/sequence-concurrent-true-sequential.test.ts b/test/config/fixtures/sequence-concurrent/sequence-concurrent-true-sequential.test.ts deleted file mode 100644 index ed2772a7c..000000000 --- a/test/config/fixtures/sequence-concurrent/sequence-concurrent-true-sequential.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' - -const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)) - -let count = 0 - -describe.sequential('sequential suite', () => { - 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.sequential('third test completes third', async ({ task }) => { - await delay(20) - expect(task.concurrent).toBeFalsy() - expect(++count).toBe(3) -}) - -test.sequential('last test completes last', 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 8fff8cdda..40505c68f 100644 --- a/test/config/test/sequence-concurrent.test.ts +++ b/test/config/test/sequence-concurrent.test.ts @@ -2,7 +2,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 () => { +test('should run suites and tests concurrently unless concurrent false is specified when sequence.concurrent is true', async () => { const { stderr, errorTree } = await runVitest({ root: './fixtures/sequence-concurrent', include: ['sequence-concurrent-true-*.test.ts'], @@ -30,14 +30,6 @@ test('should run suites and tests concurrently unless sequential specified when "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", - }, } `) }) diff --git a/test/core/test/concurrent-suite.test.ts b/test/core/test/concurrent-suite.test.ts index 45200d8c3..5dce907bd 100644 --- a/test/core/test/concurrent-suite.test.ts +++ b/test/core/test/concurrent-suite.test.ts @@ -115,14 +115,6 @@ describe('override concurrent', { concurrent: true }, () => { checkSequentialTests() }) - describe.sequential('s-x-1', () => { - checkSequentialTests() - }) - - describe('s-x-2', { sequential: true }, () => { - checkSequentialTests() - }) - describe('s-y', () => { checkParallelTests() }) diff --git a/test/core/test/sequential.test.ts b/test/core/test/sequential.test.ts index a6d6d8ce0..a3d49d4cf 100644 --- a/test/core/test/sequential.test.ts +++ b/test/core/test/sequential.test.ts @@ -42,27 +42,16 @@ function assertConcurrent() { expect(++count).toBe(1) }) - test.sequential('third test completes third', async ({ task }) => { + test('third test completes third', { concurrent: false }, async ({ task }) => { await delay(50) expect(task.concurrent).toBeFalsy() expect(++count).toBe(3) }) - test.sequential('fourth test completes fourth', ({ task }) => { + test('fourth test completes fourth', { concurrent: false }, ({ task }) => { 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() @@ -72,14 +61,6 @@ describe.concurrent('describe.concurrent', () => { describe('describe', assertConcurrent) - describe.sequential('describe.sequential', () => { - assertSequential() - - describe('describe', assertSequential) - - describe.concurrent('describe.concurrent', assertConcurrent) - }) - describe('describe concurrent false', { concurrent: false }, () => { assertSequential()