diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 15ce40ce0a99..14b779f45fea 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -154,7 +154,7 @@ export default ({ mode }: { mode: string }) => { title: 'Vitest', items: [ { text: 'Guides', link: '/guide/' }, - { text: 'API', link: '/api/' }, + { text: 'API', link: '/api/test' }, { text: 'Config', link: '/config/' }, ], }, @@ -195,7 +195,7 @@ export default ({ mode }: { mode: string }) => { nav: [ { text: 'Guides', link: '/guide/', activeMatch: '^/guide/' }, - { text: 'API', link: '/api/', activeMatch: '^/api/' }, + { text: 'API', link: '/api/test', activeMatch: '^/api/' }, { text: 'Config', link: '/config/', activeMatch: '^/config/' }, { text: 'Blog', @@ -961,7 +961,20 @@ export default ({ mode }: { mode: string }) => { '/api': [ { text: 'Test API Reference', - link: '/api/', + items: [ + { + text: 'Test', + link: '/api/test', + }, + { + text: 'Describe', + link: '/api/describe', + }, + { + text: 'Hooks', + link: '/api/hooks', + }, + ], }, { text: 'Mocks', diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index ff5c5f627524..84d65ad0bcb1 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -25,6 +25,10 @@ export default { return true } const url = new URL(to, location.href) + if (url.pathname === '/api/' || url.pathname === '/api' || url.pathname === '/api/index.html') { + setTimeout(() => { router.go(`/api/test`) }) + return false + } if (!url.hash) { return true } diff --git a/docs/api/describe.md b/docs/api/describe.md new file mode 100644 index 000000000000..0eca4f427f61 --- /dev/null +++ b/docs/api/describe.md @@ -0,0 +1,378 @@ +--- +outline: deep +--- + +# describe + +- **Alias:** `suite` + +```ts +function describe( + name: string | Function, + body?: () => unknown, + timeout?: number +): void +function describe( + name: string | Function, + options: SuiteOptions, + body?: () => unknown, +): void +``` + +`describe` is used to group related tests and benchmarks into a suite. Suites help organize your test files by creating logical blocks, making test output easier to read and enabling shared setup/teardown through [lifecycle hooks](/api/hooks). + +When you use `test` in the top level of file, they are collected as part of the implicit suite for it. Using `describe` you can define a new suite in the current context, as a set of related tests or benchmarks and other nested suites. + +```ts [basic.spec.ts] +import { describe, expect, test } from 'vitest' + +const person = { + isActive: true, + age: 32, +} + +describe('person', () => { + test('person is defined', () => { + expect(person).toBeDefined() + }) + + test('is active', () => { + expect(person.isActive).toBeTruthy() + }) + + test('age limit', () => { + expect(person.age).toBeLessThanOrEqual(32) + }) +}) +``` + +You can also nest `describe` blocks if you have a hierarchy of tests: + +```ts +import { describe, expect, test } from 'vitest' + +function numberToCurrency(value: number | string) { + if (typeof value !== 'number') { + throw new TypeError('Value must be a number') + } + + return value.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') +} + +describe('numberToCurrency', () => { + describe('given an invalid number', () => { + test('composed of non-numbers to throw error', () => { + expect(() => numberToCurrency('abc')).toThrowError() + }) + }) + + describe('given a valid number', () => { + test('returns the correct currency format', () => { + expect(numberToCurrency(10000)).toBe('10,000.00') + }) + }) +}) +``` + +## Test Options + +You can use [test options](/api/test#test-options) to apply configuration to every test inside a suite, including nested suites. This is useful when you want to set timeouts, retries, or other options for a group of related tests. + +```ts +import { describe, test } from 'vitest' + +describe('slow tests', { timeout: 10_000 }, () => { + test('test 1', () => { /* ... */ }) + test('test 2', () => { /* ... */ }) + + // nested suites also inherit the timeout + describe('nested', () => { + test('test 3', () => { /* ... */ }) + }) +}) +``` + +### `shuffle` + +- **Type:** `boolean` +- **Default:** `false` (configured by [`sequence.shuffle`](/config/sequence#sequence-shuffle)) +- **Alias:** [`describe.shuffle`](#describe-shuffle) + +Run tests within the suite in random order. This option is inherited by nested suites. + +```ts +import { describe, test } from 'vitest' + +describe('randomized tests', { shuffle: true }, () => { + test('test 1', () => { /* ... */ }) + test('test 2', () => { /* ... */ }) + test('test 3', () => { /* ... */ }) +}) +``` + +## describe.skip + +- **Alias:** `suite.skip` + +Use `describe.skip` in a suite to avoid running a particular describe block. + +```ts +import { assert, describe, test } from 'vitest' + +describe.skip('skipped suite', () => { + test('sqrt', () => { + // Suite skipped, no error + assert.equal(Math.sqrt(4), 3) + }) +}) +``` + +## describe.skipIf + +- **Alias:** `suite.skipIf` + +In some cases, you might run suites multiple times with different environments, and some of the suites might be environment-specific. Instead of wrapping the suite with `if`, you can use `describe.skipIf` to skip the suite whenever the condition is truthy. + +```ts +import { describe, test } from 'vitest' + +const isDev = process.env.NODE_ENV === 'development' + +describe.skipIf(isDev)('prod only test suite', () => { + // this test suite only runs in production +}) +``` + +## describe.runIf + +- **Alias:** `suite.runIf` + +Opposite of [describe.skipIf](#describe-skipif). + +```ts +import { assert, describe, test } from 'vitest' + +const isDev = process.env.NODE_ENV === 'development' + +describe.runIf(isDev)('dev only test suite', () => { + // this test suite only runs in development +}) +``` + +## describe.only + +- **Alias:** `suite.only` + +Use `describe.only` to only run certain suites + +```ts +import { assert, describe, test } from 'vitest' + +// Only this suite (and others marked with only) are run +describe.only('suite', () => { + test('sqrt', () => { + assert.equal(Math.sqrt(4), 3) + }) +}) + +describe('other suite', () => { + // ... will be skipped +}) +``` + +Sometimes it is very useful to run `only` tests in a certain file, ignoring all other tests from the whole test suite, which pollute the output. + +In order to do that, run `vitest` with specific file containing the tests in question: + +```shell +vitest interesting.test.ts +``` + +## describe.concurrent + +- **Alias:** `suite.concurrent` + +`describe.concurrent` runs all inner suites and tests in parallel + +```ts +import { describe, test } from 'vitest' + +// All suites and tests within this suite will be run in parallel +describe.concurrent('suite', () => { + test('concurrent test 1', async () => { /* ... */ }) + describe('concurrent suite 2', async () => { + test('concurrent test inner 1', async () => { /* ... */ }) + test('concurrent test inner 2', async () => { /* ... */ }) + }) + test.concurrent('concurrent test 3', async () => { /* ... */ }) +}) +``` + +`.skip`, `.only`, and `.todo` works with concurrent suites. All the following combinations are valid: + +```ts +describe.concurrent(/* ... */) +describe.skip.concurrent(/* ... */) // or describe.concurrent.skip(/* ... */) +describe.only.concurrent(/* ... */) // or describe.concurrent.only(/* ... */) +describe.todo.concurrent(/* ... */) // or describe.concurrent.todo(/* ... */) +``` + +When running concurrent tests, Snapshots and Assertions must use `expect` from the local [Test Context](/guide/test-context) to ensure the right test is detected. + +```ts +describe.concurrent('suite', () => { + test('concurrent test 1', async ({ expect }) => { + expect(foo).toMatchSnapshot() + }) + test('concurrent test 2', async ({ expect }) => { + expect(foo).toMatchSnapshot() + }) +}) +``` + +## describe.sequential + +- **Alias:** `suite.sequential` + +`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` + +Vitest provides a way to run all tests in random order via CLI flag [`--sequence.shuffle`](/guide/cli) or config option [`sequence.shuffle`](/config/#sequence-shuffle), but if you want to have only part of your test suite to run tests in random order, you can mark it with this flag. + +```ts +import { describe, test } from 'vitest' + +// or describe('suite', { shuffle: true }, ...) +describe.shuffle('suite', () => { + test('random test 1', async () => { /* ... */ }) + test('random test 2', async () => { /* ... */ }) + test('random test 3', async () => { /* ... */ }) + + // `shuffle` is inherited + describe('still random', () => { + test('random 4.1', async () => { /* ... */ }) + test('random 4.2', async () => { /* ... */ }) + }) + + // disable shuffle inside + describe('not random', { shuffle: false }, () => { + test('in order 5.1', async () => { /* ... */ }) + test('in order 5.2', async () => { /* ... */ }) + }) +}) +// order depends on sequence.seed option in config (Date.now() by default) +``` + +`.skip`, `.only`, and `.todo` works with random suites. + +## describe.todo + +- **Alias:** `suite.todo` + +Use `describe.todo` to stub suites to be implemented later. An entry will be shown in the report for the tests so you know how many tests you still need to implement. + +```ts +// An entry will be shown in the report for this suite +describe.todo('unimplemented suite') +``` + +## describe.each + +- **Alias:** `suite.each` + +::: tip +While `describe.each` is provided for Jest compatibility, +Vitest also has [`describe.for`](#describe-for) which simplifies argument types and aligns with [`test.for`](/api/test#test-for). +::: + +Use `describe.each` if you have more than one test that depends on the same data. + +```ts +import { describe, expect, test } from 'vitest' + +describe.each([ + { a: 1, b: 1, expected: 2 }, + { a: 1, b: 2, expected: 3 }, + { a: 2, b: 1, expected: 3 }, +])('describe object add($a, $b)', ({ a, b, expected }) => { + test(`returns ${expected}`, () => { + expect(a + b).toBe(expected) + }) + + test(`returned value not be greater than ${expected}`, () => { + expect(a + b).not.toBeGreaterThan(expected) + }) + + test(`returned value not be less than ${expected}`, () => { + expect(a + b).not.toBeLessThan(expected) + }) +}) +``` + +* First row should be column names, separated by `|`; +* One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax. + +```ts +import { describe, expect, test } from 'vitest' + +describe.each` + a | b | expected + ${1} | ${1} | ${2} + ${'a'} | ${'b'} | ${'ab'} + ${[]} | ${'b'} | ${'b'} + ${{}} | ${'b'} | ${'[object Object]b'} + ${{ asd: 1 }} | ${'b'} | ${'[object Object]b'} +`('describe template string add($a, $b)', ({ a, b, expected }) => { + test(`returns ${expected}`, () => { + expect(a + b).toBe(expected) + }) +}) +``` + +## describe.for + +- **Alias:** `suite.for` + +The difference from `describe.each` is how array case is provided in the arguments. +Other non array case (including template string usage) works exactly same. + +```ts +// `each` spreads array case +describe.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) -> %i', (a, b, expected) => { // [!code --] + test('test', () => { + expect(a + b).toBe(expected) + }) +}) + +// `for` doesn't spread array case +describe.for([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) -> %i', ([a, b, expected]) => { // [!code ++] + test('test', () => { + expect(a + b).toBe(expected) + }) +}) +``` diff --git a/docs/api/expect.md b/docs/api/expect.md index 40c424e6535c..e6d670ee6f92 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -31,7 +31,7 @@ expect(input).to.equal(2) // chai API expect(input).toBe(2) // jest API ``` -Technically this example doesn't use [`test`](/api/#test) function, so in the console you will see Node.js error instead of Vitest output. To learn more about `test`, please read [Test API Reference](/api/). +Technically this example doesn't use [`test`](/api/test) function, so in the console you will see Node.js error instead of Vitest output. To learn more about `test`, please read [Test API Reference](/api/test). Also, `expect` can be used statically to access matcher functions, described later, and more. @@ -98,7 +98,7 @@ test('expect.soft test', () => { ``` ::: warning -`expect.soft` can only be used inside the [`test`](/api/#test) function. +`expect.soft` can only be used inside the [`test`](/api/test) function. ::: ## poll diff --git a/docs/api/hooks.md b/docs/api/hooks.md new file mode 100644 index 000000000000..94ba3c1cd3c2 --- /dev/null +++ b/docs/api/hooks.md @@ -0,0 +1,461 @@ +--- +outline: deep +--- + +# Hooks + +These functions allow you to hook into the life cycle of tests to avoid repeating setup and teardown code. They apply to the current context: the file if they are used at the top-level or the current suite if they are inside a `describe` block. These hooks are not called, when you are running Vitest as a [type checker](/guide/testing-types). + +Test hooks are called in a stack order ("after" hooks are reversed) by default, but you can configure it via [`sequence.hooks`](/config/sequence#sequence-hooks) option. + +## beforeEach + +```ts +function beforeEach( + body: () => unknown, + timeout?: number, +): void +``` + +Register a callback to be called before each of the tests in the current suite runs. +If the function returns a promise, Vitest waits until the promise resolve before running the test. + +Optionally, you can pass a timeout (in milliseconds) defining how long to wait before terminating. The default is 10 seconds, and can be configured globally with [`hookTimeout`](/config/hooktimeout). + +```ts +import { beforeEach } from 'vitest' + +beforeEach(async () => { + // Clear mocks and add some testing data before each test run + await stopMocking() + await addUser({ name: 'John' }) +}) +``` + +Here, the `beforeEach` ensures that user is added for each test. + +`beforeEach` can also return an optional cleanup function (equivalent to [`afterEach`](#aftereach)): + +```ts +import { beforeEach } from 'vitest' + +beforeEach(async () => { + // called once before each test run + await prepareSomething() + + // clean up function, called once after each test run + return async () => { + await resetSomething() + } +}) +``` + +## afterEach + +```ts +function afterEach( + body: () => unknown, + timeout?: number, +): void +``` + +Register a callback to be called after each one of the tests in the current suite completes. +If the function returns a promise, Vitest waits until the promise resolve before continuing. + +Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The default is 10 seconds, and can be configured globally with [`hookTimeout`](/config/hooktimeout). + +```ts +import { afterEach } from 'vitest' + +afterEach(async () => { + await clearTestingData() // clear testing data after each test run +}) +``` + +Here, the `afterEach` ensures that testing data is cleared after each test runs. + +::: tip +You can also use [`onTestFinished`](#ontestfinished) during the test execution to cleanup any state after the test has finished running. +::: + +## beforeAll + +```ts +function beforeAll( + body: () => unknown, + timeout?: number, +): void +``` + +Register a callback to be called once before starting to run all tests in the current suite. +If the function returns a promise, Vitest waits until the promise resolve before running tests. + +Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The default is 10 seconds, and can be configured globally with [`hookTimeout`](/config/hooktimeout). + +```ts +import { beforeAll } from 'vitest' + +beforeAll(async () => { + await startMocking() // called once before all tests run +}) +``` + +Here the `beforeAll` ensures that the mock data is set up before tests run. + +`beforeAll` can also return an optional cleanup function (equivalent to [`afterAll`](#afterall)): + +```ts +import { beforeAll } from 'vitest' + +beforeAll(async () => { + // called once before all tests run + await startMocking() + + // clean up function, called once after all tests run + return async () => { + await stopMocking() + } +}) +``` + +## afterAll + +```ts +function afterAll( + body: () => unknown, + timeout?: number, +): void +``` + +Register a callback to be called once after all tests have run in the current suite. +If the function returns a promise, Vitest waits until the promise resolve before continuing. + +Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The default is 10 seconds, and can be configured globally with [`hookTimeout`](/config/hooktimeout). + +```ts +import { afterAll } from 'vitest' + +afterAll(async () => { + await stopMocking() // this method is called after all tests run +}) +``` + +Here the `afterAll` ensures that `stopMocking` method is called after all tests run. + +## aroundEach + +```ts +function aroundEach( + body: (runTest: () => Promise, context: TestContext) => Promise, + timeout?: number, +): void +``` + +Register a callback function that wraps around each test within the current suite. The callback receives a `runTest` function that **must** be called to run the test. + +The `runTest()` function runs `beforeEach` hooks, the test itself, fixtures accessed in the test, and `afterEach` hooks. Fixtures that are accessed in the `aroundEach` callback are initialized before `runTest()` is called and are torn down after the aroundEach teardown code completes, allowing you to safely use them in both setup and teardown phases. + +::: warning +You **must** call `runTest()` within your callback. If `runTest()` is not called, the test will fail with an error. +::: + +Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The timeout applies independently to the setup phase (before `runTest()`) and teardown phase (after `runTest()`). The default is 10 seconds, and can be configured globally with [`hookTimeout`](/config/hooktimeout). + +```ts +import { aroundEach, test } from 'vitest' + +aroundEach(async (runTest) => { + await db.transaction(runTest) +}) + +test('insert user', async () => { + await db.insert({ name: 'Alice' }) + // transaction is automatically rolled back after the test +}) +``` + +::: tip When to use `aroundEach` +Use `aroundEach` when your test needs to run **inside a context** that wraps around it, such as: +- Wrapping tests in [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context +- Wrapping tests with tracing spans +- Database transactions + +If you just need to run code before and after tests, prefer using [`beforeEach`](#beforeeach) with a cleanup return function: +```ts +beforeEach(async () => { + await database.connect() + return async () => { + await database.disconnect() + } +}) +``` +::: + +### Multiple Hooks + +When multiple `aroundEach` hooks are registered, they are nested inside each other. The first registered hook is the outermost wrapper: + +```ts +aroundEach(async (runTest) => { + console.log('outer before') + await runTest() + console.log('outer after') +}) + +aroundEach(async (runTest) => { + console.log('inner before') + await runTest() + console.log('inner after') +}) + +// Output order: +// outer before +// inner before +// test +// inner after +// outer after +``` + +### Context and Fixtures + +The callback receives the test context as the second argument which means that you can use fixtures with `aroundEach`: + +```ts +import { aroundEach, test as base } from 'vitest' + +const test = base.extend<{ db: Database; user: User }>({ + db: async ({}, use) => { + // db is created before `aroundEach` hook + const db = await createTestDatabase() + await use(db) + await db.close() + }, + user: async ({ db }, use) => { + // `user` runs as part of the transaction + // because it's accessed inside the `test` + const user = await db.createUser() + await use(user) + }, +}) + +// note that `aroundEach` is available on test +// for a better TypeScript support of fixtures +test.aroundEach(async (runTest, { db }) => { + await db.transaction(runTest) +}) + +test('insert user', async ({ db, user }) => { + await db.insert(user) +}) +``` + +## aroundAll + +```ts +function aroundAll( + body: (runSuite: () => Promise) => Promise, + timeout?: number, +): void +``` + +Register a callback function that wraps around all tests within the current suite. The callback receives a `runSuite` function that **must** be called to run the suite's tests. + +The `runSuite()` function runs all tests in the suite, including `beforeAll`/`afterAll`/`beforeEach`/`afterEach` hooks, `aroundEach` hooks, and fixtures. + +::: warning +You **must** call `runSuite()` within your callback. If `runSuite()` is not called, the hook will fail with an error and all tests in the suite will be skipped. +::: + +Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The timeout applies independently to the setup phase (before `runSuite()`) and teardown phase (after `runSuite()`). The default is 10 seconds, and can be configured globally with [`hookTimeout`](/config/hooktimeout). + +```ts +import { aroundAll, test } from 'vitest' + +aroundAll(async (runSuite) => { + await tracer.trace('test-suite', runSuite) +}) + +test('test 1', () => { + // Runs within the tracing span +}) + +test('test 2', () => { + // Also runs within the same tracing span +}) +``` + +::: tip When to use `aroundAll` +Use `aroundAll` when your suite needs to run **inside a context** that wraps around all tests, such as: +- Wrapping an entire suite in [AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context +- Wrapping a suite with tracing spans +- Database transactions + +If you just need to run code once before and after all tests, prefer using [`beforeAll`](#beforeall) with a cleanup return function: +```ts +beforeAll(async () => { + await server.start() + return async () => { + await server.stop() + } +}) +``` +::: + +### Multiple Hooks + +When multiple `aroundAll` hooks are registered, they are nested inside each other. The first registered hook is the outermost wrapper: + +```ts +aroundAll(async (runSuite) => { + console.log('outer before') + await runSuite() + console.log('outer after') +}) + +aroundAll(async (runSuite) => { + console.log('inner before') + await runSuite() + console.log('inner after') +}) + +// Output order: outer before → inner before → tests → inner after → outer after +``` + +Each suite has its own independent `aroundAll` hooks. Parent suite's `aroundAll` wraps around child suite's execution: + +```ts +import { AsyncLocalStorage } from 'node:async_hooks' +import { aroundAll, describe, test } from 'vitest' + +const context = new AsyncLocalStorage<{ suiteId: string }>() + +aroundAll(async (runSuite) => { + await context.run({ suiteId: 'root' }, runSuite) +}) + +test('root test', () => { + // context.getStore() returns { suiteId: 'root' } +}) + +describe('nested', () => { + aroundAll(async (runSuite) => { + // Parent's context is available here + await context.run({ suiteId: 'nested' }, runSuite) + }) + + test('nested test', () => { + // context.getStore() returns { suiteId: 'nested' } + }) +}) +``` + +## Test Hooks + +Vitest provides a few hooks that you can call _during_ the test execution to cleanup the state when the test has finished running. + +::: warning +These hooks will throw an error if they are called outside of the test body. +::: + +### onTestFinished {#ontestfinished} + +This hook is always called after the test has finished running. It is called after `afterEach` hooks since they can influence the test result. It receives an `TestContext` object like `beforeEach` and `afterEach`. + +```ts {1,5} +import { onTestFinished, test } from 'vitest' + +test('performs a query', () => { + const db = connectDb() + onTestFinished(() => db.close()) + db.query('SELECT * FROM users') +}) +``` + +::: warning +If you are running tests concurrently, you should always use `onTestFinished` hook from the test context since Vitest doesn't track concurrent tests in global hooks: + +```ts {3,5} +import { test } from 'vitest' + +test.concurrent('performs a query', ({ onTestFinished }) => { + const db = connectDb() + onTestFinished(() => db.close()) + db.query('SELECT * FROM users') +}) +``` +::: + +This hook is particularly useful when creating reusable logic: + +```ts +// this can be in a separate file +function getTestDb() { + const db = connectMockedDb() + onTestFinished(() => db.close()) + return db +} + +test('performs a user query', async () => { + const db = getTestDb() + expect( + await db.query('SELECT * from users').perform() + ).toEqual([]) +}) + +test('performs an organization query', async () => { + const db = getTestDb() + expect( + await db.query('SELECT * from organizations').perform() + ).toEqual([]) +}) +``` + +It is also a good practice to cleanup your spies after each test, so they don't leak into other tests. You can do so by enabling [`restoreMocks`](/config/restoremocks) config globally, or restoring the spy inside `onTestFinished` (if you try to restore the mock at the end of the test, it won't be restored if one of the assertions fails - using `onTestFinished` ensures the code always runs): + +```ts +import { onTestFinished, test } from 'vitest' + +test('performs a query', () => { + const spy = vi.spyOn(db, 'query') + onTestFinished(() => spy.mockClear()) + + db.query('SELECT * FROM users') + expect(spy).toHaveBeenCalled() +}) +``` + +::: tip +This hook is always called in reverse order and is not affected by [`sequence.hooks`](/config/#sequence-hooks) option. +::: + +### onTestFailed + +This hook is called only after the test has failed. It is called after `afterEach` hooks since they can influence the test result. It receives a `TestContext` object like `beforeEach` and `afterEach`. This hook is useful for debugging. + +```ts {1,5-7} +import { onTestFailed, test } from 'vitest' + +test('performs a query', () => { + const db = connectDb() + onTestFailed(({ task }) => { + console.log(task.result.errors) + }) + db.query('SELECT * FROM users') +}) +``` + +::: warning +If you are running tests concurrently, you should always use `onTestFailed` hook from the test context since Vitest doesn't track concurrent tests in global hooks: + +```ts {3,5-7} +import { test } from 'vitest' + +test.concurrent('performs a query', ({ onTestFailed }) => { + const db = connectDb() + onTestFailed(({ task }) => { + console.log(task.result.errors) + }) + db.query('SELECT * FROM users') +}) +``` +::: diff --git a/docs/api/index.md b/docs/api/index.md deleted file mode 100644 index 850a79a06a9f..000000000000 --- a/docs/api/index.md +++ /dev/null @@ -1,1316 +0,0 @@ ---- -outline: deep ---- - -# Test API Reference - -The following types are used in the type signatures below - -```ts -type Awaitable = T | PromiseLike -type TestFunction = () => Awaitable - -interface TestOptions { - /** - * Will fail the test if it takes too long to execute - */ - timeout?: number - /** - * Will retry the test specific number of times if it fails - * - * @default 0 - */ - retry?: number - /** - * Will repeat the same test several times even if it fails each time - * If you have "retry" option and it fails, it will use every retry in each cycle - * Useful for debugging random failings - * - * @default 0 - */ - repeats?: number - /** - * Custom tags of the test. Useful for filtering tests. - */ - tags?: string[] | string -} -``` - - - -When a test function returns a promise, the runner will wait until it is resolved to collect async expectations. If the promise is rejected, the test will fail. - -::: tip -In Jest, `TestFunction` can also be of type `(done: DoneCallback) => void`. If this form is used, the test will not be concluded until `done` is called. You can achieve the same using an `async` function, see the [Migration guide Done Callback section](/guide/migration#done-callback). -::: - -You can define options by chaining properties on a function: - -```ts -import { test } from 'vitest' - -test.skip('skipped test', () => { - // some logic that fails right now -}) - -test.concurrent.skip('skipped concurrent test', () => { - // some logic that fails right now -}) -``` - -But you can also provide an object as a second argument instead: - -```ts -import { test } from 'vitest' - -test('skipped test', { skip: true }, () => { - // some logic that fails right now -}) - -test('skipped concurrent test', { skip: true, concurrent: true }, () => { - // some logic that fails right now -}) -``` - -They both work in exactly the same way. To use either one is purely a stylistic choice. - -Note that if you are providing timeout as the last argument, you cannot use options anymore: - -```ts -import { test } from 'vitest' - -// ✅ this works -test.skip('heavy test', () => { - // ... -}, 10_000) - -// ❌ this doesn't work -test('heavy test', { skip: true }, () => { - // ... -}, 10_000) -``` - -However, you can provide a timeout inside the object: - -```ts -import { test } from 'vitest' - -// ✅ this works -test('heavy test', { skip: true, timeout: 10_000 }, () => { - // ... -}) -``` - -## test - -- **Alias:** `it` - -`test` defines a set of related expectations. It receives the test name and a function that holds the expectations to test. - -Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The default is 5 seconds, and can be configured globally with [testTimeout](/config/#testtimeout) - -```ts -import { expect, test } from 'vitest' - -test('should work as expected', () => { - expect(Math.sqrt(4)).toBe(2) -}) -``` - -### test.extend {#test-extended} - -- **Alias:** `it.extend` - -Use `test.extend` to extend the test context with custom fixtures. This will return a new `test` and it's also extendable, so you can compose more fixtures or override existing ones by extending it as you need. See [Extend Test Context](/guide/test-context.html#test-extend) for more information. - -```ts -import { expect, test } from 'vitest' - -const todos = [] -const archive = [] - -const myTest = test.extend({ - todos: async ({ task }, use) => { - todos.push(1, 2, 3) - await use(todos) - todos.length = 0 - }, - archive -}) - -myTest('add item', ({ todos }) => { - expect(todos.length).toBe(3) - - todos.push(4) - expect(todos.length).toBe(4) -}) -``` - -### test.skip - -- **Alias:** `it.skip` - -If you want to skip running certain tests, but you don't want to delete the code due to any reason, you can use `test.skip` to avoid running them. - -```ts -import { assert, test } from 'vitest' - -test.skip('skipped test', () => { - // Test skipped, no error - assert.equal(Math.sqrt(4), 3) -}) -``` - -You can also skip test by calling `skip` on its [context](/guide/test-context) dynamically: - -```ts -import { assert, test } from 'vitest' - -test('skipped test', (context) => { - context.skip() - // Test skipped, no error - assert.equal(Math.sqrt(4), 3) -}) -``` - -Since Vitest 3.1, if the condition is unknown, you can provide it to the `skip` method as the first arguments: - -```ts -import { assert, test } from 'vitest' - -test('skipped test', (context) => { - context.skip(Math.random() < 0.5, 'optional message') - // Test skipped, no error - assert.equal(Math.sqrt(4), 3) -}) -``` - -### test.skipIf - -- **Alias:** `it.skipIf` - -In some cases you might run tests multiple times with different environments, and some of the tests might be environment-specific. Instead of wrapping the test code with `if`, you can use `test.skipIf` to skip the test whenever the condition is truthy. - -```ts -import { assert, test } from 'vitest' - -const isDev = process.env.NODE_ENV === 'development' - -test.skipIf(isDev)('prod only test', () => { - // this test only runs in production -}) -``` - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### test.runIf - -- **Alias:** `it.runIf` - -Opposite of [test.skipIf](#test-skipif). - -```ts -import { assert, test } from 'vitest' - -const isDev = process.env.NODE_ENV === 'development' - -test.runIf(isDev)('dev only test', () => { - // this test only runs in development -}) -``` - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### test.only - -- **Alias:** `it.only` - -Use `test.only` to only run certain tests in a given suite. This is useful when debugging. - -Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The default is 5 seconds, and can be configured globally with [testTimeout](/config/#testtimeout). - -```ts -import { assert, test } from 'vitest' - -test.only('test', () => { - // Only this test (and others marked with only) are run - assert.equal(Math.sqrt(4), 2) -}) -``` - -Sometimes it is very useful to run `only` tests in a certain file, ignoring all other tests from the whole test suite, which pollute the output. - -In order to do that run `vitest` with specific file containing the tests in question. -``` -# vitest interesting.test.ts -``` - -### test.concurrent - -- **Alias:** `it.concurrent` - -`test.concurrent` marks consecutive tests to be run in parallel. It receives the test name, an async function with the tests to collect, and an optional timeout (in milliseconds). - -```ts -import { describe, test } from 'vitest' - -// The two tests marked with concurrent will be run in parallel -describe('suite', () => { - test('serial test', async () => { /* ... */ }) - test.concurrent('concurrent test 1', async () => { /* ... */ }) - test.concurrent('concurrent test 2', async () => { /* ... */ }) -}) -``` - -`test.skip`, `test.only`, and `test.todo` works with concurrent tests. All the following combinations are valid: - -```ts -test.concurrent(/* ... */) -test.skip.concurrent(/* ... */) // or test.concurrent.skip(/* ... */) -test.only.concurrent(/* ... */) // or test.concurrent.only(/* ... */) -test.todo.concurrent(/* ... */) // or test.concurrent.todo(/* ... */) -``` - -When running concurrent tests, Snapshots and Assertions must use `expect` from the local [Test Context](/guide/test-context.md) to ensure the right test is detected. - -```ts -test.concurrent('test 1', async ({ expect }) => { - expect(foo).toMatchSnapshot() -}) -test.concurrent('test 2', async ({ expect }) => { - expect(foo).toMatchSnapshot() -}) -``` - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### test.sequential - -- **Alias:** `it.sequential` - -`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` - -Use `test.todo` to stub tests to be implemented later. An entry will be shown in the report for the tests so you know how many tests you still need to implement. - -```ts -// An entry will be shown in the report for this test -test.todo('unimplemented test') -``` - -### test.fails - -- **Alias:** `it.fails` - -Use `test.fails` to indicate that an assertion will fail explicitly. - -```ts -import { expect, test } from 'vitest' - -function myAsyncFunc() { - return new Promise(resolve => resolve(1)) -} -test.fails('fail test', async () => { - await expect(myAsyncFunc()).rejects.toBe(1) -}) -``` - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### test.each - -- **Alias:** `it.each` - -::: tip -While `test.each` is provided for Jest compatibility, -Vitest also has [`test.for`](#test-for) with an additional feature to integrate [`TestContext`](/guide/test-context). -::: - -Use `test.each` when you need to run the same test with different variables. -You can inject parameters with [printf formatting](https://nodejs.org/api/util.html#util_util_format_format_args) in the test name in the order of the test function parameters. - -- `%s`: string -- `%d`: number -- `%i`: integer -- `%f`: floating point value -- `%j`: json -- `%o`: object -- `%#`: 0-based index of the test case -- `%$`: 1-based index of the test case -- `%%`: single percent sign ('%') - -```ts -import { expect, test } from 'vitest' - -test.each([ - [1, 1, 2], - [1, 2, 3], - [2, 1, 3], -])('add(%i, %i) -> %i', (a, b, expected) => { - expect(a + b).toBe(expected) -}) - -// this will return -// ✓ add(1, 1) -> 2 -// ✓ add(1, 2) -> 3 -// ✓ add(2, 1) -> 3 -``` - -You can also access object properties and array elements with `$` prefix: - -```ts -test.each([ - { a: 1, b: 1, expected: 2 }, - { a: 1, b: 2, expected: 3 }, - { a: 2, b: 1, expected: 3 }, -])('add($a, $b) -> $expected', ({ a, b, expected }) => { - expect(a + b).toBe(expected) -}) - -// this will return -// ✓ add(1, 1) -> 2 -// ✓ add(1, 2) -> 3 -// ✓ add(2, 1) -> 3 - -test.each([ - [1, 1, 2], - [1, 2, 3], - [2, 1, 3], -])('add($0, $1) -> $2', (a, b, expected) => { - expect(a + b).toBe(expected) -}) - -// this will return -// ✓ add(1, 1) -> 2 -// ✓ add(1, 2) -> 3 -// ✓ add(2, 1) -> 3 -``` - -You can also access Object attributes with `.`, if you are using objects as arguments: - - ```ts - test.each` - a | b | expected - ${{ val: 1 }} | ${'b'} | ${'1b'} - ${{ val: 2 }} | ${'b'} | ${'2b'} - ${{ val: 3 }} | ${'b'} | ${'3b'} - `('add($a.val, $b) -> $expected', ({ a, b, expected }) => { - expect(a.val + b).toBe(expected) - }) - - // this will return - // ✓ add(1, b) -> 1b - // ✓ add(2, b) -> 2b - // ✓ add(3, b) -> 3b - ``` - -* First row should be column names, separated by `|`; -* One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax. - -```ts -import { expect, test } from 'vitest' - -test.each` - a | b | expected - ${1} | ${1} | ${2} - ${'a'} | ${'b'} | ${'ab'} - ${[]} | ${'b'} | ${'b'} - ${{}} | ${'b'} | ${'[object Object]b'} - ${{ asd: 1 }} | ${'b'} | ${'[object Object]b'} -`('returns $expected when $a is added $b', ({ a, b, expected }) => { - expect(a + b).toBe(expected) -}) -``` - -::: tip -Vitest processes `$values` with Chai `format` method. If the value is too truncated, you can increase [chaiConfig.truncateThreshold](/config/#chaiconfig-truncatethreshold) in your config file. -::: - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### test.for - -- **Alias:** `it.for` - -Alternative to `test.each` to provide [`TestContext`](/guide/test-context). - -The difference from `test.each` lies in how arrays are provided in the arguments. -Non-array arguments to `test.for` (including template string usage) work exactly the same as for `test.each`. - -```ts -// `each` spreads arrays -test.each([ - [1, 1, 2], - [1, 2, 3], - [2, 1, 3], -])('add(%i, %i) -> %i', (a, b, expected) => { // [!code --] - expect(a + b).toBe(expected) -}) - -// `for` doesn't spread arrays (notice the square brackets around the arguments) -test.for([ - [1, 1, 2], - [1, 2, 3], - [2, 1, 3], -])('add(%i, %i) -> %i', ([a, b, expected]) => { // [!code ++] - expect(a + b).toBe(expected) -}) -``` - -The 2nd argument is [`TestContext`](/guide/test-context) and can be used for concurrent snapshots, for example: - -```ts -test.concurrent.for([ - [1, 1], - [1, 2], - [2, 1], -])('add(%i, %i)', ([a, b], { expect }) => { - expect(a + b).matchSnapshot() -}) -``` - -## bench - -- **Type:** `(name: string | Function, fn: BenchFunction, options?: BenchOptions) => void` - -`bench` defines a benchmark. In Vitest terms, benchmark is a function that defines a series of operations. Vitest runs this function multiple times to display different performance results. - -Vitest uses the [`tinybench`](https://github.com/tinylibs/tinybench) library under the hood, inheriting all its options that can be used as a third argument. - -```ts -import { bench } from 'vitest' - -bench('normal sorting', () => { - const x = [1, 5, 4, 2, 3] - x.sort((a, b) => { - return a - b - }) -}, { time: 1000 }) -``` - -```ts -export interface Options { - /** - * time needed for running a benchmark task (milliseconds) - * @default 500 - */ - time?: number - - /** - * number of times that a task should run if even the time option is finished - * @default 10 - */ - iterations?: number - - /** - * function to get the current timestamp in milliseconds - */ - now?: () => number - - /** - * An AbortSignal for aborting the benchmark - */ - signal?: AbortSignal - - /** - * Throw if a task fails (events will not work if true) - */ - throws?: boolean - - /** - * warmup time (milliseconds) - * @default 100ms - */ - warmupTime?: number - - /** - * warmup iterations - * @default 5 - */ - warmupIterations?: number - - /** - * setup function to run before each benchmark task (cycle) - */ - setup?: Hook - - /** - * teardown function to run after each benchmark task (cycle) - */ - teardown?: Hook -} -``` -After the test case is run, the output structure information is as follows: - -``` - name hz min max mean p75 p99 p995 p999 rme samples -· normal sorting 6,526,368.12 0.0001 0.3638 0.0002 0.0002 0.0002 0.0002 0.0004 ±1.41% 652638 -``` -```ts -export interface TaskResult { - /* - * the last error that was thrown while running the task - */ - error?: unknown - - /** - * The amount of time in milliseconds to run the benchmark task (cycle). - */ - totalTime: number - - /** - * the minimum value in the samples - */ - min: number - /** - * the maximum value in the samples - */ - max: number - - /** - * the number of operations per second - */ - hz: number - - /** - * how long each operation takes (ms) - */ - period: number - - /** - * task samples of each task iteration time (ms) - */ - samples: number[] - - /** - * samples mean/average (estimate of the population mean) - */ - mean: number - - /** - * samples variance (estimate of the population variance) - */ - variance: number - - /** - * samples standard deviation (estimate of the population standard deviation) - */ - sd: number - - /** - * standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean) - */ - sem: number - - /** - * degrees of freedom - */ - df: number - - /** - * critical value of the samples - */ - critical: number - - /** - * margin of error - */ - moe: number - - /** - * relative margin of error - */ - rme: number - - /** - * median absolute deviation - */ - mad: number - - /** - * p50/median percentile - */ - p50: number - - /** - * p75 percentile - */ - p75: number - - /** - * p99 percentile - */ - p99: number - - /** - * p995 percentile - */ - p995: number - - /** - * p999 percentile - */ - p999: number -} -``` - -### bench.skip - -- **Type:** `(name: string | Function, fn: BenchFunction, options?: BenchOptions) => void` - -You can use `bench.skip` syntax to skip running certain benchmarks. - -```ts -import { bench } from 'vitest' - -bench.skip('normal sorting', () => { - const x = [1, 5, 4, 2, 3] - x.sort((a, b) => { - return a - b - }) -}) -``` - -### bench.only - -- **Type:** `(name: string | Function, fn: BenchFunction, options?: BenchOptions) => void` - -Use `bench.only` to only run certain benchmarks in a given suite. This is useful when debugging. - -```ts -import { bench } from 'vitest' - -bench.only('normal sorting', () => { - const x = [1, 5, 4, 2, 3] - x.sort((a, b) => { - return a - b - }) -}) -``` - -### bench.todo - -- **Type:** `(name: string | Function) => void` - -Use `bench.todo` to stub benchmarks to be implemented later. - -```ts -import { bench } from 'vitest' - -bench.todo('unimplemented test') -``` - -## describe - -When you use `test` or `bench` in the top level of file, they are collected as part of the implicit suite for it. Using `describe` you can define a new suite in the current context, as a set of related tests or benchmarks and other nested suites. A suite lets you organize your tests and benchmarks so reports are more clear. - -```ts -// basic.spec.ts -// organizing tests - -import { describe, expect, test } from 'vitest' - -const person = { - isActive: true, - age: 32, -} - -describe('person', () => { - test('person is defined', () => { - expect(person).toBeDefined() - }) - - test('is active', () => { - expect(person.isActive).toBeTruthy() - }) - - test('age limit', () => { - expect(person.age).toBeLessThanOrEqual(32) - }) -}) -``` - -```ts -// basic.bench.ts -// organizing benchmarks - -import { bench, describe } from 'vitest' - -describe('sort', () => { - bench('normal', () => { - const x = [1, 5, 4, 2, 3] - x.sort((a, b) => { - return a - b - }) - }) - - bench('reverse', () => { - const x = [1, 5, 4, 2, 3] - x.reverse().sort((a, b) => { - return a - b - }) - }) -}) -``` - -You can also nest describe blocks if you have a hierarchy of tests or benchmarks: - -```ts -import { describe, expect, test } from 'vitest' - -function numberToCurrency(value: number | string) { - if (typeof value !== 'number') { - throw new TypeError('Value must be a number') - } - - return value.toFixed(2).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') -} - -describe('numberToCurrency', () => { - describe('given an invalid number', () => { - test('composed of non-numbers to throw error', () => { - expect(() => numberToCurrency('abc')).toThrowError() - }) - }) - - describe('given a valid number', () => { - test('returns the correct currency format', () => { - expect(numberToCurrency(10000)).toBe('10,000.00') - }) - }) -}) -``` - -### describe.skip - -- **Alias:** `suite.skip` - -Use `describe.skip` in a suite to avoid running a particular describe block. - -```ts -import { assert, describe, test } from 'vitest' - -describe.skip('skipped suite', () => { - test('sqrt', () => { - // Suite skipped, no error - assert.equal(Math.sqrt(4), 3) - }) -}) -``` - -### describe.skipIf - -- **Alias:** `suite.skipIf` - -In some cases, you might run suites multiple times with different environments, and some of the suites might be environment-specific. Instead of wrapping the suite with `if`, you can use `describe.skipIf` to skip the suite whenever the condition is truthy. - -```ts -import { describe, test } from 'vitest' - -const isDev = process.env.NODE_ENV === 'development' - -describe.skipIf(isDev)('prod only test suite', () => { - // this test suite only runs in production -}) -``` - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### describe.runIf - -- **Alias:** `suite.runIf` - -Opposite of [describe.skipIf](#describe-skipif). - -```ts -import { assert, describe, test } from 'vitest' - -const isDev = process.env.NODE_ENV === 'development' - -describe.runIf(isDev)('dev only test suite', () => { - // this test suite only runs in development -}) -``` - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### describe.only - -- **Type:** `(name: string | Function, fn: TestFunction, options?: number | TestOptions) => void` - -Use `describe.only` to only run certain suites - -```ts -import { assert, describe, test } from 'vitest' - -// Only this suite (and others marked with only) are run -describe.only('suite', () => { - test('sqrt', () => { - assert.equal(Math.sqrt(4), 3) - }) -}) - -describe('other suite', () => { - // ... will be skipped -}) -``` - -Sometimes it is very useful to run `only` tests in a certain file, ignoring all other tests from the whole test suite, which pollute the output. - -In order to do that run `vitest` with specific file containing the tests in question. -``` -# vitest interesting.test.ts -``` - -### describe.concurrent - -- **Alias:** `suite.concurrent` - -`describe.concurrent` runs all inner suites and tests in parallel - -```ts -import { describe, test } from 'vitest' - -// All suites and tests within this suite will be run in parallel -describe.concurrent('suite', () => { - test('concurrent test 1', async () => { /* ... */ }) - describe('concurrent suite 2', async () => { - test('concurrent test inner 1', async () => { /* ... */ }) - test('concurrent test inner 2', async () => { /* ... */ }) - }) - test.concurrent('concurrent test 3', async () => { /* ... */ }) -}) -``` - -`.skip`, `.only`, and `.todo` works with concurrent suites. All the following combinations are valid: - -```ts -describe.concurrent(/* ... */) -describe.skip.concurrent(/* ... */) // or describe.concurrent.skip(/* ... */) -describe.only.concurrent(/* ... */) // or describe.concurrent.only(/* ... */) -describe.todo.concurrent(/* ... */) // or describe.concurrent.todo(/* ... */) -``` - -When running concurrent tests, Snapshots and Assertions must use `expect` from the local [Test Context](/guide/test-context) to ensure the right test is detected. - -```ts -describe.concurrent('suite', () => { - test('concurrent test 1', async ({ expect }) => { - expect(foo).toMatchSnapshot() - }) - test('concurrent test 2', async ({ expect }) => { - expect(foo).toMatchSnapshot() - }) -}) -``` - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### describe.sequential - -- **Alias:** `suite.sequential` - -`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` - -Vitest provides a way to run all tests in random order via CLI flag [`--sequence.shuffle`](/guide/cli) or config option [`sequence.shuffle`](/config/#sequence-shuffle), but if you want to have only part of your test suite to run tests in random order, you can mark it with this flag. - -```ts -import { describe, test } from 'vitest' - -// or describe('suite', { shuffle: true }, ...) -describe.shuffle('suite', () => { - test('random test 1', async () => { /* ... */ }) - test('random test 2', async () => { /* ... */ }) - test('random test 3', async () => { /* ... */ }) - - // `shuffle` is inherited - describe('still random', () => { - test('random 4.1', async () => { /* ... */ }) - test('random 4.2', async () => { /* ... */ }) - }) - - // disable shuffle inside - describe('not random', { shuffle: false }, () => { - test('in order 5.1', async () => { /* ... */ }) - test('in order 5.2', async () => { /* ... */ }) - }) -}) -// order depends on sequence.seed option in config (Date.now() by default) -``` - -`.skip`, `.only`, and `.todo` works with random suites. - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### describe.todo - -- **Alias:** `suite.todo` - -Use `describe.todo` to stub suites to be implemented later. An entry will be shown in the report for the tests so you know how many tests you still need to implement. - -```ts -// An entry will be shown in the report for this suite -describe.todo('unimplemented suite') -``` - -### describe.each - -- **Alias:** `suite.each` - -::: tip -While `describe.each` is provided for Jest compatibility, -Vitest also has [`describe.for`](#describe-for) which simplifies argument types and aligns with [`test.for`](#test-for). -::: - -Use `describe.each` if you have more than one test that depends on the same data. - -```ts -import { describe, expect, test } from 'vitest' - -describe.each([ - { a: 1, b: 1, expected: 2 }, - { a: 1, b: 2, expected: 3 }, - { a: 2, b: 1, expected: 3 }, -])('describe object add($a, $b)', ({ a, b, expected }) => { - test(`returns ${expected}`, () => { - expect(a + b).toBe(expected) - }) - - test(`returned value not be greater than ${expected}`, () => { - expect(a + b).not.toBeGreaterThan(expected) - }) - - test(`returned value not be less than ${expected}`, () => { - expect(a + b).not.toBeLessThan(expected) - }) -}) -``` - -* First row should be column names, separated by `|`; -* One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax. - -```ts -import { describe, expect, test } from 'vitest' - -describe.each` - a | b | expected - ${1} | ${1} | ${2} - ${'a'} | ${'b'} | ${'ab'} - ${[]} | ${'b'} | ${'b'} - ${{}} | ${'b'} | ${'[object Object]b'} - ${{ asd: 1 }} | ${'b'} | ${'[object Object]b'} -`('describe template string add($a, $b)', ({ a, b, expected }) => { - test(`returns ${expected}`, () => { - expect(a + b).toBe(expected) - }) -}) -``` - -::: warning -You cannot use this syntax when using Vitest as [type checker](/guide/testing-types). -::: - -### describe.for - -- **Alias:** `suite.for` - -The difference from `describe.each` is how array case is provided in the arguments. -Other non array case (including template string usage) works exactly same. - -```ts -// `each` spreads array case -describe.each([ - [1, 1, 2], - [1, 2, 3], - [2, 1, 3], -])('add(%i, %i) -> %i', (a, b, expected) => { // [!code --] - test('test', () => { - expect(a + b).toBe(expected) - }) -}) - -// `for` doesn't spread array case -describe.for([ - [1, 1, 2], - [1, 2, 3], - [2, 1, 3], -])('add(%i, %i) -> %i', ([a, b, expected]) => { // [!code ++] - test('test', () => { - expect(a + b).toBe(expected) - }) -}) -``` - -## Setup and Teardown - -These functions allow you to hook into the life cycle of tests to avoid repeating setup and teardown code. They apply to the current context: the file if they are used at the top-level or the current suite if they are inside a `describe` block. These hooks are not called, when you are running Vitest as a type checker. - -### beforeEach - -- **Type:** `beforeEach(fn: () => Awaitable, timeout?: number)` - -Register a callback to be called before each of the tests in the current context runs. -If the function returns a promise, Vitest waits until the promise resolve before running the test. - -Optionally, you can pass a timeout (in milliseconds) defining how long to wait before terminating. The default is 5 seconds. - -```ts -import { beforeEach } from 'vitest' - -beforeEach(async () => { - // Clear mocks and add some testing data before each test run - await stopMocking() - await addUser({ name: 'John' }) -}) -``` - -Here, the `beforeEach` ensures that user is added for each test. - -`beforeEach` also accepts an optional cleanup function (equivalent to `afterEach`). - -```ts -import { beforeEach } from 'vitest' - -beforeEach(async () => { - // called once before each test run - await prepareSomething() - - // clean up function, called once after each test run - return async () => { - await resetSomething() - } -}) -``` - -### afterEach - -- **Type:** `afterEach(fn: () => Awaitable, timeout?: number)` - -Register a callback to be called after each one of the tests in the current context completes. -If the function returns a promise, Vitest waits until the promise resolve before continuing. - -Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The default is 5 seconds. - -```ts -import { afterEach } from 'vitest' - -afterEach(async () => { - await clearTestingData() // clear testing data after each test run -}) -``` - -Here, the `afterEach` ensures that testing data is cleared after each test runs. - -::: tip -You can also use [`onTestFinished`](#ontestfinished) during the test execution to cleanup any state after the test has finished running. -::: - -### beforeAll - -- **Type:** `beforeAll(fn: () => Awaitable, timeout?: number)` - -Register a callback to be called once before starting to run all tests in the current context. -If the function returns a promise, Vitest waits until the promise resolve before running tests. - -Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The default is 5 seconds. - -```ts -import { beforeAll } from 'vitest' - -beforeAll(async () => { - await startMocking() // called once before all tests run -}) -``` - -Here the `beforeAll` ensures that the mock data is set up before tests run. - -`beforeAll` also accepts an optional cleanup function (equivalent to `afterAll`). - -```ts -import { beforeAll } from 'vitest' - -beforeAll(async () => { - // called once before all tests run - await startMocking() - - // clean up function, called once after all tests run - return async () => { - await stopMocking() - } -}) -``` - -### afterAll - -- **Type:** `afterAll(fn: () => Awaitable, timeout?: number)` - -Register a callback to be called once after all tests have run in the current context. -If the function returns a promise, Vitest waits until the promise resolve before continuing. - -Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating. The default is 5 seconds. - -```ts -import { afterAll } from 'vitest' - -afterAll(async () => { - await stopMocking() // this method is called after all tests run -}) -``` - -Here the `afterAll` ensures that `stopMocking` method is called after all tests run. - -## Test Hooks - -Vitest provides a few hooks that you can call _during_ the test execution to cleanup the state when the test has finished running. - -::: warning -These hooks will throw an error if they are called outside of the test body. -::: - -### onTestFinished {#ontestfinished} - -This hook is always called after the test has finished running. It is called after `afterEach` hooks since they can influence the test result. It receives an `ExtendedContext` object like `beforeEach` and `afterEach`. - -```ts {1,5} -import { onTestFinished, test } from 'vitest' - -test('performs a query', () => { - const db = connectDb() - onTestFinished(() => db.close()) - db.query('SELECT * FROM users') -}) -``` - -::: warning -If you are running tests concurrently, you should always use `onTestFinished` hook from the test context since Vitest doesn't track concurrent tests in global hooks: - -```ts {3,5} -import { test } from 'vitest' - -test.concurrent('performs a query', ({ onTestFinished }) => { - const db = connectDb() - onTestFinished(() => db.close()) - db.query('SELECT * FROM users') -}) -``` -::: - -This hook is particularly useful when creating reusable logic: - -```ts -// this can be in a separate file -function getTestDb() { - const db = connectMockedDb() - onTestFinished(() => db.close()) - return db -} - -test('performs a user query', async () => { - const db = getTestDb() - expect( - await db.query('SELECT * from users').perform() - ).toEqual([]) -}) - -test('performs an organization query', async () => { - const db = getTestDb() - expect( - await db.query('SELECT * from organizations').perform() - ).toEqual([]) -}) -``` - -::: tip -This hook is always called in reverse order and is not affected by [`sequence.hooks`](/config/#sequence-hooks) option. -::: - -### onTestFailed - -This hook is called only after the test has failed. It is called after `afterEach` hooks since they can influence the test result. It receives an `ExtendedContext` object like `beforeEach` and `afterEach`. This hook is useful for debugging. - -```ts {1,5-7} -import { onTestFailed, test } from 'vitest' - -test('performs a query', () => { - const db = connectDb() - onTestFailed(({ task }) => { - console.log(task.result.errors) - }) - db.query('SELECT * FROM users') -}) -``` - -::: warning -If you are running tests concurrently, you should always use `onTestFailed` hook from the test context since Vitest doesn't track concurrent tests in global hooks: - -```ts {3,5-7} -import { test } from 'vitest' - -test.concurrent('performs a query', ({ onTestFailed }) => { - const db = connectDb() - onTestFailed(({ task }) => { - console.log(task.result.errors) - }) - db.query('SELECT * FROM users') -}) -``` -::: diff --git a/docs/api/test.md b/docs/api/test.md new file mode 100644 index 000000000000..c44e890f87f0 --- /dev/null +++ b/docs/api/test.md @@ -0,0 +1,857 @@ +--- +outline: deep +--- + +# Test + +- **Alias:** `it` + +```ts +function test( + name: string | Function, + body?: () => unknown, + timeout?: number +): void +function test( + name: string | Function, + options: TestOptions, + body?: () => unknown, +): void +``` + +`test` or `it` defines a set of related expectations. It receives the test name and a function that holds the expectations to test. + +Optionally, you can provide a timeout (in milliseconds) for specifying how long to wait before terminating, or a set of [additional options](#test-options). The default timeout is 5 seconds, and can be configured globally with [`testTimeout`](/config/testtimeout). + +```ts +import { expect, test } from 'vitest' + +test('should work as expected', () => { + expect(Math.sqrt(4)).toBe(2) +}) +``` + +::: warning +If the first argument is a function, its `name` property will be used as the name of the test. The function itself will not be called. + +If test body is not provided, the test is marked as `todo`. +::: + +When a test function returns a promise, the runner will wait until it is resolved to collect async expectations. If the promise is rejected, the test will fail. + +::: tip +In Jest, `TestFunction` can also be of type `(done: DoneCallback) => void`. If this form is used, the test will not be concluded until `done` is called. You can achieve the same using an `async` function, see the [Migration guide Done Callback section](/guide/migration#done-callback). +::: + +## Test Options + +You can define boolean options by chaining properties on a function: + +```ts +import { test } from 'vitest' + +test.skip('skipped test', () => { + // some logic that fails right now +}) + +test.concurrent.skip('skipped concurrent test', () => { + // some logic that fails right now +}) +``` + +But you can also provide an object as a second argument instead: + +```ts +import { test } from 'vitest' + +test('skipped test', { skip: true }, () => { + // some logic that fails right now +}) + +test('skipped concurrent test', { skip: true, concurrent: true }, () => { + // some logic that fails right now +}) +``` + +They both work in exactly the same way. To use either one is purely a stylistic choice. + +### timeout + +- **Type:** `number` +- **Default:** `5_000` (configured by [`testTimeout`](/config/testtimeout)) + +Test timeout in milliseconds. + +::: warning +Note that if you are providing timeout as the last argument, you cannot use options anymore: + +```ts +import { test } from 'vitest' + +// ✅ this works +test.skip('heavy test', () => { + // ... +}, 10_000) + +// ❌ this doesn't work +test('heavy test', { skip: true }, () => { + // ... +}, 10_000) +``` + +However, you can provide a timeout inside the object: + +```ts +import { test } from 'vitest' + +// ✅ this works +test('heavy test', { skip: true, timeout: 10_000 }, () => { + // ... +}) +``` +::: + +### retry + +- **Default:** `0` (configured by [`retry`](/config/retry)) +- **Type:** + +```ts +type Retry = number | { + /** + * The number of times to retry the test if it fails. + * @default 0 + */ + count?: number + /** + * Delay in milliseconds between retry attempts. + * @default 0 + */ + delay?: number + /** + * Condition to determine if a test should be retried based on the error. + * - If a RegExp, it is tested against the error message + * - If a function, called with the TestError object; return true to retry + * + * NOTE: Functions can only be used in test files, not in vitest.config.ts, + * because the configuration is serialized when passed to worker threads. + * + * @default undefined (retry on all errors) + */ + condition?: RegExp | ((error: TestError) => boolean) +} +``` + +Retry configuration for the test. If a number, specifies how many times to retry. If an object, allows fine-grained retry control. + +Note that the object configuration is available only since Vitest 4.1. + +### repeats + +- **Type:** `number` +- **Default:** `0` + +How many times the test will run again. If set to `0` (the default), the test will run only one time. + +This can be useful for debugging flaky tests. + +### tags 4.1.0 {#tags} + +- **Type:** `string[]` +- **Default:** `[]` + +Custom user [tags](/guide/test-tags). If the tag is not specified in the [configuration](/config/tags), the test will fail before it starts, unless [`strictTags`](/config/stricttags) is disabled manually. + +```ts +import { it } from 'vitest' + +it('user returns data from db', { tags: ['db', 'flaky'] }, () => { + // ... +}) +``` + +### concurrent + +- **Type:** `boolean` +- **Default:** `false` (configured by [`sequence.concurrent`](/config/sequence#sequence-concurrent)) +- **Alias:** [`test.concurrent`](#test-concurrent) + +Whether this test run concurrently with other concurrent tests in the suite. + +### sequential + +- **Type:** `boolean` +- **Default:** `true` +- **Alias:** [`test.sequential`](#test-sequential) + +Whether tests run sequentially. When both `concurrent` and `sequential` are specified, `concurrent` takes precendence. + +### skip + +- **Type:** `boolean` +- **Default:** `false` +- **Alias:** [`test.skip`](#test-skip) + +Whether the test should be skipped. + +### only + +- **Type:** `boolean` +- **Default:** `false` +- **Alias:** [`test.only`](#test-only) + +Should this test be the only one running in a suite. + +### todo + +- **Type:** `boolean` +- **Default:** `false` +- **Alias:** [`test.todo`](#test-todo) + +Whether the test should be skipped and marked as a todo. + +### fails + +- **Type:** `boolean` +- **Default:** `false` +- **Alias:** [`test.fails`](#test-fails) + +Whether the test is expected to fail. If it does, the test will pass, otherwise it will fail. + +## test.extend + +- **Alias:** `it.extend` + +Use `test.extend` to extend the test context with custom fixtures. This will return a new `test` and it's also extendable, so you can compose more fixtures or override existing ones by extending it as you need. See [Extend Test Context](/guide/test-context.html#test-extend) for more information. + +```ts +import { test as baseTest, expect } from 'vitest' + +const todos = [] +const archive = [] + +const test = baseTest.extend({ + todos: async ({ task }, use) => { + todos.push(1, 2, 3) + await use(todos) + todos.length = 0 + }, + archive, +}) + +test('add item', ({ todos }) => { + expect(todos.length).toBe(3) + + todos.push(4) + expect(todos.length).toBe(4) +}) +``` + +## test.skip + +- **Alias:** `it.skip` + +If you want to skip running certain tests, but you don't want to delete the code due to any reason, you can use `test.skip` to avoid running them. + +```ts +import { assert, test } from 'vitest' + +test.skip('skipped test', () => { + // Test skipped, no error + assert.equal(Math.sqrt(4), 3) +}) +``` + +You can also skip test by calling `skip` on its [context](/guide/test-context) dynamically: + +```ts +import { assert, test } from 'vitest' + +test('skipped test', (context) => { + context.skip() + // Test skipped, no error + assert.equal(Math.sqrt(4), 3) +}) +``` + +If the condition is unknown, you can provide it to the `skip` method as the first arguments: + +```ts +import { assert, test } from 'vitest' + +test('skipped test', (context) => { + context.skip(Math.random() < 0.5, 'optional message') + // Test skipped, no error + assert.equal(Math.sqrt(4), 3) +}) +``` + +## test.skipIf + +- **Alias:** `it.skipIf` + +In some cases you might run tests multiple times with different environments, and some of the tests might be environment-specific. Instead of wrapping the test code with `if`, you can use `test.skipIf` to skip the test whenever the condition is truthy. + +```ts +import { assert, test } from 'vitest' + +const isDev = process.env.NODE_ENV === 'development' + +test.skipIf(isDev)('prod only test', () => { + // this test only runs in production +}) +``` + +## test.runIf + +- **Alias:** `it.runIf` + +Opposite of [test.skipIf](#test-skipif). + +```ts +import { assert, test } from 'vitest' + +const isDev = process.env.NODE_ENV === 'development' + +test.runIf(isDev)('dev only test', () => { + // this test only runs in development +}) +``` + +## test.only + +- **Alias:** `it.only` + +Use `test.only` to only run certain tests in a given suite. This is useful when debugging. + +```ts +import { assert, test } from 'vitest' + +test.only('test', () => { + // Only this test (and others marked with only) are run + assert.equal(Math.sqrt(4), 2) +}) +``` + +Sometimes it is very useful to run `only` tests in a certain file, ignoring all other tests from the whole test suite, which pollute the output. + +In order to do that, run `vitest` with specific file containing the tests in question: + +```shell +vitest interesting.test.ts +``` + +::: warning +Vitest detects when tests are running in CI and will throw an error if any test has `only` flag. You can configure this behaviour via [`allowOnly`](/config/allowonly) option. +::: + +## test.concurrent + +- **Alias:** `it.concurrent` + +`test.concurrent` marks consecutive tests to be run in parallel. It receives the test name, an async function with the tests to collect, and an optional timeout (in milliseconds). + +```ts +import { describe, test } from 'vitest' + +// The two tests marked with concurrent will be run in parallel +describe('suite', () => { + test('serial test', async () => { /* ... */ }) + test.concurrent('concurrent test 1', async () => { /* ... */ }) + test.concurrent('concurrent test 2', async () => { /* ... */ }) +}) +``` + +`test.skip`, `test.only`, and `test.todo` works with concurrent tests. All the following combinations are valid: + +```ts +test.concurrent(/* ... */) +test.skip.concurrent(/* ... */) // or test.concurrent.skip(/* ... */) +test.only.concurrent(/* ... */) // or test.concurrent.only(/* ... */) +test.todo.concurrent(/* ... */) // or test.concurrent.todo(/* ... */) +``` + +When running concurrent tests, Snapshots and Assertions must use `expect` from the local [Test Context](/guide/test-context.md) to ensure the right test is detected. + +```ts +test.concurrent('test 1', async ({ expect }) => { + expect(foo).toMatchSnapshot() +}) +test.concurrent('test 2', async ({ expect }) => { + expect(foo).toMatchSnapshot() +}) +``` + +Note that if tests are synchronous, Vitest will still run them sequentially. + +## test.sequential + +- **Alias:** `it.sequential` + +`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` + +Use `test.todo` to stub tests to be implemented later. An entry will be shown in the report for the tests so you know how many tests you still need to implement. + +```ts +// An entry will be shown in the report for this test +test.todo('unimplemented test', () => { + // failing implementation... +}) +``` + +::: tip +Vitest will automatically mark test as `todo` if test has no body. +::: + +## test.fails + +- **Alias:** `it.fails` + +Use `test.fails` to indicate that an assertion will fail explicitly. + +```ts +import { expect, test } from 'vitest' + +test.fails('repro #1234', () => { + expect(add(1, 2)).toBe(4) +}) +``` + +This flag is useful to track difference in behaviour of your library over time. For example, you can define a failing test without fixing the issue yet due to time constraints. Tests marked with `fails` are tracked in the test summary since Vitest 4.1. + +## test.each + +- **Alias:** `it.each` + +::: tip +While `test.each` is provided for Jest compatibility, +Vitest also has [`test.for`](#test-for) with an additional feature to integrate [`TestContext`](/guide/test-context). +::: + +Use `test.each` when you need to run the same test with different variables. +You can inject parameters with [printf formatting](https://nodejs.org/api/util.html#util_util_format_format_args) in the test name in the order of the test function parameters. + +- `%s`: string +- `%d`: number +- `%i`: integer +- `%f`: floating point value +- `%j`: json +- `%o`: object +- `%#`: 0-based index of the test case +- `%$`: 1-based index of the test case +- `%%`: single percent sign ('%') + +```ts +import { expect, test } from 'vitest' + +test.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) -> %i', (a, b, expected) => { + expect(a + b).toBe(expected) +}) + +// this will return +// ✓ add(1, 1) -> 2 +// ✓ add(1, 2) -> 3 +// ✓ add(2, 1) -> 3 +``` + +You can also access object properties and array elements with `$` prefix: + +```ts +test.each([ + { a: 1, b: 1, expected: 2 }, + { a: 1, b: 2, expected: 3 }, + { a: 2, b: 1, expected: 3 }, +])('add($a, $b) -> $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) + +// this will return +// ✓ add(1, 1) -> 2 +// ✓ add(1, 2) -> 3 +// ✓ add(2, 1) -> 3 + +test.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add($0, $1) -> $2', (a, b, expected) => { + expect(a + b).toBe(expected) +}) + +// this will return +// ✓ add(1, 1) -> 2 +// ✓ add(1, 2) -> 3 +// ✓ add(2, 1) -> 3 +``` + +You can also access Object attributes with `.`, if you are using objects as arguments: + + ```ts + test.each` + a | b | expected + ${{ val: 1 }} | ${'b'} | ${'1b'} + ${{ val: 2 }} | ${'b'} | ${'2b'} + ${{ val: 3 }} | ${'b'} | ${'3b'} + `('add($a.val, $b) -> $expected', ({ a, b, expected }) => { + expect(a.val + b).toBe(expected) + }) + + // this will return + // ✓ add(1, b) -> 1b + // ✓ add(2, b) -> 2b + // ✓ add(3, b) -> 3b + ``` + +* First row should be column names, separated by `|`; +* One or more subsequent rows of data supplied as template literal expressions using `${value}` syntax. + +```ts +import { expect, test } from 'vitest' + +test.each` + a | b | expected + ${1} | ${1} | ${2} + ${'a'} | ${'b'} | ${'ab'} + ${[]} | ${'b'} | ${'b'} + ${{}} | ${'b'} | ${'[object Object]b'} + ${{ asd: 1 }} | ${'b'} | ${'[object Object]b'} +`('returns $expected when $a is added $b', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) +``` + +::: tip +Vitest processes `$values` with Chai `format` method. If the value is too truncated, you can increase [chaiConfig.truncateThreshold](/config/#chaiconfig-truncatethreshold) in your config file. +::: + +## test.for + +- **Alias:** `it.for` + +Alternative to `test.each` to provide [`TestContext`](/guide/test-context). + +The difference from `test.each` lies in how arrays are provided in the arguments. +Non-array arguments to `test.for` (including template string usage) work exactly the same as for `test.each`. + +```ts +// `each` spreads arrays +test.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) -> %i', (a, b, expected) => { // [!code --] + expect(a + b).toBe(expected) +}) + +// `for` doesn't spread arrays (notice the square brackets around the arguments) +test.for([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) -> %i', ([a, b, expected]) => { // [!code ++] + expect(a + b).toBe(expected) +}) +``` + +The 2nd argument is [`TestContext`](/guide/test-context) and can be used for concurrent snapshots, for example: + +```ts +test.concurrent.for([ + [1, 1], + [1, 2], + [2, 1], +])('add(%i, %i)', ([a, b], { expect }) => { + expect(a + b).toMatchSnapshot() +}) +``` + +## test.describe 4.1.0 {#test-describe} + +Scoped `describe`. See [describe](/api/describe) for more information. + +## test.beforeEach + +Scoped `beforeEach` hook that inherits types from [`test.extend`](#test-extend). See [beforeEach](/api/hooks#beforeeach) for more information. + +## test.afterEach + +Scoped `afterEach` hook that inherits types from [`test.extend`](#test-extend). See [afterEach](/api/hooks#aftereach) for more information. + +## test.beforeAll + +Scoped `beforeAll` hook. See [beforeAll](/api/hooks#beforeall) for more information. + +## test.afterAll + +Scoped `afterAll` hook. See [afterAll](/api/hooks#afterall) for more information. + +## test.aroundEach 4.1.0 {#test-aroundeach} + +Scoped `aroundEach` hook that inherits types from [`test.extend`](#test-extend). See [aroundEach](/api/hooks#aroundeach) for more information. + +## test.aroundAll 4.1.0 {#test-aroundall} + +Scoped `aroundAll` hook. See [aroundAll](/api/hooks#aroundall) for more information. + +## bench {#bench} + +- **Type:** `(name: string | Function, fn: BenchFunction, options?: BenchOptions) => void` + +::: danger +Benchmarking is experimental and does not follow SemVer. +::: + +`bench` defines a benchmark. In Vitest terms, benchmark is a function that defines a series of operations. Vitest runs this function multiple times to display different performance results. + +Vitest uses the [`tinybench`](https://github.com/tinylibs/tinybench) library under the hood, inheriting all its options that can be used as a third argument. + +```ts +import { bench } from 'vitest' + +bench('normal sorting', () => { + const x = [1, 5, 4, 2, 3] + x.sort((a, b) => { + return a - b + }) +}, { time: 1000 }) +``` + +```ts +export interface Options { + /** + * time needed for running a benchmark task (milliseconds) + * @default 500 + */ + time?: number + + /** + * number of times that a task should run if even the time option is finished + * @default 10 + */ + iterations?: number + + /** + * function to get the current timestamp in milliseconds + */ + now?: () => number + + /** + * An AbortSignal for aborting the benchmark + */ + signal?: AbortSignal + + /** + * Throw if a task fails (events will not work if true) + */ + throws?: boolean + + /** + * warmup time (milliseconds) + * @default 100ms + */ + warmupTime?: number + + /** + * warmup iterations + * @default 5 + */ + warmupIterations?: number + + /** + * setup function to run before each benchmark task (cycle) + */ + setup?: Hook + + /** + * teardown function to run after each benchmark task (cycle) + */ + teardown?: Hook +} +``` +After the test case is run, the output structure information is as follows: + +``` + name hz min max mean p75 p99 p995 p999 rme samples +· normal sorting 6,526,368.12 0.0001 0.3638 0.0002 0.0002 0.0002 0.0002 0.0004 ±1.41% 652638 +``` +```ts +export interface TaskResult { + /* + * the last error that was thrown while running the task + */ + error?: unknown + + /** + * The amount of time in milliseconds to run the benchmark task (cycle). + */ + totalTime: number + + /** + * the minimum value in the samples + */ + min: number + /** + * the maximum value in the samples + */ + max: number + + /** + * the number of operations per second + */ + hz: number + + /** + * how long each operation takes (ms) + */ + period: number + + /** + * task samples of each task iteration time (ms) + */ + samples: number[] + + /** + * samples mean/average (estimate of the population mean) + */ + mean: number + + /** + * samples variance (estimate of the population variance) + */ + variance: number + + /** + * samples standard deviation (estimate of the population standard deviation) + */ + sd: number + + /** + * standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean) + */ + sem: number + + /** + * degrees of freedom + */ + df: number + + /** + * critical value of the samples + */ + critical: number + + /** + * margin of error + */ + moe: number + + /** + * relative margin of error + */ + rme: number + + /** + * median absolute deviation + */ + mad: number + + /** + * p50/median percentile + */ + p50: number + + /** + * p75 percentile + */ + p75: number + + /** + * p99 percentile + */ + p99: number + + /** + * p995 percentile + */ + p995: number + + /** + * p999 percentile + */ + p999: number +} +``` + +### bench.skip + +- **Type:** `(name: string | Function, fn: BenchFunction, options?: BenchOptions) => void` + +You can use `bench.skip` syntax to skip running certain benchmarks. + +```ts +import { bench } from 'vitest' + +bench.skip('normal sorting', () => { + const x = [1, 5, 4, 2, 3] + x.sort((a, b) => { + return a - b + }) +}) +``` + +### bench.only + +- **Type:** `(name: string | Function, fn: BenchFunction, options?: BenchOptions) => void` + +Use `bench.only` to only run certain benchmarks in a given suite. This is useful when debugging. + +```ts +import { bench } from 'vitest' + +bench.only('normal sorting', () => { + const x = [1, 5, 4, 2, 3] + x.sort((a, b) => { + return a - b + }) +}) +``` + +### bench.todo + +- **Type:** `(name: string | Function) => void` + +Use `bench.todo` to stub benchmarks to be implemented later. + +```ts +import { bench } from 'vitest' + +bench.todo('unimplemented test') +``` diff --git a/docs/api/vi.md b/docs/api/vi.md index e72dcea06d87..cd63286f3226 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -628,7 +628,7 @@ it('calls console.log', () => { ::: ::: tip -You can call [`vi.restoreAllMocks`](#vi-restoreallmocks) inside [`afterEach`](/api/#aftereach) (or enable [`test.restoreMocks`](/config/#restoreMocks)) to restore all methods to their original implementations after every test. This will restore the original [object descriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), so you won't be able to change method's implementation anymore, unless you spy again: +You can call [`vi.restoreAllMocks`](#vi-restoreallmocks) inside [`afterEach`](/api/hooks#aftereach) (or enable [`test.restoreMocks`](/config/#restoreMocks)) to restore all methods to their original implementations after every test. This will restore the original [object descriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), so you won't be able to change method's implementation anymore, unless you spy again: ```ts const cart = { diff --git a/docs/config/allowonly.md b/docs/config/allowonly.md index e6071032f59a..8b7fbaa37d44 100644 --- a/docs/config/allowonly.md +++ b/docs/config/allowonly.md @@ -9,7 +9,7 @@ outline: deep - **Default**: `!process.env.CI` - **CLI:** `--allowOnly`, `--allowOnly=false` -By default, Vitest does not permit tests marked with the [`only`](/api/#test-only) flag in Continuous Integration (CI) environments. Conversely, in local development environments, Vitest allows these tests to run. +By default, Vitest does not permit tests marked with the [`only`](/api/test#test-only) flag in Continuous Integration (CI) environments. Conversely, in local development environments, Vitest allows these tests to run. ::: info Vitest uses [`std-env`](https://www.npmjs.com/package/std-env) package to detect the environment. @@ -32,6 +32,6 @@ vitest --allowOnly ``` ::: -When enabled, Vitest will not fail the test suite if tests marked with [`only`](/api/#test-only) are detected, including in CI environments. +When enabled, Vitest will not fail the test suite if tests marked with [`only`](/api/test#test-only) are detected, including in CI environments. -When disabled, Vitest will fail the test suite if tests marked with [`only`](/api/#test-only) are detected, including in local development environments. +When disabled, Vitest will fail the test suite if tests marked with [`only`](/api/test#test-only) are detected, including in local development environments. diff --git a/docs/config/browser.md b/docs/config/browser.md index 5e3027bdaf17..32f1b765627b 100644 --- a/docs/config/browser.md +++ b/docs/config/browser.md @@ -472,8 +472,8 @@ receives an object with the following properties: - `testName: string` - The [`test`](/api/#test)'s name, including parent - [`describe`](/api/#describe), sanitized. + The [`test`](/api/test)'s name, including parent + [`describe`](/api/describe), sanitized. - `attachmentsDir: string` diff --git a/docs/config/browser/expect.md b/docs/config/browser/expect.md index 501fb0c1573e..3990ed68106d 100644 --- a/docs/config/browser/expect.md +++ b/docs/config/browser/expect.md @@ -110,8 +110,8 @@ receives an object with the following properties: - `testName: string` - The [`test`](/api/#test)'s name, including parent - [`describe`](/api/#describe), sanitized. + The [`test`](/api/test)'s name, including parent + [`describe`](/api/describe), sanitized. - `attachmentsDir: string` diff --git a/docs/config/fileparallelism.md b/docs/config/fileparallelism.md index 20f1143fbdd9..09daf095cc84 100644 --- a/docs/config/fileparallelism.md +++ b/docs/config/fileparallelism.md @@ -12,5 +12,5 @@ outline: deep Should all test files run in parallel. Setting this to `false` will override `maxWorkers` option to `1`. ::: tip -This option doesn't affect tests running in the same file. If you want to run those in parallel, use `concurrent` option on [describe](/api/#describe-concurrent) or via [a config](#sequence-concurrent). +This option doesn't affect tests running in the same file. If you want to run those in parallel, use `concurrent` option on [describe](/api/describe#describe-concurrent) or via [a config](#sequence-concurrent). ::: diff --git a/docs/config/sequence.md b/docs/config/sequence.md index c37fecb43c4d..f3b05eab398f 100644 --- a/docs/config/sequence.md +++ b/docs/config/sequence.md @@ -148,7 +148,7 @@ Changes the order in which hooks are executed. - `parallel` will run hooks in a single group in parallel (hooks in parent suites will still run before the current suite's hooks) ::: tip -This option doesn't affect [`onTestFinished`](/api/#ontestfinished). It is always called in reverse order. +This option doesn't affect [`onTestFinished`](/api/hooks#ontestfinished). It is always called in reverse order. ::: ## sequence.setupFiles {#sequence-setupfiles} diff --git a/docs/config/tags.md b/docs/config/tags.md index 6ef0a77c5fe4..c3ea02606c48 100644 --- a/docs/config/tags.md +++ b/docs/config/tags.md @@ -99,61 +99,13 @@ When a test has both tags, the `timeout` will be `30_000` because `flaky` has a ## Test Options -Tags can define test options that will be applied to every test marked with the tag. These options are merged with the test's own options, with the test's options taking precedence. +Tags can define [test options](/api/test#test-options) that will be applied to every test marked with the tag. These options are merged with the test's own options, with the test's options taking precedence. -### timeout +::: warning +The [`retry.condition`](/api/test#retry) can onle be a regexp because the config values need to be serialised. -- **Type:** `number` - -Test timeout in milliseconds. - -### retry - -- **Type:** `number | { count?: number, delay?: number, condition?: RegExp }` - -Retry configuration for the test. If a number, specifies how many times to retry. If an object, allows fine-grained retry control. - -### repeats - -- **Type:** `number` - -How many times the test will run again. - -### concurrent - -- **Type:** `boolean` - -Whether suites and tests run concurrently. - -### sequential - -- **Type:** `boolean` - -Whether tests run sequentially. - -### skip - -- **Type:** `boolean` - -Whether the test should be skipped. - -### only - -- **Type:** `boolean` - -Should this test be the only one running in a suite. - -### todo - -- **Type:** `boolean` - -Whether the test should be skipped and marked as a todo. - -### fails - -- **Type:** `boolean` - -Whether the test is expected to fail. If it does, the test will pass, otherwise it will fail. +Tags also cannot apply other [tags](/api/test#tags) via these options. +::: ## Example diff --git a/docs/guide/browser/trace-view.md b/docs/guide/browser/trace-view.md index 52882d6b4918..20ff556898bc 100644 --- a/docs/guide/browser/trace-view.md +++ b/docs/guide/browser/trace-view.md @@ -25,7 +25,7 @@ vitest --browser.trace=on ``` ::: -By default, Vitest will generate a trace file for each test. You can also configure it to only generate traces on test failures by setting `trace` to `'on-first-retry'`, `'on-all-retries'` or `'retain-on-failure'`. The files will be saved in `__traces__` folder next to your test files. The name of the trace includes the project name, the test name, the [`repeats` count and `retry` count](/api/#test-api-reference): +By default, Vitest will generate a trace file for each test. You can also configure it to only generate traces on test failures by setting `trace` to `'on-first-retry'`, `'on-all-retries'` or `'retain-on-failure'`. The files will be saved in `__traces__` folder next to your test files. The name of the trace includes the project name, the test name, the [`repeats`](/api/test#repeats) count and [`retry`](/api/test#retry) count: ``` chromium-my-test-0-0.trace.zip diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index b1f59e61ef4f..7139715daa89 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -844,3 +844,17 @@ Enable caching of modules on the file system between reruns. - **Config:** [experimental.printImportBreakdown](/config/experimental#experimental-printimportbreakdown) Print import breakdown after the summary. If the reporter doesn't support summary, this will have no effect. Note that UI's "Module Graph" tab always has an import breakdown. + +### experimental.viteModuleRunner + +- **CLI:** `--experimental.viteModuleRunner` +- **Config:** [experimental.viteModuleRunner](/config/experimental#experimental-vitemodulerunner) + +Control whether Vitest uses Vite's module runner to run the code or fallback to the native `import`. (default: `true`) + +### experimental.nodeLoader + +- **CLI:** `--experimental.nodeLoader` +- **Config:** [experimental.nodeLoader](/config/experimental#experimental-nodeloader) + +Controls whether Vitest will use Node.js Loader API to process in-source or mocked files. This has no effect if `viteModuleRunner` is enabled. Disabling this can increase performance. (default: `true`) diff --git a/docs/guide/features.md b/docs/guide/features.md index c75f1d40fe05..a0dd58de27e0 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -77,7 +77,7 @@ describe.concurrent('suite', () => { }) ``` -You can also use `.skip`, `.only`, and `.todo` with concurrent suites and tests. Read more in the [API Reference](/api/#test-concurrent). +You can also use `.skip`, `.only`, and `.todo` with concurrent suites and tests. Read more in the [API Reference](/api/test#test-concurrent). ::: warning When running concurrent tests, Snapshots and Assertions must use `expect` from the local [Test Context](/guide/test-context) to ensure the right test is detected. @@ -192,7 +192,7 @@ Learn more at [In-source testing](/guide/in-source). ## Benchmarking Experimental {#benchmarking} -You can run benchmark tests with [`bench`](/api/#bench) function via [Tinybench](https://github.com/tinylibs/tinybench) to compare performance results. +You can run benchmark tests with [`bench`](/api/test#bench) function via [Tinybench](https://github.com/tinylibs/tinybench) to compare performance results. ```ts [sort.bench.ts] import { bench, describe } from 'vitest' diff --git a/docs/guide/index.md b/docs/guide/index.md index bfa753a0a7d6..6d81e9b01fbb 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -92,7 +92,7 @@ Test Files 1 passed (1) If you are using Bun as your package manager, make sure to use `bun run test` command instead of `bun test`, otherwise Bun will run its own test runner. ::: -Learn more about the usage of Vitest, see the [API](/api/) section. +Learn more about the usage of Vitest, see the [API](/api/test) section. ## Configuring Vitest diff --git a/docs/guide/lifecycle.md b/docs/guide/lifecycle.md index 90fc65630dbd..88c8dbdde3b8 100644 --- a/docs/guide/lifecycle.md +++ b/docs/guide/lifecycle.md @@ -131,8 +131,8 @@ The execution follows this order: - `beforeEach` hooks execute (in order defined, or based on [`sequence.hooks`](/config/sequence#sequence-hooks)) - Test function executes - `afterEach` hooks execute (reverse order by default with `sequence.hooks: 'stack'`) - - [`onTestFinished`](/api/#ontestfinished) callbacks run (always in reverse order) - - If test failed: [`onTestFailed`](/api/#ontestfailed) callbacks run + - [`onTestFinished`](/api/hooks#ontestfinished) callbacks run (always in reverse order) + - If test failed: [`onTestFailed`](/api/hooks#ontestfailed) callbacks run - Note: if `repeats` or `retry` are set, all of these steps are executed again 5. **`afterAll` hooks** - Run once after all tests in the suite complete @@ -317,4 +317,4 @@ For tips on how to improve performance, read the [Improving Performance](/guide/ - [Isolation Configuration](/config/isolate) - [Pool Configuration](/config/pool) - [Extending Reporters](/guide/advanced/reporters) - for reporter lifecycle events -- [Test API Reference](/api/) - for hook APIs and test functions +- [Test API Reference](/api/hooks) - for hook APIs diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 488b32fba757..d0c40059e39d 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -319,7 +319,7 @@ New pool architecture allows Vitest to simplify many previously complex configur - `maxThreads` and `maxForks` are now `maxWorkers`. - Environment variables `VITEST_MAX_THREADS` and `VITEST_MAX_FORKS` are now `VITEST_MAX_WORKERS`. -- `singleThread` and `singleFork` are now `maxWorkers: 1, isolate: false`. If your tests were relying on module reset between tests, you'll need to add [setupFile](/config/setupfiles) that calls [`vi.resetModules()`](/api/vi.html#vi-resetmodules) in [`beforeAll` test hook](/api/#beforeall). +- `singleThread` and `singleFork` are now `maxWorkers: 1, isolate: false`. If your tests were relying on module reset between tests, you'll need to add [setupFile](/config/setupfiles) that calls [`vi.resetModules()`](/api/vi.html#vi-resetmodules) in [`beforeAll` test hook](/api/hooks#beforeall). - `poolOptions` is removed. All previous `poolOptions` are now top-level options. The `memoryLimit` of VM pools is renamed to `vmMemoryLimit`. - `threads.useAtomics` is removed. If you have a use case for this, feel free to open a new feature request. - Custom pool interface has been rewritten, see [Custom Pool](/guide/advanced/pool#custom-pool) @@ -573,7 +573,7 @@ Just like Jest, Vitest sets `NODE_ENV` to `test`, if it wasn't set before. Vites ### Replace property -If you want to modify the object, you will use [replaceProperty API](https://jestjs.io/docs/jest-object#jestreplacepropertyobject-propertykey-value) in Jest, you can use [`vi.stubEnv`](/api/#vi-stubenv) or [`vi.spyOn`](/api/vi#vi-spyon) to do the same also in Vitest. +If you want to modify the object, you will use [replaceProperty API](https://jestjs.io/docs/jest-object#jestreplacepropertyobject-propertykey-value) in Jest, you can use [`vi.stubEnv`](/api/vi#vi-stubenv) or [`vi.spyOn`](/api/vi#vi-spyon) to do the same also in Vitest. ### Done Callback @@ -583,7 +583,7 @@ Vitest does not support the callback style of declaring tests. You can rewrite t ### Hooks -`beforeAll`/`beforeEach` hooks may return [teardown function](/api/#setup-and-teardown) in Vitest. Because of that you may need to rewrite your hooks declarations, if they return something other than `undefined` or `null`: +`beforeAll`/`beforeEach` hooks may return [teardown function](/api/hooks#beforeach) in Vitest. Because of that you may need to rewrite your hooks declarations, if they return something other than `undefined` or `null`: ```ts beforeEach(() => setActivePinia(createTestingPinia())) // [!code --] diff --git a/docs/guide/parallelism.md b/docs/guide/parallelism.md index 20b47b0e6471..ce866bf61eb8 100644 --- a/docs/guide/parallelism.md +++ b/docs/guide/parallelism.md @@ -20,7 +20,7 @@ If you have a lot of tests, it is usually faster to run them in parallel, but it Unlike _test files_, Vitest runs _tests_ in sequence. This means that tests inside a single test file will run in the order they are defined. -Vitest supports the [`concurrent`](/api/#test-concurrent) option to run tests together. If this option is set, Vitest will group concurrent tests in the same _file_ (the number of simultaneously running tests depends on the [`maxConcurrency`](/config/#maxconcurrency) option) and run them with [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). +Vitest supports the [`concurrent`](/api/test#test-concurrent) option to run tests together. If this option is set, Vitest will group concurrent tests in the same _file_ (the number of simultaneously running tests depends on the [`maxConcurrency`](/config/#maxconcurrency) option) and run them with [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). Vitest doesn't perform any smart analysis and doesn't create additional workers to run these tests. This means that the performance of your tests will improve only if you rely heavily on asynchronous operations. For example, these tests will still run one after another even though the `concurrent` option is specified. This is because they are synchronous: diff --git a/docs/guide/test-context.md b/docs/guide/test-context.md index ad125dae5500..d19da1feabdf 100644 --- a/docs/guide/test-context.md +++ b/docs/guide/test-context.md @@ -119,11 +119,11 @@ it('stop request when test times out', async ({ signal }) => { #### `onTestFailed` -The [`onTestFailed`](/api/#ontestfailed) hook bound to the current test. This API is useful if you are running tests concurrently and need to have a special handling only for this specific test. +The [`onTestFailed`](/api/hooks#ontestfailed) hook bound to the current test. This API is useful if you are running tests concurrently and need to have a special handling only for this specific test. #### `onTestFinished` -The [`onTestFinished`](/api/#ontestfailed) hook bound to the current test. This API is useful if you are running tests concurrently and need to have a special handling only for this specific test. +The [`onTestFinished`](/api/hooks#ontestfailed) hook bound to the current test. This API is useful if you are running tests concurrently and need to have a special handling only for this specific test. ## Extend Test Context diff --git a/netlify.toml b/netlify.toml index b8c4d1cc14b3..041e1799a5bc 100755 --- a/netlify.toml +++ b/netlify.toml @@ -85,6 +85,11 @@ from = "/guide/browser/webdriverio" to = "/config/browser/webdriverio" status = 301 +[[redirects]] +from = "/api/" +to = "/api/test" +status = 301 + [[headers]] for = "/manifest.webmanifest" diff --git a/packages/runner/src/errors.ts b/packages/runner/src/errors.ts index 53d2f551c2f5..66e93c020b60 100644 --- a/packages/runner/src/errors.ts +++ b/packages/runner/src/errors.ts @@ -19,3 +19,15 @@ export class TestRunAbortError extends Error { this.reason = reason } } + +export class AroundHookSetupError extends Error { + public name = 'AroundHookSetupError' +} + +export class AroundHookTeardownError extends Error { + public name = 'AroundHookTeardownError' +} + +export class AroundHookMultipleCallsError extends Error { + public name = 'AroundHookMultipleCallsError' +} diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index 323a053191e7..6192673f973b 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -132,6 +132,34 @@ export async function callFixtureCleanup(context: object): Promise { cleanupFnArrayMap.delete(context) } +/** + * Returns the current number of cleanup functions registered for the context. + * This can be used as a checkpoint to later clean up only fixtures added after this point. + */ +export function getFixtureCleanupCount(context: object): number { + return cleanupFnArrayMap.get(context)?.length ?? 0 +} + +/** + * Cleans up only fixtures that were added after the given checkpoint index. + * This is used by aroundEach to clean up fixtures created inside runTest() + * while preserving fixtures that were created for aroundEach itself. + */ +export async function callFixtureCleanupFrom(context: object, fromIndex: number): Promise { + const cleanupFnArray = cleanupFnArrayMap.get(context) + if (!cleanupFnArray || cleanupFnArray.length <= fromIndex) { + return + } + // Get items added after the checkpoint + const toCleanup = cleanupFnArray.slice(fromIndex) + // Clean up in reverse order + for (const cleanup of toCleanup.reverse()) { + await cleanup() + } + // Remove cleaned up items from the array, keeping items before checkpoint + cleanupFnArray.length = fromIndex +} + export function withFixtures(runner: VitestRunner, fn: Function, testContext?: TestContext) { return (hookContext?: TestContext): any => { const context: (TestContext & { [key: string]: any }) | undefined diff --git a/packages/runner/src/hooks.ts b/packages/runner/src/hooks.ts index 1144981233b0..dad7c9c9170a 100644 --- a/packages/runner/src/hooks.ts +++ b/packages/runner/src/hooks.ts @@ -1,6 +1,9 @@ +import type { VitestRunner } from './types' import type { AfterAllListener, AfterEachListener, + AroundAllListener, + AroundEachListener, BeforeAllListener, BeforeEachListener, OnTestFailedHandler, @@ -21,6 +24,8 @@ function getDefaultHookTimeout() { const CLEANUP_TIMEOUT_KEY = Symbol.for('VITEST_CLEANUP_TIMEOUT') const CLEANUP_STACK_TRACE_KEY = Symbol.for('VITEST_CLEANUP_STACK_TRACE') +const AROUND_TIMEOUT_KEY = Symbol.for('VITEST_AROUND_TIMEOUT') +const AROUND_STACK_TRACE_KEY = Symbol.for('VITEST_AROUND_STACK_TRACE') export function getBeforeHookCleanupCallback(hook: Function, result: any, context?: TestContext): Function | undefined { if (typeof result === 'function') { @@ -266,6 +271,144 @@ export const onTestFinished: TaskHook = createTestHook( }, ) +/** + * Registers a callback function that wraps around all tests within the current suite. + * The callback receives a `runSuite` function that must be called to run the suite's tests. + * This hook is useful for scenarios where you need to wrap an entire suite in a context + * (e.g., starting a server, opening a database connection that all tests share). + * + * **Note:** When multiple `aroundAll` hooks are registered, they are nested inside each other. + * The first registered hook is the outermost wrapper. + * + * **Note:** Unlike `aroundEach`, the `aroundAll` hook does not receive test context or support fixtures, + * as it runs at the suite level before any individual test context is created. + * + * @param {Function} fn - The callback function that wraps the suite. Must call `runSuite()` to run the tests. + * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used. + * @returns {void} + * @example + * ```ts + * // Example of using aroundAll to wrap suite in a tracing span + * aroundAll(async (runSuite) => { + * await tracer.trace('test-suite', runSuite); + * }); + * ``` + * @example + * ```ts + * // Example of using aroundAll with AsyncLocalStorage context + * aroundAll(async (runSuite) => { + * await asyncLocalStorage.run({ suiteId: 'my-suite' }, runSuite); + * }); + * ``` + */ +export function aroundAll( + fn: AroundAllListener, + timeout?: number, +): void { + assertTypes(fn, '"aroundAll" callback', ['function']) + const stackTraceError = new Error('STACK_TRACE_ERROR') + const resolvedTimeout = timeout ?? getDefaultHookTimeout() + + return getCurrentSuite().on( + 'aroundAll', + Object.assign(fn, { + [AROUND_TIMEOUT_KEY]: resolvedTimeout, + [AROUND_STACK_TRACE_KEY]: stackTraceError, + }), + ) +} + +/** + * Registers a callback function that wraps around each test within the current suite. + * The callback receives a `runTest` function that must be called to run the test. + * This hook is useful for scenarios where you need to wrap tests in a context (e.g., database transactions). + * + * **Note:** When multiple `aroundEach` hooks are registered, they are nested inside each other. + * The first registered hook is the outermost wrapper. + * + * @param {Function} fn - The callback function that wraps the test. Must call `runTest()` to run the test. + * @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used. + * @returns {void} + * @example + * ```ts + * // Example of using aroundEach to wrap tests in a database transaction + * aroundEach(async (runTest) => { + * await database.transaction(() => runTest()); + * }); + * ``` + * @example + * ```ts + * // Example of using aroundEach with fixtures + * aroundEach(async (runTest, { db }) => { + * await db.transaction(() => runTest()); + * }); + * ``` + */ +export function aroundEach( + fn: AroundEachListener, + timeout?: number, +): void { + assertTypes(fn, '"aroundEach" callback', ['function']) + const stackTraceError = new Error('STACK_TRACE_ERROR') + const resolvedTimeout = timeout ?? getDefaultHookTimeout() + const runner = getRunner() + + // Create a wrapper function that supports fixtures in the second argument (context) + // withFixtures resolves fixtures into context, then we call fn with all 3 args + const wrappedFn: AroundEachListener = withAroundEachFixtures(runner, fn) + + // Store timeout and stack trace on the function for use in callAroundEachHooks + // Setup and teardown phases will each have their own timeout + return getCurrentSuite().on( + 'aroundEach', + Object.assign(wrappedFn, { + [AROUND_TIMEOUT_KEY]: resolvedTimeout, + [AROUND_STACK_TRACE_KEY]: stackTraceError, + }), + ) +} + +/** + * Wraps an aroundEach listener to support fixtures. + * Similar to withFixtures, but handles the aroundEach signature where: + * - First arg is runTest function + * - Second arg is context (where fixtures are destructured from) + * - Third arg is suite + */ +function withAroundEachFixtures( + runner: VitestRunner, + fn: AroundEachListener, +): AroundEachListener { + // Create the wrapper that will be returned + const wrapper: AroundEachListener = (runTest, context, suite) => { + // Create inner function that will be passed to withFixtures + // This function receives context (with fixtures resolved) and calls original fn + const innerFn = (ctx: any) => fn(runTest, ctx, suite) + // Set fixture index to 1 to tell parser to look at second arg of original fn + // Set toString to return original fn string so parser extracts correct params + ;(innerFn as any).__VITEST_FIXTURE_INDEX__ = 1 + ;(innerFn as any).toString = () => fn.toString() + + // Use withFixtures to resolve fixtures, passing context as the hook context + const fixtureResolver = withFixtures(runner, innerFn) + return fixtureResolver(context) + } + + return wrapper +} + +export function getAroundHookTimeout(hook: Function): number { + return AROUND_TIMEOUT_KEY in hook && typeof hook[AROUND_TIMEOUT_KEY] === 'number' + ? hook[AROUND_TIMEOUT_KEY] + : getDefaultHookTimeout() +} + +export function getAroundHookStackTrace(hook: Function): Error | undefined { + return AROUND_STACK_TRACE_KEY in hook && hook[AROUND_STACK_TRACE_KEY] instanceof Error + ? hook[AROUND_STACK_TRACE_KEY] + : undefined +} + function createTestHook( name: string, handler: (test: TaskPopulated, handler: T, timeout?: number) => void, diff --git a/packages/runner/src/index.ts b/packages/runner/src/index.ts index 39a91ee3ec69..0e3e143468bb 100644 --- a/packages/runner/src/index.ts +++ b/packages/runner/src/index.ts @@ -2,6 +2,8 @@ export { recordArtifact } from './artifact' export { afterAll, afterEach, + aroundAll, + aroundEach, beforeAll, beforeEach, onTestFailed, diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 61f62f77e97f..7b1f1bf7240e 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -2,6 +2,8 @@ import type { Awaitable, TestError } from '@vitest/utils' import type { DiffOptions } from '@vitest/utils/diff' import type { FileSpecification, VitestRunner } from './types/runner' import type { + AroundAllListener, + AroundEachListener, File, SequenceHooks, Suite, @@ -21,9 +23,9 @@ import { shuffle } from '@vitest/utils/helpers' import { getSafeTimers } from '@vitest/utils/timers' import { collectTests } from './collect' import { abortContextSignal, getFileContext } from './context' -import { PendingError, TestRunAbortError } from './errors' -import { callFixtureCleanup } from './fixture' -import { getBeforeHookCleanupCallback } from './hooks' +import { AroundHookMultipleCallsError, AroundHookSetupError, AroundHookTeardownError, PendingError, TestRunAbortError } from './errors' +import { callFixtureCleanup, callFixtureCleanupFrom, getFixtureCleanupCount } from './fixture' +import { getAroundHookStackTrace, getAroundHookTimeout, getBeforeHookCleanupCallback } from './hooks' import { getFn, getHooks } from './map' import { addRunningTest, getRunningTests, setCurrentTest } from './test-state' import { limitConcurrency } from './utils/limit-concurrency' @@ -217,6 +219,224 @@ export async function callSuiteHook( return callbacks } +function getAroundEachHooks(suite: Suite): AroundEachListener[] { + const hooks: AroundEachListener[] = [] + const parentSuite: Suite | null = 'filepath' in suite ? null : suite.suite || suite.file + if (parentSuite) { + hooks.push(...getAroundEachHooks(parentSuite)) + } + hooks.push(...getHooks(suite).aroundEach) + return hooks +} + +function getAroundAllHooks(suite: Suite): AroundAllListener[] { + return getHooks(suite).aroundAll +} + +interface AroundHooksOptions { + hooks: THook[] + hookName: 'aroundEach' | 'aroundAll' + callbackName: 'runTest()' | 'runSuite()' + onTimeout?: (error: Error) => void + invokeHook: (hook: THook, use: () => Promise) => Awaitable +} + +function makeAroundHookTimeoutError( + hookName: string, + phase: 'setup' | 'teardown', + timeout: number, + stackTraceError?: Error, +) { + const message = `The ${phase} phase of "${hookName}" hook timed out after ${timeout}ms.` + const ErrorClass = phase === 'setup' ? AroundHookSetupError : AroundHookTeardownError + const error = new ErrorClass(message) + if (stackTraceError?.stack) { + error.stack = stackTraceError.stack.replace(stackTraceError.message, error.message) + } + return error +} + +async function callAroundHooks( + runInner: () => Promise, + options: AroundHooksOptions, +): Promise { + const { hooks, hookName, callbackName, onTimeout, invokeHook } = options + + if (!hooks.length) { + await runInner() + return + } + + const createTimeoutPromise = ( + timeout: number, + phase: 'setup' | 'teardown', + stackTraceError: Error | undefined, + ): { promise: Promise; clear: () => void } => { + let timer: ReturnType | undefined + + const promise = new Promise((_, reject) => { + if (timeout > 0 && timeout !== Number.POSITIVE_INFINITY) { + timer = setTimeout(() => { + const error = makeAroundHookTimeoutError(hookName, phase, timeout, stackTraceError) + onTimeout?.(error) + reject(error) + }, timeout) + timer.unref?.() + } + }) + + const clear = () => { + if (timer) { + clearTimeout(timer) + timer = undefined + } + } + + return { promise, clear } + } + + const runNextHook = async (index: number): Promise => { + if (index >= hooks.length) { + return runInner() + } + + const hook = hooks[index] + const timeout = getAroundHookTimeout(hook) + const stackTraceError = getAroundHookStackTrace(hook) + + let useCalled = false + let setupTimeout: { promise: Promise; clear: () => void } + let teardownTimeout: { promise: Promise; clear: () => void } | undefined + + // Promise that resolves when use() is called (setup phase complete) + let resolveUseCalled!: () => void + const useCalledPromise = new Promise((resolve) => { + resolveUseCalled = resolve + }) + + // Promise that resolves when use() returns (inner hooks complete, teardown phase starts) + let resolveUseReturned!: () => void + const useReturnedPromise = new Promise((resolve) => { + resolveUseReturned = resolve + }) + + // Promise that resolves when hook completes + let resolveHookComplete!: () => void + let rejectHookComplete!: (error: Error) => void + const hookCompletePromise = new Promise((resolve, reject) => { + resolveHookComplete = resolve + rejectHookComplete = reject + }) + + const use = async () => { + if (useCalled) { + throw new AroundHookMultipleCallsError( + `The \`${callbackName}\` callback was called multiple times in the \`${hookName}\` hook. ` + + `The callback can only be called once per hook.`, + ) + } + useCalled = true + resolveUseCalled() + + // Setup phase completed - clear setup timer + setupTimeout.clear() + + // Run inner hooks - don't time this against our teardown timeout + await runNextHook(index + 1) + + // Start teardown timer after inner hooks complete - only times this hook's teardown code + teardownTimeout = createTimeoutPromise(timeout, 'teardown', stackTraceError) + + // Signal that use() is returning (teardown phase starting) + resolveUseReturned() + } + + // Start setup timeout + setupTimeout = createTimeoutPromise(timeout, 'setup', stackTraceError) + + // Run the hook in the background + ;(async () => { + try { + await invokeHook(hook, use) + if (!useCalled) { + throw new AroundHookSetupError( + `The \`${callbackName}\` callback was not called in the \`${hookName}\` hook. ` + + `Make sure to call \`${callbackName}\` to run the ${hookName === 'aroundEach' ? 'test' : 'suite'}.`, + ) + } + resolveHookComplete() + } + catch (error) { + rejectHookComplete(error as Error) + } + })() + + // Wait for either: use() to be called OR hook to complete (error) OR setup timeout + try { + await Promise.race([ + useCalledPromise, + hookCompletePromise, + setupTimeout.promise, + ]) + } + finally { + setupTimeout.clear() + } + + // Wait for use() to return (inner hooks complete) OR hook to complete (error during inner hooks) + await Promise.race([ + useReturnedPromise, + hookCompletePromise, + ]) + + // Now teardownTimeout is guaranteed to be set + // Wait for hook to complete (teardown) OR teardown timeout + try { + await Promise.race([ + hookCompletePromise, + teardownTimeout!.promise, + ]) + } + finally { + teardownTimeout!.clear() + } + } + + await runNextHook(0) +} + +async function callAroundAllHooks( + suite: Suite, + runSuiteInner: () => Promise, +): Promise { + await callAroundHooks(runSuiteInner, { + hooks: getAroundAllHooks(suite), + hookName: 'aroundAll', + callbackName: 'runSuite()', + invokeHook: (hook, use) => hook(use, suite), + }) +} + +async function callAroundEachHooks( + suite: Suite, + test: Test, + runTest: (fixtureCheckpoint: number) => Promise, +): Promise { + await callAroundHooks( + // Take checkpoint right before runTest - at this point all aroundEach fixtures + // have been resolved, so we can correctly identify which fixtures belong to + // aroundEach (before checkpoint) vs inside runTest (after checkpoint) + () => runTest(getFixtureCleanupCount(test.context)), + { + hooks: getAroundEachHooks(suite), + hookName: 'aroundEach', + callbackName: 'runTest()', + onTimeout: error => abortContextSignal(test.context, error), + invokeHook: (hook, use) => hook(use, test.context, suite), + }, + ) +} + const packs = new Map() const eventsPacks: [string, TaskUpdateEvent, undefined][] = [] const pendingTasksUpdates: Promise[] = [] @@ -365,95 +585,114 @@ export async function runTest(test: Test, runner: VitestRunner): Promise { const retry = getRetryCount(test.retry) for (let retryCount = 0; retryCount <= retry; retryCount++) { let beforeEachCleanups: unknown[] = [] - try { - await runner.onBeforeTryTask?.(test, { - retry: retryCount, - repeats: repeatCount, - }) + // fixtureCheckpoint is passed by callAroundEachHooks - it represents the count + // of fixture cleanup functions AFTER all aroundEach fixtures have been resolved + // but BEFORE the test runs. This allows us to clean up only fixtures created + // inside runTest while preserving aroundEach fixtures for teardown. + await callAroundEachHooks(suite, test, async (fixtureCheckpoint) => { + try { + await runner.onBeforeTryTask?.(test, { + retry: retryCount, + repeats: repeatCount, + }) + + test.result!.repeatCount = repeatCount + + beforeEachCleanups = await $('test.beforeEach', () => callSuiteHook( + suite, + test, + 'beforeEach', + runner, + [test.context, suite], + )) + + if (runner.runTask) { + await $('test.callback', () => runner.runTask!(test)) + } + else { + const fn = getFn(test) + if (!fn) { + throw new Error( + 'Test function is not found. Did you add it using `setFn`?', + ) + } + await $('test.callback', () => fn()) + } - test.result.repeatCount = repeatCount + await runner.onAfterTryTask?.(test, { + retry: retryCount, + repeats: repeatCount, + }) - beforeEachCleanups = await $('test.beforeEach', () => callSuiteHook( - suite, - test, - 'beforeEach', - runner, - [test.context, suite], - )) + if (test.result!.state !== 'fail') { + if (!test.repeats) { + test.result!.state = 'pass' + } + else if (test.repeats && retry === retryCount) { + test.result!.state = 'pass' + } + } + } + catch (e) { + failTask(test.result!, e, runner.config.diffOptions) + } - if (runner.runTask) { - await $('test.callback', () => runner.runTask!(test)) + try { + await runner.onTaskFinished?.(test) + } + catch (e) { + failTask(test.result!, e, runner.config.diffOptions) } - else { - const fn = getFn(test) - if (!fn) { - throw new Error( - 'Test function is not found. Did you add it using `setFn`?', - ) + + try { + await $('test.afterEach', () => callSuiteHook(suite, test, 'afterEach', runner, [ + test.context, + suite, + ])) + if (beforeEachCleanups.length) { + await $('test.cleanup', () => callCleanupHooks(runner, beforeEachCleanups)) } - await $('test.callback', () => fn()) + // Only clean up fixtures created inside runTest (after the checkpoint) + // Fixtures created for aroundEach will be cleaned up after aroundEach teardown + await callFixtureCleanupFrom(test.context, fixtureCheckpoint) + } + catch (e) { + failTask(test.result!, e, runner.config.diffOptions) } - await runner.onAfterTryTask?.(test, { - retry: retryCount, - repeats: repeatCount, - }) + if (test.onFinished?.length) { + await $('test.onFinished', () => callTestHooks(runner, test, test.onFinished!, 'stack')) + } - if (test.result.state !== 'fail') { - if (!test.repeats) { - test.result.state = 'pass' - } - else if (test.repeats && retry === retryCount) { - test.result.state = 'pass' - } + if (test.result!.state === 'fail' && test.onFailed?.length) { + await $('test.onFailed', () => callTestHooks( + runner, + test, + test.onFailed!, + runner.config.sequence.hooks, + )) } - } - catch (e) { - failTask(test.result, e, runner.config.diffOptions) - } - try { - await runner.onTaskFinished?.(test) - } - catch (e) { - failTask(test.result, e, runner.config.diffOptions) - } + test.onFailed = undefined + test.onFinished = undefined + + await runner.onAfterRetryTask?.(test, { + retry: retryCount, + repeats: repeatCount, + }) + }).catch((error) => { + failTask(test.result!, error, runner.config.diffOptions) + }) + // Clean up fixtures that were created for aroundEach (before the checkpoint) + // This runs after aroundEach teardown has completed try { - await $('test.afterEach', () => callSuiteHook(suite, test, 'afterEach', runner, [ - test.context, - suite, - ])) - if (beforeEachCleanups.length) { - await $('test.cleanup', () => callCleanupHooks(runner, beforeEachCleanups)) - } await callFixtureCleanup(test.context) } catch (e) { - failTask(test.result, e, runner.config.diffOptions) - } - - if (test.onFinished?.length) { - await $('test.onFinished', () => callTestHooks(runner, test, test.onFinished!, 'stack')) - } - - if (test.result.state === 'fail' && test.onFailed?.length) { - await $('test.onFailed', () => callTestHooks( - runner, - test, - test.onFailed!, - runner.config.sequence.hooks, - )) + failTask(test.result!, e, runner.config.diffOptions) } - test.onFailed = undefined - test.onFinished = undefined - - await runner.onAfterRetryTask?.(test, { - retry: retryCount, - repeats: repeatCount, - }) - // skipped with new PendingError if (test.result?.pending || test.result?.state === 'skip') { test.mode = 'skip' @@ -580,66 +819,87 @@ export async function runSuite(suite: Suite, runner: VitestRunner): Promise callSuiteHook( - suite, - suite, - 'beforeAll', - runner, - [suite], - )) - } - catch (e) { - markTasksAsSkipped(suite, runner) - throw e - } + await callAroundAllHooks(suite, async () => { + suiteRan = true + try { + // beforeAll + try { + beforeAllCleanups = await $('suite.beforeAll', () => callSuiteHook( + suite, + suite, + 'beforeAll', + runner, + [suite], + )) + } + catch (e) { + beforeAllError = e + failTask(suite.result!, beforeAllError, runner.config.diffOptions) + markTasksAsSkipped(suite, runner) + throw e + } - if (runner.runSuite) { - await runner.runSuite(suite) - } - else { - for (let tasksGroup of partitionSuiteChildren(suite)) { - if (tasksGroup[0].concurrent === true) { - await Promise.all(tasksGroup.map(c => runSuiteChild(c, runner))) + // run suite children + if (runner.runSuite) { + await runner.runSuite(suite) } else { - const { sequence } = runner.config - if (suite.shuffle) { - // run describe block independently from tests - const suites = tasksGroup.filter( - group => group.type === 'suite', - ) - const tests = tasksGroup.filter(group => group.type === 'test') - const groups = shuffle([suites, tests], sequence.seed) - tasksGroup = groups.flatMap(group => - shuffle(group, sequence.seed), - ) + for (let tasksGroup of partitionSuiteChildren(suite)) { + if (tasksGroup[0].concurrent === true) { + await Promise.all(tasksGroup.map(c => runSuiteChild(c, runner))) + } + else { + const { sequence } = runner.config + if (suite.shuffle) { + // run describe block independently from tests + const suites = tasksGroup.filter( + group => group.type === 'suite', + ) + const tests = tasksGroup.filter(group => group.type === 'test') + const groups = shuffle([suites, tests], sequence.seed) + tasksGroup = groups.flatMap(group => + shuffle(group, sequence.seed), + ) + } + for (const c of tasksGroup) { + await runSuiteChild(c, runner) + } + } } - for (const c of tasksGroup) { - await runSuiteChild(c, runner) + } + } + finally { + // afterAll runs even if beforeAll or suite children fail + try { + await $('suite.afterAll', () => callSuiteHook(suite, suite, 'afterAll', runner, [suite])) + if (beforeAllCleanups.length) { + await $('suite.cleanup', () => callCleanupHooks(runner, beforeAllCleanups)) + } + if (suite.file === suite) { + const context = getFileContext(suite as File) + await callFixtureCleanup(context) } } + catch (e) { + failTask(suite.result!, e, runner.config.diffOptions) + } } - } + }) } catch (e) { - failTask(suite.result, e, runner.config.diffOptions) - } - - try { - await $('suite.afterAll', () => callSuiteHook(suite, suite, 'afterAll', runner, [suite])) - if (beforeAllCleanups.length) { - await $('suite.cleanup', () => callCleanupHooks(runner, beforeAllCleanups)) + // mark tasks as skipped if aroundAll failed before the suite callback was executed + if (!suiteRan) { + markTasksAsSkipped(suite, runner) } - if (suite.file === suite) { - const context = getFileContext(suite as File) - await callFixtureCleanup(context) + // don't push beforeAll error again - it was already pushed to preserve the order of beforeAll/afterAll + if (e !== beforeAllError) { + failTask(suite.result!, e, runner.config.diffOptions) } } - catch (e) { - failTask(suite.result, e, runner.config.diffOptions) - } if (suite.mode === 'run' || suite.mode === 'queued') { if (!runner.config.passWithNoTests && !hasTests(suite)) { diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 0626b8690f8e..911479b88f19 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -35,7 +35,7 @@ import { withTimeout, } from './context' import { mergeContextFixtures, mergeScopedFixtures, withFixtures } from './fixture' -import { afterAll, afterEach, beforeAll, beforeEach } from './hooks' +import { afterAll, afterEach, aroundAll, aroundEach, beforeAll, beforeEach } from './hooks' import { getHooks, setFn, setHooks, setTestFixture } from './map' import { getCurrentTest } from './test-state' import { findTestFileStackTrace } from './utils' @@ -254,6 +254,8 @@ export function createSuiteHooks(): SuiteHooks { afterAll: [], beforeEach: [], afterEach: [], + aroundEach: [], + aroundAll: [], } } @@ -870,10 +872,13 @@ export function createTaskCollector( }, _context) } + taskFn.describe = suite taskFn.beforeEach = beforeEach taskFn.afterEach = afterEach taskFn.beforeAll = beforeAll taskFn.afterAll = afterAll + taskFn.aroundEach = aroundEach + taskFn.aroundAll = aroundAll const _test = createChainable( ['concurrent', 'sequential', 'skip', 'only', 'todo', 'fails'], diff --git a/packages/runner/src/types.ts b/packages/runner/src/types.ts index 659a4efd9329..304023fa2a13 100644 --- a/packages/runner/src/types.ts +++ b/packages/runner/src/types.ts @@ -10,6 +10,8 @@ export type { export type { AfterAllListener, AfterEachListener, + AroundAllListener, + AroundEachListener, BeforeAllListener, BeforeEachListener, File, @@ -32,6 +34,7 @@ export type { SuiteCollector, SuiteFactory, SuiteHooks, + SuiteOptions, Task, TaskBase, TaskCustomOptions, diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index df22a30ad810..948e358c68ba 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -1,6 +1,6 @@ import type { Awaitable, TestError } from '@vitest/utils' import type { FixtureItem } from '../fixture' -import type { afterAll, afterEach, beforeAll, beforeEach } from '../hooks' +import type { afterAll, afterEach, aroundAll, aroundEach, beforeAll, beforeEach } from '../hooks' import type { ChainableFunction } from '../utils/chain' export type RunMode = 'run' | 'skip' | 'only' | 'todo' | 'queued' @@ -591,6 +591,8 @@ interface Hooks { afterAll: typeof afterAll beforeEach: typeof beforeEach afterEach: typeof afterEach + aroundEach: typeof aroundEach + aroundAll: typeof aroundAll } export type TestAPI = ChainableTestAPI @@ -607,6 +609,7 @@ export type TestAPI = ChainableTestAPI scoped: ( fixtures: Partial>, ) => void + describe: SuiteAPI } export interface FixtureOptions { @@ -703,11 +706,28 @@ export interface AfterEachListener { ): Awaitable } +export interface AroundEachListener { + ( + runTest: () => Promise, + context: TestContext & ExtraContext, + suite: Readonly + ): Awaitable +} + +export interface AroundAllListener { + ( + runSuite: () => Promise, + suite: Readonly + ): Awaitable +} + export interface SuiteHooks { beforeAll: BeforeAllListener[] afterAll: AfterAllListener[] beforeEach: BeforeEachListener[] afterEach: AfterEachListener[] + aroundEach: AroundEachListener[] + aroundAll: AroundAllListener[] } export interface TaskCustomOptions extends TestOptions { diff --git a/packages/vitest/globals.d.ts b/packages/vitest/globals.d.ts index c55256aebd68..a654eaa12b1a 100644 --- a/packages/vitest/globals.d.ts +++ b/packages/vitest/globals.d.ts @@ -14,6 +14,8 @@ declare global { let afterAll: typeof import('vitest')['afterAll'] let beforeEach: typeof import('vitest')['beforeEach'] let afterEach: typeof import('vitest')['afterEach'] + let aroundEach: typeof import('vitest')['aroundEach'] + let aroundAll: typeof import('vitest')['aroundAll'] let onTestFailed: typeof import('vitest')['onTestFailed'] let onTestFinished: typeof import('vitest')['onTestFinished'] } diff --git a/packages/vitest/src/constants.ts b/packages/vitest/src/constants.ts index dcb053b63461..2124ef6411e0 100644 --- a/packages/vitest/src/constants.ts +++ b/packages/vitest/src/constants.ts @@ -36,4 +36,6 @@ export const globalApis: string[] = [ 'afterEach', 'onTestFinished', 'onTestFailed', + 'aroundEach', + 'aroundAll', ] diff --git a/packages/vitest/src/public/index.ts b/packages/vitest/src/public/index.ts index 466ef4af614c..09aab798d971 100644 --- a/packages/vitest/src/public/index.ts +++ b/packages/vitest/src/public/index.ts @@ -100,6 +100,8 @@ export type { export { afterAll, afterEach, + aroundAll, + aroundEach, beforeAll, beforeEach, describe, @@ -126,6 +128,7 @@ export type { SuiteAPI, SuiteCollector, SuiteFactory, + SuiteOptions, TaskCustomOptions, TaskMeta, TaskState, diff --git a/test/cli/test/around-each.test.ts b/test/cli/test/around-each.test.ts new file mode 100644 index 000000000000..810a6a9b5646 --- /dev/null +++ b/test/cli/test/around-each.test.ts @@ -0,0 +1,1894 @@ +import { expect, test } from 'vitest' +import { runInlineTests } from '../../test-utils' + +function extractLogs(stdout: string): string { + return stdout.split('\n').filter(l => l.includes('>>')).join('\n') +} + +test('basic aroundEach wraps the test', async () => { + const { stdout, stderr } = await runInlineTests({ + 'basic.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> before test') + await runTest() + console.log('>> after test') + }) + + test('test 1', () => { + console.log('>> inside test') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> before test + >> inside test + >> after test" + `) +}) + +test('multiple aroundEach hooks are nested (first is outermost)', async () => { + const { stdout, stderr } = await runInlineTests({ + 'nested-hooks.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> outer before') + await runTest() + console.log('>> outer after') + }) + + aroundEach(async (runTest) => { + console.log('>> inner before') + await runTest() + console.log('>> inner after') + }) + + test('test 1', () => { + console.log('>> test') + }) + `, + }) + + expect(stderr).toBe('') + + // Extract log lines + const logs = stdout.split('\n').filter(line => line.startsWith('>> ')).map(l => l.trim()) + expect(logs).toEqual([ + '>> outer before', + '>> inner before', + '>> test', + '>> inner after', + '>> outer after', + ]) +}) + +test('aroundEach in nested suites wraps correctly', async () => { + const { stdout, stderr } = await runInlineTests({ + 'nested-suites.test.ts': ` + import { aroundEach, describe, test } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> root before') + await runTest() + console.log('>> root after') + }) + + describe('suite 1', () => { + aroundEach(async (runTest) => { + console.log('>> suite1 before') + await runTest() + console.log('>> suite1 after') + }) + + test('test in suite 1', () => { + console.log('>> test suite1') + }) + + describe('nested suite', () => { + aroundEach(async (runTest) => { + console.log('>> nested before') + await runTest() + console.log('>> nested after') + }) + + test('test in nested suite', () => { + console.log('>> test nested') + }) + }) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> root before + >> suite1 before + >> test suite1 + >> suite1 after + >> root after + >> root before + >> suite1 before + >> nested before + >> test nested + >> nested after + >> suite1 after + >> root after" + `) +}) + +test('throws error when runTest is called multiple times', async () => { + const { stderr } = await runInlineTests({ + 'multiple-calls.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + await runTest() + await runTest() // second call should throw + }) + + test('test 1', () => { + console.log('>> test ran') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL multiple-calls.test.ts > test 1 + AroundHookMultipleCallsError: The \`runTest()\` callback was called multiple times in the \`aroundEach\` hook. The callback can only be called once per hook. + ❯ multiple-calls.test.ts:6:15 + 4| aroundEach(async (runTest) => { + 5| await runTest() + 6| await runTest() // second call should throw + | ^ + 7| }) + 8| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) +}) + +test('throws error when runTest is not called', async () => { + const { stderr } = await runInlineTests({ + 'no-runtest.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (_runTest) => { + console.log('>> aroundEach without calling runTest') + // Not calling runTest() + }) + + test('test 1', () => { + console.log('>> test should not run') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL no-runtest.test.ts > test 1 + AroundHookSetupError: The \`runTest()\` callback was not called in the \`aroundEach\` hook. Make sure to call \`runTest()\` to run the test. + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) +}) + +test('aroundEach with async operations', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'async.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> setup start') + await new Promise(r => setTimeout(r, 10)) + console.log('>> setup done') + await runTest() + console.log('>> cleanup start') + await new Promise(r => setTimeout(r, 10)) + console.log('>> cleanup done') + }) + + test('async test', async () => { + console.log('>> test running') + await new Promise(r => setTimeout(r, 10)) + console.log('>> test done') + }) + `, + }) + + expect(stderr).toBe('') + expect(errorTree()).toMatchInlineSnapshot(` + { + "async.test.ts": { + "async test": "passed", + }, + } + `) + + const logs = stdout.split('\n').filter(line => line.startsWith('>> ')).map(l => l.trim()) + expect(logs).toEqual([ + '>> setup start', + '>> setup done', + '>> test running', + '>> test done', + '>> cleanup start', + '>> cleanup done', + ]) +}) + +test('aroundEach runs for each test', async () => { + const { stdout, stderr } = await runInlineTests({ + 'each-test.test.ts': ` + import { aroundEach, test } from 'vitest' + + let counter = 0 + + aroundEach(async (runTest) => { + counter++ + console.log('>> aroundEach run ' + counter) + await runTest() + }) + + test('test 1', () => { + console.log('>> test 1') + }) + + test('test 2', () => { + console.log('>> test 2') + }) + + test('test 3', () => { + console.log('>> test 3') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundEach run 1 + >> test 1 + >> aroundEach run 2 + >> test 2 + >> aroundEach run 3 + >> test 3" + `) +}) + +test('aroundEach with beforeEach and afterEach', async () => { + const { stdout, stderr } = await runInlineTests({ + 'with-hooks.test.ts': ` + import { aroundEach, beforeEach, afterEach, test } from 'vitest' + + beforeEach(() => { + console.log('>> beforeEach') + }) + + aroundEach(async (runTest) => { + console.log('>> aroundEach before') + await runTest() + console.log('>> aroundEach after') + }) + + afterEach(() => { + console.log('>> afterEach') + }) + + test('test 1', () => { + console.log('>> test') + }) + `, + }) + + expect(stderr).toBe('') + + // aroundEach should wrap around beforeEach/test/afterEach + const logs = stdout.split('\n').filter(line => line.startsWith('>> ')).map(l => l.trim()) + expect(logs).toEqual([ + '>> aroundEach before', + '>> beforeEach', + '>> test', + '>> afterEach', + '>> aroundEach after', + ]) +}) + +test('aroundEach receives test context', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'context.test.ts': ` + import { aroundEach, test, expect } from 'vitest' + + aroundEach(async (runTest, context) => { + console.log('>> test name:', context.task.name) + await runTest() + }) + + test('my test name', () => { + console.log('>> inside test') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> test name: my test name + >> inside test" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "context.test.ts": { + "my test name": "passed", + }, + } + `) +}) + +test('aroundEach cleanup runs even on test failure', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'test-failure.test.ts': ` + import { aroundEach, test, expect } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> setup') + await runTest() + console.log('>> cleanup (should run)') + }) + + test('failing test', () => { + console.log('>> test running') + expect(1).toBe(2) // This will fail + }) + `, + }) + + // Cleanup should still run even when test fails + expect(stderr).toContain('expected 1 to be 2') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> setup + >> test running + >> cleanup (should run)" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "test-failure.test.ts": { + "failing test": [ + "expected 1 to be 2 // Object.is equality", + ], + }, + } + `) +}) + +test('aroundEach error prevents test from running', async () => { + const { stdout, stderr } = await runInlineTests({ + 'hook-error.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> before error') + throw new Error('aroundEach error') + await runTest() // unreachable + }) + + test('test 1', () => { + console.log('>> test should not run') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL hook-error.test.ts > test 1 + Error: aroundEach error + ❯ hook-error.test.ts:6:15 + 4| aroundEach(async (runTest) => { + 5| console.log('>> before error') + 6| throw new Error('aroundEach error') + | ^ + 7| await runTest() // unreachable + 8| }) + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + expect(extractLogs(stdout)).toMatchInlineSnapshot(`">> before error"`) +}) + +test('aroundEach cleanup error is reported', async () => { + const { stdout, stderr } = await runInlineTests({ + 'cleanup-error.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> setup') + await runTest() + console.log('>> cleanup before error') + throw new Error('cleanup error') + }) + + test('test 1', () => { + console.log('>> test ran') + }) + `, + }) + + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> setup + >> test ran + >> cleanup before error" + `) + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL cleanup-error.test.ts > test 1 + Error: cleanup error + ❯ cleanup-error.test.ts:8:15 + 6| await runTest() + 7| console.log('>> cleanup before error') + 8| throw new Error('cleanup error') + | ^ + 9| }) + 10| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) +}) + +test('aroundEach with database transaction pattern', async () => { + const { stderr, errorTree } = await runInlineTests({ + 'transaction.test.ts': ` + import { aroundEach, test, expect } from 'vitest' + + // Simulating a database transaction pattern + const db = { + data: [] as string[], + inTransaction: false, + beginTransaction() { + this.inTransaction = true + console.log('>> BEGIN TRANSACTION') + }, + commit() { + this.inTransaction = false + console.log('>> COMMIT') + }, + rollback() { + this.data = [] + this.inTransaction = false + console.log('>> ROLLBACK') + }, + insert(value: string) { + if (!this.inTransaction) throw new Error('Not in transaction') + this.data.push(value) + console.log('>> INSERT:', value) + } + } + + aroundEach(async (runTest) => { + db.beginTransaction() + try { + await runTest() + } finally { + db.rollback() // Always rollback to keep tests isolated + } + }) + + test('insert data', () => { + db.insert('test1') + expect(db.data).toContain('test1') + }) + + test('data is rolled back', () => { + expect(db.data).toEqual([]) // Previous test's data was rolled back + db.insert('test2') + expect(db.data).toContain('test2') + }) + `, + }) + + expect(stderr).toBe('') + expect(errorTree()).toMatchInlineSnapshot(` + { + "transaction.test.ts": { + "data is rolled back": "passed", + "insert data": "passed", + }, + } + `) +}) + +test('aroundEach with globals: true', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'globals.test.ts': ` + aroundEach(async (runTest) => { + console.log('>> aroundEach global') + await runTest() + console.log('>> aroundEach global done') + }) + + test('test with globals', () => { + console.log('>> test') + }) + `, + }, { globals: true }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundEach global + >> test + >> aroundEach global done" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "globals.test.ts": { + "test with globals": "passed", + }, + } + `) +}) + +test('aroundEach with test.each', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'test-each.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest, context) => { + console.log('>> aroundEach for:', context.task.name) + await runTest() + }) + + test.each([1, 2, 3])('test %i', (num) => { + console.log('>> test value:', num) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundEach for: test 1 + >> test value: 1 + >> aroundEach for: test 2 + >> test value: 2 + >> aroundEach for: test 3 + >> test value: 3" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "test-each.test.ts": { + "test 1": "passed", + "test 2": "passed", + "test 3": "passed", + }, + } + `) +}) + +test('aroundEach with concurrent tests', async () => { + const { stderr, errorTree } = await runInlineTests({ + 'concurrent.test.ts': ` + import { aroundEach, describe, test } from 'vitest' + + const logs: string[] = [] + + aroundEach(async (runTest, context) => { + logs.push('start ' + context.task.name) + await runTest() + logs.push('end ' + context.task.name) + }) + + describe('concurrent suite', { concurrent: true }, () => { + test('test 1', async () => { + await new Promise(r => setTimeout(r, 50)) + }) + + test('test 2', async () => { + await new Promise(r => setTimeout(r, 30)) + }) + + test('test 3', async () => { + await new Promise(r => setTimeout(r, 10)) + }) + }) + `, + }) + + expect(stderr).toBe('') + expect(errorTree()).toMatchInlineSnapshot(` + { + "concurrent.test.ts": { + "concurrent suite": { + "test 1": "passed", + "test 2": "passed", + "test 3": "passed", + }, + }, + } + `) +}) + +test('aroundEach with retry', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'retry.test.ts': ` + import { aroundEach, test, expect } from 'vitest' + + let attempt = 0 + + aroundEach(async (runTest) => { + attempt++ + console.log('>> aroundEach attempt:', attempt) + await runTest() + }) + + test('retried test', { retry: 2 }, () => { + console.log('>> test attempt:', attempt) + if (attempt < 3) { + throw new Error('fail on purpose') + } + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundEach attempt: 1 + >> test attempt: 1 + >> aroundEach attempt: 2 + >> test attempt: 2 + >> aroundEach attempt: 3 + >> test attempt: 3" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "retry.test.ts": { + "retried test": "passed", + }, + } + `) +}) + +test('aroundEach receives suite as third argument', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'suite-arg.test.ts': ` + import { aroundEach, describe, test } from 'vitest' + + describe('my suite', () => { + aroundEach(async (runTest, _context, suite) => { + console.log('>> suite name:', suite.name) + await runTest() + }) + + test('test 1', () => { + console.log('>> test') + }) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> suite name: my suite + >> test" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "suite-arg.test.ts": { + "my suite": { + "test 1": "passed", + }, + }, + } + `) +}) + +test('aroundEach skipped when test is skipped', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'skipped.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest, context) => { + console.log('>> aroundEach for:', context.task.name) + await runTest() + }) + + test('normal test', () => { + console.log('>> normal test') + }) + + test.skip('skipped test', () => { + console.log('>> skipped test') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundEach for: normal test + >> normal test" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "skipped.test.ts": { + "normal test": "passed", + "skipped test": "skipped", + }, + } + `) +}) + +test('aroundEach setup phase timeout', async () => { + const { stderr } = await runInlineTests({ + 'setup-timeout.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> setup start') + // Simulate slow setup + await new Promise(r => setTimeout(r, 5000)) + console.log('>> setup end (should not reach)') + await runTest() + console.log('>> teardown') + }, 100) // 100ms timeout + + test('test with slow setup', () => { + console.log('>> test (should not run)') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL setup-timeout.test.ts > test with slow setup + AroundHookSetupError: The setup phase of "aroundEach" hook timed out after 100ms. + ❯ setup-timeout.test.ts:4:7 + 2| import { aroundEach, test } from 'vitest' + 3| + 4| aroundEach(async (runTest) => { + | ^ + 5| console.log('>> setup start') + 6| // Simulate slow setup + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) +}) + +test('aroundEach teardown phase timeout', async () => { + const { stdout, stderr } = await runInlineTests({ + 'teardown-timeout.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + console.log('>> setup') + await runTest() + console.log('>> teardown start') + // Simulate slow teardown + await new Promise(r => setTimeout(r, 5000)) + console.log('>> teardown end (should not reach)') + }, 100) // 100ms timeout + + test('test with slow teardown', () => { + console.log('>> test') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL teardown-timeout.test.ts > test with slow teardown + AroundHookTeardownError: The teardown phase of "aroundEach" hook timed out after 100ms. + ❯ teardown-timeout.test.ts:4:7 + 2| import { aroundEach, test } from 'vitest' + 3| + 4| aroundEach(async (runTest) => { + | ^ + 5| console.log('>> setup') + 6| await runTest() + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> setup + >> test + >> teardown start" + `) +}) + +test('aroundEach setup and teardown have independent timeouts', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'independent-timeouts.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + // Setup takes 80ms - under the 100ms timeout + console.log('>> setup start') + await new Promise(r => setTimeout(r, 80)) + console.log('>> setup end') + await runTest() + // Teardown takes 80ms - under the 100ms timeout + console.log('>> teardown start') + await new Promise(r => setTimeout(r, 80)) + console.log('>> teardown end') + }, 100) // 100ms timeout for each phase + + test('test with slow but valid phases', () => { + console.log('>> test') + }) + `, + }) + + // Both phases complete within their individual timeouts + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> setup start + >> setup end + >> test + >> teardown start + >> teardown end" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "independent-timeouts.test.ts": { + "test with slow but valid phases": "passed", + }, + } + `) +}) + +test('aroundEach default timeout uses hookTimeout config', async () => { + const { stderr } = await runInlineTests({ + 'default-timeout.test.ts': ` + import { aroundEach, test } from 'vitest' + + aroundEach(async (runTest) => { + // Setup takes longer than hookTimeout (10ms) + await new Promise(r => setTimeout(r, 200)) + await runTest() + }) + + test('test', () => {}) + `, + }, { hookTimeout: 10 }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL default-timeout.test.ts > test + AroundHookSetupError: The setup phase of "aroundEach" hook timed out after 10ms. + ❯ default-timeout.test.ts:4:7 + 2| import { aroundEach, test } from 'vitest' + 3| + 4| aroundEach(async (runTest) => { + | ^ + 5| // Setup takes longer than hookTimeout (10ms) + 6| await new Promise(r => setTimeout(r, 200)) + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) +}) + +test('multiple aroundEach hooks with different timeouts', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'multiple-timeouts.test.ts': ` + import { aroundEach, test } from 'vitest' + + // Outer hook with 200ms timeout + aroundEach(async (runTest) => { + console.log('>> outer setup') + await runTest() + console.log('>> outer teardown') + }, 200) + + // Inner hook with 50ms timeout - this should timeout during setup + aroundEach(async (runTest) => { + console.log('>> inner setup start') + await new Promise(r => setTimeout(r, 100)) // 100ms > 50ms timeout + console.log('>> inner setup end (should not reach)') + await runTest() + console.log('>> inner teardown') + }, 10) + + test('test', () => { + console.log('>> test (should not run)') + }) + `, + }) + + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> outer setup + >> inner setup start" + `) + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL multiple-timeouts.test.ts > test + AroundHookSetupError: The setup phase of "aroundEach" hook timed out after 10ms. + ❯ multiple-timeouts.test.ts:12:7 + 10| + 11| // Inner hook with 50ms timeout - this should timeout during set… + 12| aroundEach(async (runTest) => { + | ^ + 13| console.log('>> inner setup start') + 14| await new Promise(r => setTimeout(r, 100)) // 100ms > 50ms tim… + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "multiple-timeouts.test.ts": { + "test": [ + "The setup phase of "aroundEach" hook timed out after 10ms.", + ], + }, + } + `) +}) + +test('multiple aroundEach hooks where inner teardown times out', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'multiple-teardown-timeout.test.ts': ` + import { aroundEach, test } from 'vitest' + + // Outer hook with 200ms timeout + aroundEach(async (runTest) => { + console.log('>> outer setup') + await runTest() + console.log('>> outer teardown') + }, 200) + + // Inner hook with 50ms timeout - this should timeout during teardown + aroundEach(async (runTest) => { + console.log('>> inner setup') + await runTest() + console.log('>> inner teardown start') + await new Promise(r => setTimeout(r, 100)) // 100ms > 50ms timeout + console.log('>> inner teardown end (should not reach)') + }, 10) + + test('test', () => { + console.log('>> test') + }) + `, + }) + + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> outer setup + >> inner setup + >> test + >> inner teardown start" + `) + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL multiple-teardown-timeout.test.ts > test + AroundHookTeardownError: The teardown phase of "aroundEach" hook timed out after 10ms. + ❯ multiple-teardown-timeout.test.ts:12:7 + 10| + 11| // Inner hook with 50ms timeout - this should timeout during tea… + 12| aroundEach(async (runTest) => { + | ^ + 13| console.log('>> inner setup') + 14| await runTest() + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "multiple-teardown-timeout.test.ts": { + "test": [ + "The teardown phase of "aroundEach" hook timed out after 10ms.", + ], + }, + } + `) +}) + +test('aroundEach hook timeouts are independent of each other', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'independent-hook-timeouts.test.ts': ` + import { aroundEach, test } from 'vitest' + + // First hook with short 50ms timeout - but completes quickly + aroundEach(async (runTest) => { + console.log('>> first hook setup') + await runTest() + console.log('>> first hook teardown') + }, 10) + + // Second hook with 200ms timeout - takes 100ms which is longer than + // the first hook's 50ms timeout, but within its own 200ms timeout + aroundEach(async (runTest) => { + console.log('>> second hook setup start') + await new Promise(r => setTimeout(r, 100)) + console.log('>> second hook setup end') + await runTest() + console.log('>> second hook teardown start') + await new Promise(r => setTimeout(r, 100)) + console.log('>> second hook teardown end') + }, 200) + + test('test', () => { + console.log('>> test') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> first hook setup + >> second hook setup start + >> second hook setup end + >> test + >> second hook teardown start + >> second hook teardown end + >> first hook teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "independent-hook-timeouts.test.ts": { + "test": "passed", + }, + } + `) +}) + +test('aroundEach with AsyncLocalStorage', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'async-local-storage.test.ts': ` + import { AsyncLocalStorage } from 'node:async_hooks' + import { aroundEach, test, expect } from 'vitest' + + const requestContext = new AsyncLocalStorage<{ requestId: number }>() + let requestIdx = 0 + + aroundEach(async (runTest) => { + const ctx = { requestId: ++requestIdx } + console.log('>> setting context:', ctx.requestId) + await requestContext.run(ctx, runTest) + console.log('>> context cleared') + }) + + test('first test gets requestId 1', () => { + const ctx = requestContext.getStore() + console.log('>> test got context:', ctx?.requestId) + expect(ctx).toBeDefined() + expect(ctx?.requestId).toBe(1) + }) + + test('second test gets fresh context with requestId 2', () => { + const ctx = requestContext.getStore() + console.log('>> test got context:', ctx?.requestId) + expect(ctx?.requestId).toBe(2) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> setting context: 1 + >> test got context: 1 + >> context cleared + >> setting context: 2 + >> test got context: 2 + >> context cleared" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "async-local-storage.test.ts": { + "first test gets requestId 1": "passed", + "second test gets fresh context with requestId 2": "passed", + }, + } + `) +}) + +test('aroundEach with fixtures', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'fixtures.test.ts': ` + import { test as base, aroundEach, expect } from 'vitest' + + const test = base.extend<{ db: { query: (sql: string) => string } }>({ + db: async ({}, use) => { + console.log('>> db fixture setup') + await use({ + query: (sql: string) => \`result of: \${sql}\` + }) + console.log('>> db fixture teardown') + }, + user: async ({}, use) => { + console.log('>> user fixture setup') + await use({ name: 'test-user' }) + console.log('>> user fixture teardown') + }, + }) + + test.aroundEach(async (runTest, { db }) => { + console.log('>> aroundEach setup, db available:', !!db) + const result = db.query('SELECT 1') + console.log('>> query result:', result) + await runTest() + console.log('>> aroundEach teardown') + }) + + test('test with fixture in aroundEach', ({ db, user }) => { + console.log('>> test running, db available:', !!db) + expect(db.query('SELECT 2')).toBe('result of: SELECT 2') + expect(user.name).toBe('test-user') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> db fixture setup + >> aroundEach setup, db available: true + >> query result: result of: SELECT 1 + >> user fixture setup + >> test running, db available: true + >> user fixture teardown + >> db fixture teardown + >> aroundEach teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "fixtures.test.ts": { + "test with fixture in aroundEach": "passed", + }, + } + `) +}) + +test('aroundEach with AsyncLocalStorage fixture and value fixture', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'als-fixtures.test.ts': ` + import { test as base, aroundEach, expect } from 'vitest' + import { AsyncLocalStorage } from 'node:async_hooks' + + interface RequestContext { + requestId: number + } + + let requestIdx = 0 + + const test = base.extend<{ + requestContext: AsyncLocalStorage + currentRequestId: number + }>({ + requestContext: async ({}, use) => { + const als = new AsyncLocalStorage() + await use(als) + }, + currentRequestId: async ({ requestContext }, use) => { + const store = requestContext.getStore() + await use(store?.requestId) + } + }) + + aroundEach(async (runTest, { requestContext }) => { + const id = ++requestIdx + console.log('>> setting context:', id) + await requestContext.run({ requestId: id }, async () => { + await runTest() + }) + console.log('>> context cleared') + }) + + test('first test gets requestId 1 via fixture', ({ currentRequestId }) => { + console.log('>> test got requestId:', currentRequestId) + expect(currentRequestId).toBe(1) + }) + + test('second test gets requestId 2 via fixture', ({ currentRequestId }) => { + console.log('>> test got requestId:', currentRequestId) + expect(currentRequestId).toBe(2) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> setting context: 1 + >> test got requestId: 1 + >> context cleared + >> setting context: 2 + >> test got requestId: 2 + >> context cleared" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "als-fixtures.test.ts": { + "first test gets requestId 1 via fixture": "passed", + "second test gets requestId 2 via fixture": "passed", + }, + } + `) +}) + +// aroundAll tests + +test('basic aroundAll wraps the suite', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'basic.test.ts': ` + import { test, aroundAll } from 'vitest' + + aroundAll(async (runSuite) => { + console.log('>> aroundAll setup') + await runSuite() + console.log('>> aroundAll teardown') + }) + + test('first test', () => { + console.log('>> first test running') + }) + + test('second test', () => { + console.log('>> second test running') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundAll setup + >> first test running + >> second test running + >> aroundAll teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "first test": "passed", + "second test": "passed", + }, + } + `) +}) + +test('multiple aroundAll hooks are nested (first is outermost)', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'nested.test.ts': ` + import { test, aroundAll } from 'vitest' + + aroundAll(async (runSuite) => { + console.log('>> outer setup') + await runSuite() + console.log('>> outer teardown') + }) + + aroundAll(async (runSuite) => { + console.log('>> inner setup') + await runSuite() + console.log('>> inner teardown') + }) + + test('test', () => { + console.log('>> test running') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> outer setup + >> inner setup + >> test running + >> inner teardown + >> outer teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "nested.test.ts": { + "test": "passed", + }, + } + `) +}) + +test('aroundAll in nested suites wraps correctly', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'nested-suite.test.ts': ` + import { test, describe, aroundAll } from 'vitest' + + aroundAll(async (runSuite) => { + console.log('>> root aroundAll setup') + await runSuite() + console.log('>> root aroundAll teardown') + }) + + test('root test', () => { + console.log('>> root test running') + }) + + describe('nested suite', () => { + aroundAll(async (runSuite) => { + console.log('>> nested aroundAll setup') + await runSuite() + console.log('>> nested aroundAll teardown') + }) + + test('nested test', () => { + console.log('>> nested test running') + }) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> root aroundAll setup + >> root test running + >> nested aroundAll setup + >> nested test running + >> nested aroundAll teardown + >> root aroundAll teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "nested-suite.test.ts": { + "nested suite": { + "nested test": "passed", + }, + "root test": "passed", + }, + } + `) +}) + +test('aroundAll throws error when runSuite is called multiple times', async () => { + const { stderr } = await runInlineTests({ + 'multiple-calls.test.ts': ` + import { test, aroundAll } from 'vitest' + + aroundAll(async (runSuite) => { + await runSuite() + await runSuite() // second call should throw + }) + + test('test', () => { + console.log('>> test running') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL multiple-calls.test.ts [ multiple-calls.test.ts ] + AroundHookMultipleCallsError: The \`runSuite()\` callback was called multiple times in the \`aroundAll\` hook. The callback can only be called once per hook. + ❯ multiple-calls.test.ts:6:15 + 4| aroundAll(async (runSuite) => { + 5| await runSuite() + 6| await runSuite() // second call should throw + | ^ + 7| }) + 8| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) +}) + +test('aroundAll throws error when runSuite is not called', async () => { + const { stderr, errorTree } = await runInlineTests({ + 'no-run.test.ts': ` + import { test, aroundAll } from 'vitest' + + aroundAll(async (runSuite) => { + console.log('>> aroundAll setup but not calling runSuite') + }) + + test('test', () => { + console.log('>> test running') + }) + `, + }) + + expect(stderr).toContain('runSuite()') + expect(errorTree()).toMatchInlineSnapshot(` + { + "no-run.test.ts": { + "test": "skipped", + }, + } + `) +}) + +test('aroundAll cleanup runs even on test failure', async () => { + const { stdout, errorTree } = await runInlineTests({ + 'cleanup.test.ts': ` + import { test, aroundAll, expect } from 'vitest' + + aroundAll(async (runSuite) => { + console.log('>> aroundAll setup') + await runSuite() + console.log('>> aroundAll teardown') + }) + + test('failing test', () => { + console.log('>> failing test running') + expect(true).toBe(false) + }) + `, + }) + + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundAll setup + >> failing test running + >> aroundAll teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "cleanup.test.ts": { + "failing test": [ + "expected true to be false // Object.is equality", + ], + }, + } + `) +}) + +test('aroundAll with beforeAll and afterAll', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'with-hooks.test.ts': ` + import { test, beforeAll, afterAll, aroundAll } from 'vitest' + + beforeAll(() => { + console.log('>> beforeAll') + }) + + aroundAll(async (runSuite) => { + console.log('>> aroundAll setup') + await runSuite() + console.log('>> aroundAll teardown') + }) + + afterAll(() => { + console.log('>> afterAll') + }) + + test('test', () => { + console.log('>> test running') + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundAll setup + >> beforeAll + >> test running + >> afterAll + >> aroundAll teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "with-hooks.test.ts": { + "test": "passed", + }, + } + `) +}) + +test('aroundAll setup phase timeout', async () => { + const { stderr, errorTree } = await runInlineTests({ + 'timeout.test.ts': ` + import { test, aroundAll } from 'vitest' + + aroundAll(async (runSuite) => { + console.log('>> aroundAll setup starting') + await new Promise(resolve => setTimeout(resolve, 200)) + console.log('>> aroundAll setup done') + await runSuite() + }, 10) + + test('test', () => { + console.log('>> test running') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL timeout.test.ts [ timeout.test.ts ] + AroundHookSetupError: The setup phase of "aroundAll" hook timed out after 10ms. + ❯ timeout.test.ts:4:7 + 2| import { test, aroundAll } from 'vitest' + 3| + 4| aroundAll(async (runSuite) => { + | ^ + 5| console.log('>> aroundAll setup starting') + 6| await new Promise(resolve => setTimeout(resolve, 200)) + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "timeout.test.ts": { + "test": "skipped", + }, + } + `) +}) + +test('aroundAll teardown phase timeout', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'teardown-timeout.test.ts': ` + import { test, aroundAll } from 'vitest' + + aroundAll(async (runSuite) => { + console.log('>> aroundAll setup') + await runSuite() + console.log('>> aroundAll teardown starting') + await new Promise(resolve => setTimeout(resolve, 200)) + console.log('>> aroundAll teardown done') + }, 10) + + test('test', () => { + console.log('>> test running') + }) + `, + }) + + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundAll setup + >> test running + >> aroundAll teardown starting" + `) + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL teardown-timeout.test.ts [ teardown-timeout.test.ts ] + AroundHookTeardownError: The teardown phase of "aroundAll" hook timed out after 10ms. + ❯ teardown-timeout.test.ts:4:7 + 2| import { test, aroundAll } from 'vitest' + 3| + 4| aroundAll(async (runSuite) => { + | ^ + 5| console.log('>> aroundAll setup') + 6| await runSuite() + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "teardown-timeout.test.ts": { + "test": "passed", + }, + } + `) +}) + +test('aroundAll receives suite as second argument', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'suite-arg.test.ts': ` + import { test, describe, aroundAll } from 'vitest' + + describe('my suite', () => { + aroundAll(async (runSuite, suite) => { + console.log('>> suite name:', suite.name) + await runSuite() + }) + + test('test', () => { + console.log('>> test running') + }) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> suite name: my suite + >> test running" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "suite-arg.test.ts": { + "my suite": { + "test": "passed", + }, + }, + } + `) +}) + +test('aroundAll with server start/stop pattern', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'server.test.ts': ` + import { test, aroundAll, expect } from 'vitest' + + let serverPort: number | null = null + + aroundAll(async (runSuite) => { + // Simulate server start + serverPort = 3000 + console.log('>> server started on port', serverPort) + await runSuite() + // Simulate server stop + console.log('>> server stopping') + serverPort = null + }) + + test('first request', () => { + console.log('>> making request to port', serverPort) + expect(serverPort).toBe(3000) + }) + + test('second request', () => { + console.log('>> making request to port', serverPort) + expect(serverPort).toBe(3000) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> server started on port 3000 + >> making request to port 3000 + >> making request to port 3000 + >> server stopping" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "server.test.ts": { + "first request": "passed", + "second request": "passed", + }, + } + `) +}) + +test('aroundAll with multiple suites and multiple hooks in same suite', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'multi-suite.test.ts': ` + import { test, describe, aroundAll } from 'vitest' + + aroundAll(async (runSuite) => { + console.log('>> root aroundAll 1 setup') + await runSuite() + console.log('>> root aroundAll 1 teardown') + }) + + aroundAll(async (runSuite) => { + console.log('>> root aroundAll 2 setup') + await runSuite() + console.log('>> root aroundAll 2 teardown') + }) + + test('root test', () => { + console.log('>> root test') + }) + + describe('suite A', () => { + aroundAll(async (runSuite) => { + console.log('>> suite A aroundAll 1 setup') + await runSuite() + console.log('>> suite A aroundAll 1 teardown') + }) + + aroundAll(async (runSuite) => { + console.log('>> suite A aroundAll 2 setup') + await runSuite() + console.log('>> suite A aroundAll 2 teardown') + }) + + test('test A1', () => { + console.log('>> test A1') + }) + + test('test A2', () => { + console.log('>> test A2') + }) + }) + + describe('suite B', () => { + aroundAll(async (runSuite) => { + console.log('>> suite B aroundAll setup') + await runSuite() + console.log('>> suite B aroundAll teardown') + }) + + test('test B1', () => { + console.log('>> test B1') + }) + + describe('nested suite', () => { + aroundAll(async (runSuite) => { + console.log('>> nested aroundAll setup') + await runSuite() + console.log('>> nested aroundAll teardown') + }) + + test('nested test', () => { + console.log('>> nested test') + }) + }) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> root aroundAll 1 setup + >> root aroundAll 2 setup + >> root test + >> suite A aroundAll 1 setup + >> suite A aroundAll 2 setup + >> test A1 + >> test A2 + >> suite A aroundAll 2 teardown + >> suite A aroundAll 1 teardown + >> suite B aroundAll setup + >> test B1 + >> nested aroundAll setup + >> nested test + >> nested aroundAll teardown + >> suite B aroundAll teardown + >> root aroundAll 2 teardown + >> root aroundAll 1 teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "multi-suite.test.ts": { + "root test": "passed", + "suite A": { + "test A1": "passed", + "test A2": "passed", + }, + "suite B": { + "nested suite": { + "nested test": "passed", + }, + "test B1": "passed", + }, + }, + } + `) +}) + +test('aroundAll with module-level AsyncLocalStorage and test fixture', async () => { + const { stdout, stderr, errorTree } = await runInlineTests({ + 'module-als.test.ts': ` + import { test as base, aroundAll, expect } from 'vitest' + import { AsyncLocalStorage } from 'node:async_hooks' + + interface RequestContext { + requestId: number + } + + // Module-level AsyncLocalStorage shared between aroundAll and fixtures + const requestContext = new AsyncLocalStorage() + let suiteRequestId = 0 + + const test = base.extend<{ + currentRequestId: number + }>({ + currentRequestId: async ({}, use) => { + const store = requestContext.getStore() + console.log('>> currentRequestId fixture reading store:', store?.requestId) + await use(store?.requestId) + } + }) + + aroundAll(async (runSuite) => { + suiteRequestId++ + console.log('>> aroundAll setup, setting requestId:', suiteRequestId) + await requestContext.run({ requestId: suiteRequestId }, async () => { + await runSuite() + }) + console.log('>> aroundAll teardown') + }) + + test('first test gets requestId from aroundAll context', ({ currentRequestId }) => { + console.log('>> first test, currentRequestId:', currentRequestId) + expect(currentRequestId).toBe(1) + }) + + test('second test gets same requestId from aroundAll context', ({ currentRequestId }) => { + console.log('>> second test, currentRequestId:', currentRequestId) + expect(currentRequestId).toBe(1) + }) + `, + }) + + expect(stderr).toBe('') + expect(extractLogs(stdout)).toMatchInlineSnapshot(` + ">> aroundAll setup, setting requestId: 1 + >> currentRequestId fixture reading store: 1 + >> first test, currentRequestId: 1 + >> currentRequestId fixture reading store: 1 + >> second test, currentRequestId: 1 + >> aroundAll teardown" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "module-als.test.ts": { + "first test gets requestId from aroundAll context": "passed", + "second test gets same requestId from aroundAll context": "passed", + }, + } + `) +}) + +test('tests are skipped when aroundAll setup fails', async () => { + const { stderr, errorTree } = await runInlineTests({ + 'aroundAll-setup-error.test.ts': ` + import { test, aroundAll } from 'vitest' + + aroundAll(async () => { + throw new Error('aroundAll setup error') + }) + + test('test should be skipped', () => { + console.log('>> test should not run') + }) + `, + }) + + expect(stderr).toMatchInlineSnapshot(` + " + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL aroundAll-setup-error.test.ts [ aroundAll-setup-error.test.ts ] + Error: aroundAll setup error + ❯ aroundAll-setup-error.test.ts:5:15 + 3| + 4| aroundAll(async () => { + 5| throw new Error('aroundAll setup error') + | ^ + 6| }) + 7| + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ + + " + `) + + // Test should be skipped because aroundAll setup failed + expect(errorTree()).toMatchInlineSnapshot(` + { + "aroundAll-setup-error.test.ts": { + "test should be skipped": "skipped", + }, + } + `) +}) diff --git a/test/cli/test/expect-task.test.ts b/test/cli/test/expect-task.test.ts index 5b612863373a..d1dae4edb203 100644 --- a/test/cli/test/expect-task.test.ts +++ b/test/cli/test/expect-task.test.ts @@ -173,7 +173,7 @@ describe('serial', { concurrent: true }, () => { test: testBoundLocalExtend, }, ] as const)('works with $name', async ({ options, test }, { expect }) => { - const { stdout } = await runInlineTests( + const { stdout, stderr } = await runInlineTests( { 'basic.test.ts': test, 'to-match-test.ts': toMatchTest, @@ -181,6 +181,7 @@ describe('serial', { concurrent: true }, () => { { reporters: ['tap'], ...options }, ) + expect(stderr).toBe('') expect(stdout.replace(/[\d.]+ms/g, '