返回 Skills
antfu/skills· MIT 内容可用

vitest

Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: vitest description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures. metadata: author: Anthony Fu version: "2026.6.22" source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills

Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app.

Key Features:

  • Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates
  • Jest-compatible: Drop-in replacement for most Jest test suites
  • Smart watch mode: Only reruns affected tests based on module graph
  • Native ESM, TypeScript, JSX support without configuration
  • Multi-threaded workers for parallel test execution
  • Built-in coverage via V8 or Istanbul
  • Snapshot testing, mocking, and spy utilities

The skill is based on Vitest 5.x (beta), generated at 2026-06-22.

Core

TopicDescriptionReference
ConfigurationVitest and Vite config integration, defineConfig usagecore-config
CLICommand line interface, commands and optionscore-cli
Test APItest/it function, modifiers like skip, only, concurrentcore-test-api
Describe APIdescribe/suite for grouping tests and nested suitescore-describe
Expect APIAssertions with toBe, toEqual, matchers and asymmetric matcherscore-expect
HooksbeforeEach, afterEach, beforeAll, afterAll, aroundEachcore-hooks

Features

TopicDescriptionReference
MockingMock functions, modules, timers, dates with vi utilitiesfeatures-mocking
SnapshotsSnapshot testing with toMatchSnapshot and inline snapshotsfeatures-snapshots
CoverageCode coverage with V8 or Istanbul providersfeatures-coverage
Test ContextTest fixtures, context.expect, test.extend for custom fixturesfeatures-context
ConcurrencyConcurrent tests, parallel execution, shardingfeatures-concurrency
FilteringFilter tests by name, file patterns, tagsfeatures-filtering
Test TagsLabel tests with tags to filter runs and apply shared optionsfeatures-test-tags
ReportersBuilt-in reporters, default selection, CI/output configfeatures-reporters
BenchmarkingWrite benchmarks with the bench fixture (Tinybench)features-benchmarking

Advanced

TopicDescriptionReference
Vi Utilitiesvi helper: mock, spyOn, fake timers, hoisted, waitForadvanced-vi
EnvironmentsTest environments: node, jsdom, happy-dom, customadvanced-environments
Type TestingType-level testing with expectTypeOf and assertTypeadvanced-type-testing
ProjectsMulti-project workspaces, different configs per projectadvanced-projects

附带文件

GENERATION.md
# Generation Info

- **Source:** `sources/vitest`
- **Git SHA:** `3f60d0893affbda989404555136fe20a6086d4e6`
- **Generated:** 2026-06-22
references/advanced-environments.md
---
name: test-environments
description: Configure environments like jsdom, happy-dom for browser APIs
---

# Test Environments

## Available Environments

- `node` (default) - Node.js environment
- `jsdom` - Browser-like with DOM APIs
- `happy-dom` - Faster alternative to jsdom
- `edge-runtime` - Vercel Edge Runtime

## Configuration

```ts
// vitest.config.ts
defineConfig({
  test: {
    environment: 'jsdom',
    
    // Environment-specific options
    environmentOptions: {
      jsdom: {
        url: 'http://localhost',
      },
    },
  },
})
```

## Installing Environment Packages

```bash
# jsdom
npm i -D jsdom

# happy-dom (faster, fewer APIs)
npm i -D happy-dom
```

## Per-File Environment

Use magic comment at top of file:

```ts
// @vitest-environment jsdom

import { expect, test } from 'vitest'

test('DOM test', () => {
  const div = document.createElement('div')
  expect(div).toBeInstanceOf(HTMLDivElement)
})
```

## jsdom Environment

Full browser environment simulation:

```ts
// @vitest-environment jsdom

test('DOM manipulation', () => {
  document.body.innerHTML = '<div id="app"></div>'
  
  const app = document.getElementById('app')
  app.textContent = 'Hello'
  
  expect(app.textContent).toBe('Hello')
})

test('window APIs', () => {
  expect(window.location.href).toBeDefined()
  expect(localStorage).toBeDefined()
})
```

### jsdom Options

```ts
defineConfig({
  test: {
    environmentOptions: {
      jsdom: {
        url: 'http://localhost:3000',
        html: '<!DOCTYPE html><html><body></body></html>',
        userAgent: 'custom-agent',
        resources: 'usable',
      },
    },
  },
})
```

## happy-dom Environment

Faster but fewer APIs:

```ts
// @vitest-environment happy-dom

test('basic DOM', () => {
  const el = document.createElement('div')
  el.className = 'test'
  expect(el.className).toBe('test')
})
```

## Multiple Environments per Project

Use projects for different environments:

```ts
defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          include: ['tests/unit/**/*.test.ts'],
          environment: 'node',
        },
      },
      {
        test: {
          name: 'dom',
          include: ['tests/dom/**/*.test.ts'],
          environment: 'jsdom',
        },
      },
    ],
  },
})
```

## Custom Environment

Create custom environment package:

```ts
// vitest-environment-custom/index.ts
import type { Environment } from 'vitest/runtime'

export default <Environment>{
  name: 'custom',
  viteEnvironment: 'ssr', // or 'client'
  
  setup() {
    // Setup global state
    globalThis.myGlobal = 'value'
    
    return {
      teardown() {
        delete globalThis.myGlobal
      },
    }
  },
}
```

Use with:

```ts
defineConfig({
  test: {
    environment: 'custom',
  },
})
```

## Environment with VM

For full isolation:

```ts
export default <Environment>{
  name: 'isolated',
  viteEnvironment: 'ssr',
  
  async setupVM() {
    const vm = await import('node:vm')
    const context = vm.createContext()
    
    return {
      getVmContext() {
        return context
      },
      teardown() {},
    }
  },
  
  setup() {
    return { teardown() {} }
  },
}
```

## Browser Mode (Separate from Environments)

For real browser testing, use Vitest Browser Mode. In **v4 the provider is an object** (not a string), and the context imports from `vitest/browser`:

```ts
import { playwright } from '@vitest/browser-playwright'

defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: playwright({ launchOptions: { slowMo: 100 } }),
      instances: [{ browser: 'chromium' }], // or 'firefox', 'webkit'
    },
  },
})
```

```ts
import { page } from 'vitest/browser' // v4: was '@vitest/browser/context'
```

> v5: DOM-environment global assignments (e.g. `window.innerWidth`) now propagate to the underlying jsdom/happy-dom implementation. Locators are also exact/strict by default.

## CSS and Assets

In jsdom/happy-dom, configure CSS handling:

```ts
defineConfig({
  test: {
    css: true, // Process CSS
    
    // Or with options
    css: {
      include: /\.module\.css$/,
      modules: {
        classNameStrategy: 'non-scoped',
      },
    },
  },
})
```

## Fixing External Dependencies

If external deps fail with CSS/asset errors:

```ts
defineConfig({
  test: {
    server: {
      deps: {
        inline: ['problematic-package'],
      },
    },
  },
})
```

## Key Points

- Default is `node` - no browser APIs
- Use `jsdom` for full browser simulation
- Use `happy-dom` for faster tests with basic DOM
- Per-file environment via `// @vitest-environment` comment
- Use projects for multiple environment configurations
- Browser Mode is for real browser testing, not environment

<!-- 
Source references:
- https://vitest.dev/guide/environment.html
-->
references/advanced-projects.md
---
name: projects-workspaces
description: Multi-project configuration for monorepos and different test types
---

# Projects

Run different test configurations in the same Vitest process.

## Basic Projects Setup

```ts
// vitest.config.ts
defineConfig({
  test: {
    projects: [
      // Glob patterns for config files
      'packages/*',
      
      // Inline config
      {
        test: {
          name: 'unit',
          include: ['tests/unit/**/*.test.ts'],
          environment: 'node',
        },
      },
      {
        test: {
          name: 'integration',
          include: ['tests/integration/**/*.test.ts'],
          environment: 'jsdom',
        },
      },
    ],
  },
})
```

## Monorepo Pattern

```ts
defineConfig({
  test: {
    projects: [
      // Each package has its own vitest.config.ts
      'packages/core',
      'packages/cli',
      'packages/utils',
    ],
  },
})
```

Package config:

```ts
// packages/core/vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    name: 'core',
    include: ['src/**/*.test.ts'],
    environment: 'node',
  },
})
```

## Different Environments

Run same tests in different environments:

```ts
defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'happy-dom',
          root: './shared-tests',
          environment: 'happy-dom',
          setupFiles: ['./setup.happy-dom.ts'],
        },
      },
      {
        test: {
          name: 'node',
          root: './shared-tests',
          environment: 'node',
          setupFiles: ['./setup.node.ts'],
        },
      },
    ],
  },
})
```

## Browser + Node Projects

```ts
defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          include: ['tests/unit/**/*.test.ts'],
          environment: 'node',
        },
      },
      {
        test: {
          name: 'browser',
          include: ['tests/browser/**/*.test.ts'],
          browser: {
            enabled: true,
            name: 'chromium',
            provider: 'playwright',
          },
        },
      },
    ],
  },
})
```

## Shared Configuration

```ts
// vitest.shared.ts
export const sharedConfig = {
  testTimeout: 10000,
  setupFiles: ['./tests/setup.ts'],
}

// vitest.config.ts
import { sharedConfig } from './vitest.shared'

defineConfig({
  test: {
    projects: [
      {
        test: {
          ...sharedConfig,
          name: 'unit',
          include: ['tests/unit/**/*.test.ts'],
        },
      },
      {
        test: {
          ...sharedConfig,
          name: 'e2e',
          include: ['tests/e2e/**/*.test.ts'],
        },
      },
    ],
  },
})
```

## Project-Specific Dependencies

Each project can have different dependencies inlined:

```ts
defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'project-a',
          server: {
            deps: {
              inline: ['package-a'],
            },
          },
        },
      },
    ],
  },
})
```

## Running Specific Projects

```bash
# Run specific project
vitest --project unit
vitest --project integration

# Multiple projects / wildcards
vitest --project unit --project e2e
vitest --project="packages*"

# Exclude a project
vitest --project="!browser"
```

## Providing Values to Projects

Share values from config to tests:

```ts
// vitest.config.ts
defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'staging',
          provide: {
            apiUrl: 'https://staging.api.com',
            debug: true,
          },
        },
      },
      {
        test: {
          name: 'production',
          provide: {
            apiUrl: 'https://api.com',
            debug: false,
          },
        },
      },
    ],
  },
})

// In tests, use inject
import { inject } from 'vitest'

test('uses correct api', () => {
  const url = inject('apiUrl')
  expect(url).toContain('api.com')
})
```

## With Fixtures

```ts
const test = base.extend({
  apiUrl: ['/default', { injected: true }],
})

test('uses injected url', ({ apiUrl }) => {
  // apiUrl comes from project's provide config
})
```

## Per-Project Pool & Isolation (v4)

Since the v4 pool rework, isolation, parallelism, and Node CLI options can be set **per project**:

```ts
defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'unit',
          isolate: false,                 // fast, non-isolated unit tests
          exclude: ['**/*.integration.test.ts'],
        },
      },
      {
        test: {
          name: 'sequential',
          include: ['**/*.sequential.test.ts'],
          fileParallelism: false,         // run these files one at a time
        },
      },
      {
        test: {
          name: 'staging',
          execArgv: ['--env-file=.env.staging'], // per-project Node flags
        },
      },
    ],
  },
})
```

## Global Setup per Project

```ts
defineConfig({
  test: {
    projects: [
      {
        test: {
          name: 'with-db',
          globalSetup: ['./tests/db-setup.ts'],
        },
      },
    ],
  },
})
```

## Key Points

- Projects run in same Vitest process (replaces the removed `workspace` option)
- Each project can have different environment, pool, isolation, and config
- Use glob patterns for monorepo packages
- Run specific projects with `--project` (supports wildcards and `!` exclusion)
- Use `provide` to inject config values into tests
- Projects inherit from root config unless overridden

<!-- 
Source references:
- https://vitest.dev/guide/projects.html
-->
references/advanced-type-testing.md
---
name: type-testing
description: Test TypeScript types with expectTypeOf and assertType
---

# Type Testing

Test TypeScript types without runtime execution.

## Setup

Type tests use `.test-d.ts` extension:

```ts
// math.test-d.ts
import { expectTypeOf } from 'vitest'
import { add } from './math'

test('add returns number', () => {
  expectTypeOf(add).returns.toBeNumber()
})
```

## Configuration

```ts
defineConfig({
  test: {
    typecheck: {
      enabled: true,
      
      // Only type check
      only: false,
      
      // Checker: 'tsc' or 'vue-tsc'
      checker: 'tsc',
      
      // Include patterns
      include: ['**/*.test-d.ts'],
      
      // tsconfig to use
      tsconfig: './tsconfig.json',
    },
  },
})
```

## expectTypeOf API

```ts
import { expectTypeOf } from 'vitest'

// Basic type checks
expectTypeOf<string>().toBeString()
expectTypeOf<number>().toBeNumber()
expectTypeOf<boolean>().toBeBoolean()
expectTypeOf<null>().toBeNull()
expectTypeOf<undefined>().toBeUndefined()
expectTypeOf<void>().toBeVoid()
expectTypeOf<never>().toBeNever()
expectTypeOf<any>().toBeAny()
expectTypeOf<unknown>().toBeUnknown()
expectTypeOf<object>().toBeObject()
expectTypeOf<Function>().toBeFunction()
expectTypeOf<[]>().toBeArray()
expectTypeOf<symbol>().toBeSymbol()
```

## Value Type Checking

```ts
const value = 'hello'
expectTypeOf(value).toBeString()

const obj = { name: 'test', count: 42 }
expectTypeOf(obj).toExtend<{ name: string }>()
expectTypeOf(obj).toHaveProperty('name')
```

## Function Types

```ts
function greet(name: string): string {
  return `Hello, ${name}`
}

expectTypeOf(greet).toBeFunction()
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>()
expectTypeOf(greet).returns.toBeString()

// Parameter checking
expectTypeOf(greet).parameter(0).toBeString()
```

## Object Types

```ts
interface User {
  id: number
  name: string
  email?: string
}

expectTypeOf<User>().toHaveProperty('id')
expectTypeOf<User>().toHaveProperty('name').toBeString()

// Check shape
expectTypeOf({ id: 1, name: 'test' }).toExtend<User>()
```

## Equality vs Matching

`toMatchTypeOf` is **deprecated** (expect-type v1.2+) — use `toExtend` for subset matching:

```ts
interface A { x: number }
interface B { x: number; y: string }

// toExtend - subset matching (replaces toMatchTypeOf)
expectTypeOf<B>().toExtend<A>()  // B extends A

// toEqualTypeOf - exact match
expectTypeOf<A>().not.toEqualTypeOf<B>()  // Not exact match
expectTypeOf<A>().toEqualTypeOf<{ x: number }>()  // Exact match
```

## Branded Types

```ts
type UserId = number & { __brand: 'UserId' }
type PostId = number & { __brand: 'PostId' }

expectTypeOf<UserId>().not.toEqualTypeOf<PostId>()
expectTypeOf<UserId>().not.toEqualTypeOf<number>()
```

## Generic Types

```ts
function identity<T>(value: T): T {
  return value
}

expectTypeOf(identity<string>).returns.toBeString()
expectTypeOf(identity<number>).returns.toBeNumber()
```

## Nullable Types

```ts
type MaybeString = string | null | undefined

expectTypeOf<MaybeString>().toBeNullable()
expectTypeOf<string>().not.toBeNullable()
```

## assertType

Assert a value matches a type (no assertion at runtime):

```ts
import { assertType } from 'vitest'

function getUser(): User | null {
  return { id: 1, name: 'test' }
}

test('returns user', () => {
  const result = getUser()
  
  // @ts-expect-error - should fail type check
  assertType<string>(result)
  
  // Correct type
  assertType<User | null>(result)
})
```

## Using @ts-expect-error

Test that code produces type error:

```ts
test('rejects wrong types', () => {
  function requireString(s: string) {}
  
  // @ts-expect-error - number not assignable to string
  requireString(123)
})
```

## Running Type Tests

```bash
# Run type tests
vitest typecheck

# Run alongside unit tests
vitest --typecheck

# Type tests only
vitest --typecheck.only
```

## Mixed Test Files

Combine runtime and type tests:

```ts
// user.test.ts
import { describe, expect, expectTypeOf, test } from 'vitest'
import { createUser } from './user'

describe('createUser', () => {
  test('runtime: creates user', () => {
    const user = createUser('John')
    expect(user.name).toBe('John')
  })

  test('types: returns User type', () => {
    expectTypeOf(createUser).returns.toExtend<{ name: string }>()
  })
})
```

## Key Points

- Use `.test-d.ts` for type-only tests
- `expectTypeOf` for type assertions
- `toExtend` for subset matching (`toMatchTypeOf` is deprecated)
- `toEqualTypeOf` for exact type matching
- Use `@ts-expect-error` to test type errors
- Run with `vitest typecheck` or `--typecheck`

<!-- 
Source references:
- https://vitest.dev/guide/testing-types.html
- https://vitest.dev/api/expect-typeof.html
-->
references/advanced-vi.md
---
name: vi-utilities
description: vi helper for mocking, timers, utilities
---

# Vi Utilities

The `vi` helper provides mocking and utility functions.

```ts
import { vi } from 'vitest'
```

## Mock Functions

```ts
// Create mock
const fn = vi.fn()
const fnWithImpl = vi.fn((x) => x * 2)

// Check if mock
vi.isMockFunction(fn) // true

// Mock methods
fn.mockReturnValue(42)
fn.mockReturnValueOnce(1)
fn.mockResolvedValue(data)
fn.mockRejectedValue(error)
fn.mockImplementation(() => 'result')
fn.mockImplementationOnce(() => 'once')

// Clear/reset
fn.mockClear()    // Clear call history
fn.mockReset()    // Clear history + implementation
fn.mockRestore()  // Restore original (for spies)
```

## Spying

```ts
const obj = { method: () => 'original' }

const spy = vi.spyOn(obj, 'method')
obj.method()

expect(spy).toHaveBeenCalled()

// Mock implementation
spy.mockReturnValue('mocked')

// Spy on getter/setter
vi.spyOn(obj, 'prop', 'get').mockReturnValue('value')
```

## Module Mocking

```ts
// Hoisted to top of file
vi.mock('./module', () => ({
  fn: vi.fn(),
}))

// Partial mock
vi.mock('./module', async (importOriginal) => ({
  ...(await importOriginal()),
  specificFn: vi.fn(),
}))

// Spy mode - keep implementation
vi.mock('./module', { spy: true })

// Import actual module inside mock
const actual = await vi.importActual('./module')

// Import as mock
const mocked = await vi.importMock('./module')
```

## Dynamic Mocking

```ts
// Not hoisted - use with dynamic imports
vi.doMock('./config', () => ({ key: 'value' }))
const config = await import('./config')

// Unmock
vi.doUnmock('./config')
vi.unmock('./module') // Hoisted
```

## Conditional Mocking — vi.when (v5)

Argument-specific spy behaviors:

```ts
vi.when(spy)
  .calledWith(1).thenReturn('one')
  .calledWith(2).thenReturn('two')

// then* actions: thenReturn / thenThrow / thenResolve / thenReject (+ *Once)
// options: { times }, second arg { onUnmatched: 'throw' | 'passthrough' | fn }

vi.isWhenChain(w)  // type guard for a When chain
```

See [features-mocking](features-mocking.md) for FIFO/LIFO matching and `toHaveBeenExhausted`.

## Assertion Helpers — vi.defineHelper (4.1+)

Wrap reusable assertion functions so failures point at the **call site**, not inside the helper:

```ts
const expectValidUser = vi.defineHelper((user: unknown) => {
  expect(user).toHaveProperty('id')
  expect(user).toHaveProperty('email')
})

test('returns a valid user', async () => {
  expectValidUser(await fetchUser('alice')) // failures reported here
})
```

## Reset Modules

```ts
// Clear module cache
vi.resetModules()

// Wait for dynamic imports
await vi.dynamicImportSettled()
```

## Fake Timers

```ts
vi.useFakeTimers()

// Choose which timers to fake (toFake and toNotFake are mutually exclusive)
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
vi.useFakeTimers({ toNotFake: ['setInterval'] })

setTimeout(() => console.log('done'), 1000)

// Advance time
vi.advanceTimersByTime(1000)
vi.advanceTimersByTimeAsync(1000)  // For async callbacks
vi.advanceTimersToNextTimer()
vi.advanceTimersToNextFrame()      // requestAnimationFrame

// Run all timers
vi.runAllTimers()
vi.runAllTimersAsync()
vi.runOnlyPendingTimers()

// Clear timers
vi.clearAllTimers()

// Check state
vi.getTimerCount()
vi.isFakeTimers()

// Restore
vi.useRealTimers()
```

## Mock Date/Time

```ts
vi.setSystemTime(new Date('2024-01-01'))
expect(new Date().getFullYear()).toBe(2024)

vi.getMockedSystemTime()  // Get mocked date
vi.getRealSystemTime()    // Get real time (ms)
```

## Global/Env Mocking

```ts
// Stub global
vi.stubGlobal('fetch', vi.fn())
vi.unstubAllGlobals()

// Stub environment
vi.stubEnv('API_KEY', 'test')
vi.stubEnv('NODE_ENV', 'test')
vi.unstubAllEnvs()
```

## Hoisted Code

Run code before imports:

```ts
const mock = vi.hoisted(() => vi.fn())

vi.mock('./module', () => ({
  fn: mock, // Can reference hoisted variable
}))
```

## Waiting Utilities

```ts
// Wait for callback to succeed
await vi.waitFor(async () => {
  const el = document.querySelector('.loaded')
  expect(el).toBeTruthy()
}, { timeout: 5000, interval: 100 })

// Wait for truthy value
const element = await vi.waitUntil(
  () => document.querySelector('.loaded'),
  { timeout: 5000 }
)
```

## Mock Object

Mock all methods of an object:

```ts
const original = {
  method: () => 'real',
  nested: { fn: () => 'nested' },
}

const mocked = vi.mockObject(original)
mocked.method()  // undefined (mocked)
mocked.method.mockReturnValue('mocked')

// Spy mode
const spied = vi.mockObject(original, { spy: true })
spied.method()  // 'real'
expect(spied.method).toHaveBeenCalled()
```

## Test Configuration

```ts
vi.setConfig({
  testTimeout: 10_000,
  hookTimeout: 10_000,
})

vi.resetConfig()
```

## Global Mock Management

```ts
vi.clearAllMocks()   // Clear all mock call history
vi.resetAllMocks()   // Reset + clear implementation
vi.restoreAllMocks() // Restore originals (spies)
```

## vi.mocked Type Helper

TypeScript helper for mocked values:

```ts
import { myFn } from './module'
vi.mock('./module')

// Type as mock
vi.mocked(myFn).mockReturnValue('typed')

// Deep mocking
vi.mocked(myModule, { deep: true })

// Partial mock typing
vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true })
```

## Key Points

- `vi.mock` is hoisted - use `vi.doMock` for dynamic mocking
- `vi.hoisted` lets you reference variables in mock factories
- Use `vi.spyOn` to spy on existing methods (v4: supports constructors)
- Use `vi.when` for argument-specific behaviors and `vi.defineHelper` for assertion helpers
- Fake timers require explicit setup and teardown
- `vi.waitFor` retries until assertion passes

<!-- 
Source references:
- https://vitest.dev/api/vi.html
-->
references/core-cli.md
---
name: vitest-cli
description: Command line interface commands and options
---

# Command Line Interface

## Commands

### `vitest`

Start Vitest in watch mode (dev) or run mode (CI):

```bash
vitest                    # Watch mode in dev, run mode in CI
vitest foobar             # Run tests containing "foobar" in path
vitest basic/foo.test.ts:10  # Run specific test by file and line number
```

### `vitest run`

Run tests once without watch mode:

```bash
vitest run
vitest run --coverage
```

### `vitest watch`

Explicitly start watch mode:

```bash
vitest watch
```

### `vitest related`

Run tests that import specific files (useful with lint-staged):

```bash
vitest related src/index.ts src/utils.ts --run
```

### `vitest bench`

Run only benchmark tests:

```bash
vitest bench
```

### `vitest list`

List all matching tests without running them:

```bash
vitest list                    # List test names
vitest list --json             # Output as JSON
vitest list --filesOnly        # List only test files
```

### `vitest init`

Initialize project setup:

```bash
vitest init browser            # Set up browser testing
```

### `vitest --list-tags`

List tags defined in config without running tests:

```bash
vitest --list-tags             # Human-readable list
vitest --list-tags=json        # JSON output
```

## Common Options

```bash
# Configuration
--config <path>           # Path to config file
--project <name>          # Run specific project

# Filtering
--testNamePattern, -t     # Run tests matching pattern
--tagsFilter <expr>       # Run tests by tag expression, e.g. "db && !flaky"
--changed                 # Run tests for changed files
--changed HEAD~1          # Tests for last commit changes
--dir <path>              # Limit test discovery to a directory

# Reporters
--reporter <name>         # default, verbose, tree, dot, json, html, junit, minimal, blob
--reporter=json --outputFile=report.json

# Coverage
--coverage                # Enable coverage
--coverage.provider v8    # Use v8 provider
--coverage.reporter text,html

# Execution
--shard <index>/<count>   # Split tests across machines
--bail <n>                # Stop after n failures
--retry <n>               # Retry failed tests n times
--shuffle                 # Randomize test order
--no-file-parallelism     # Run test files one at a time

# Watch mode
--no-watch                # Disable watch mode
--standalone              # Start without running (v4: runs matched files if a filter is passed)

# Environment
--environment <env>       # jsdom, happy-dom, node
--globals                 # Enable global APIs

# Debugging
--inspect                 # Enable Node inspector
--inspect-brk             # Break on start

# Output
--silent                  # Suppress console output
--no-color                # Disable colors
```

## Package.json Scripts

```json
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:ui": "vitest --ui",
    "coverage": "vitest run --coverage"
  }
}
```

## Sharding for CI

Split tests across multiple machines. The blob reporter writes to `.vitest/blob/` by default:

```bash
# Machine 1
vitest run --shard=1/3 --reporter=blob --outputFile=reports/blob-1.json

# Machine 2
vitest run --shard=2/3 --reporter=blob --outputFile=reports/blob-2.json

# Merge all blobs into a final report
vitest --merge-reports=reports --reporter=junit --reporter=default
```

## Watch Mode Keyboard Shortcuts

In watch mode, press:
- `a` - Run all tests
- `f` - Run only failed tests
- `u` - Update snapshots
- `p` - Filter by filename pattern
- `t` - Filter by test name pattern
- `q` - Quit

## Key Points

- Watch mode is default in dev, run mode in CI (when `process.env.CI` is set)
- Use `--run` flag to ensure single run (important for lint-staged)
- Both camelCase (`--testTimeout`) and kebab-case (`--test-timeout`) work
- Boolean options can be negated with `--no-` prefix
- Filter tests by tag with `--tagsFilter` (tags must be declared in config) — see [features-test-tags](features-test-tags.md)
- `--merge-reports` and `--reporter=blob` do not work in watch mode

<!-- 
Source references:
- https://vitest.dev/guide/cli.html
-->
references/core-config.md
---
name: vitest-configuration
description: Configure Vitest with vite.config.ts or vitest.config.ts
---

# Configuration

Vitest reads configuration from `vitest.config.ts` or `vite.config.ts`. It shares the same config format as Vite.

## Basic Setup

```ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    // test options
  },
})
```

## Using with Existing Vite Config

Add Vitest types reference and use the `test` property:

```ts
// vite.config.ts
/// <reference types="vitest/config" />
import { defineConfig } from 'vite'

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
  },
})
```

## Merging Configs

If you have separate config files, use `mergeConfig`:

```ts
// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'

export default mergeConfig(viteConfig, defineConfig({
  test: {
    environment: 'jsdom',
  },
}))
```

## Common Options

```ts
defineConfig({
  test: {
    // Enable global APIs (describe, it, expect) without imports
    globals: true,
    
    // Test environment: 'node', 'jsdom', 'happy-dom'
    environment: 'node',
    
    // Setup files to run before each test file
    setupFiles: ['./tests/setup.ts'],
    
    // Include patterns for test files
    include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
    
    // Exclude patterns
    exclude: ['**/node_modules/**', '**/dist/**'],
    
    // Limit test discovery to a directory (faster than broad excludes)
    dir: './src',

    // Test timeout in ms
    testTimeout: 5000,
    
    // Hook timeout in ms
    hookTimeout: 10000,
    
    // Coverage configuration (v4+: define `include`, no more `all`)
    coverage: {
      provider: 'v8', // or 'istanbul'
      reporter: ['text', 'html'],
      include: ['src/**/*.ts'],
    },
    
    // Run each file in an isolated module graph (threads/forks pools only)
    isolate: true,
    
    // Pool: 'forks' (default), 'threads', 'vmForks', 'vmThreads'
    pool: 'forks',
    
    // v4+: pool options are top-level (poolOptions was removed)
    maxWorkers: 4,
    fileParallelism: true,
    
    // Automatically clear mocks between tests
    clearMocks: true,
    
    // Restore spies created with vi.spyOn between tests
    restoreMocks: true,
    
    // Retry failed tests
    retry: 0,
    
    // Stop after first failure
    bail: 0,
  },
})
```

## v4/v5 Config Changes

- **Pool default is `forks`** (child processes), not `threads`.
- **`poolOptions` removed** — `maxThreads`/`maxForks` are now top-level `maxWorkers`; `singleThread`/`singleFork` become `maxWorkers: 1, isolate: false`; VM `memoryLimit` is `vmMemoryLimit`. `minWorkers` was removed.
- **`workspace` removed** — use [`projects`](advanced-projects.md). `vitest.workspace.ts` no longer supported.
- **`coverage.all` and `coverage.extensions` removed** — by default only covered files are reported; set `coverage.include` explicitly.
- **Simplified `exclude`** — only `node_modules`/`.git` excluded by default. Use `test.dir` to scope discovery, or spread `configDefaults.exclude`.
- **Config not looked up from parent dirs** — pass `--config` explicitly when running from a subdirectory.
- **`.vitest` artifact dir** — blob reports (`.vitest/blob/`), attachments (`.vitest/attachments/`), and HTML report now live under a single `.vitest/` directory; add one entry to `.gitignore`.
- `deps.optimizer.web` renamed to `deps.optimizer.client`; `deps.inline`/`deps.external` moved under `server.deps`.

## Conditional Configuration

Use `mode` or `process.env.VITEST` for test-specific config:

```ts
export default defineConfig(({ mode }) => ({
  plugins: mode === 'test' ? [] : [myPlugin()],
  test: {
    // test options
  },
}))
```

## Projects (Monorepos)

Run different configurations in the same Vitest process:

```ts
defineConfig({
  test: {
    projects: [
      'packages/*',
      {
        test: {
          name: 'unit',
          include: ['tests/unit/**/*.test.ts'],
          environment: 'node',
        },
      },
      {
        test: {
          name: 'integration',
          include: ['tests/integration/**/*.test.ts'],
          environment: 'jsdom',
        },
      },
    ],
  },
})
```

## Key Points

- Vitest uses Vite's transformation pipeline - same `resolve.alias`, plugins work
- `vitest.config.ts` takes priority over `vite.config.ts`
- Use `--config` flag to specify a custom config path (required from subdirectories in v5)
- `process.env.VITEST` is set to `true` when running tests
- Test config uses `test` property, rest is Vite config
- v4 requires **Vite >= 6** and **Node >= 20**; v5 is currently in beta

<!-- 
Source references:
- https://vitest.dev/guide/#configuring-vitest
- https://vitest.dev/config/
-->
references/core-describe.md
---
name: describe-api
description: describe/suite for grouping tests into logical blocks
---

# Describe API

Group related tests into suites for organization and shared setup.

## Basic Usage

```ts
import { describe, expect, test } from 'vitest'

describe('Math', () => {
  test('adds numbers', () => {
    expect(1 + 1).toBe(2)
  })

  test('subtracts numbers', () => {
    expect(3 - 1).toBe(2)
  })
})

// Alias: suite
import { suite } from 'vitest'
suite('equivalent to describe', () => {})
```

## Nested Suites

```ts
describe('User', () => {
  describe('when logged in', () => {
    test('shows dashboard', () => {})
    test('can update profile', () => {})
  })

  describe('when logged out', () => {
    test('shows login page', () => {})
  })
})
```

## Suite Options

```ts
// All tests inherit options
describe('slow tests', { timeout: 30_000 }, () => {
  test('test 1', () => {}) // 30s timeout
  test('test 2', () => {}) // 30s timeout
})
```

## Suite Modifiers

### Skip Suites

```ts
describe.skip('skipped suite', () => {
  test('wont run', () => {})
})

// Conditional
describe.skipIf(process.env.CI)('not in CI', () => {})
describe.runIf(!process.env.CI)('only local', () => {})
```

### Focus Suites

```ts
describe.only('only this suite runs', () => {
  test('runs', () => {})
})
```

### Todo Suites

```ts
describe.todo('implement later')
```

### Concurrent Suites

```ts
// All tests run in parallel
describe.concurrent('parallel tests', () => {
  test('test 1', async ({ expect }) => {})
  test('test 2', async ({ expect }) => {})
})
```

### Opt Out of Concurrency

`describe.sequential` was **removed in v5**. Use `{ concurrent: false }` to opt a suite out of inherited/global concurrency:

```ts
describe.concurrent('parallel', () => {
  test('concurrent 1', async () => {})

  describe('must be sequential', { concurrent: false }, () => {
    test('step 1', async () => {})
    test('step 2', async () => {})
  })
})
```

### Shuffle Tests

```ts
describe.shuffle('random order', () => {
  test('test 1', () => {})
  test('test 2', () => {})
  test('test 3', () => {})
})

// Or with option
describe('random', { shuffle: true }, () => {})
```

## Parameterized Suites

### describe.each

```ts
describe.each([
  { name: 'Chrome', version: 100 },
  { name: 'Firefox', version: 90 },
])('$name browser', ({ name, version }) => {
  test('has version', () => {
    expect(version).toBeGreaterThan(0)
  })
})
```

### describe.for

```ts
describe.for([
  ['Chrome', 100],
  ['Firefox', 90],
])('%s browser', ([name, version]) => {
  test('has version', () => {
    expect(version).toBeGreaterThan(0)
  })
})
```

## Hooks in Suites

```ts
describe('Database', () => {
  let db

  beforeAll(async () => {
    db = await createDb()
  })

  afterAll(async () => {
    await db.close()
  })

  beforeEach(async () => {
    await db.clear()
  })

  test('insert works', async () => {
    await db.insert({ name: 'test' })
    expect(await db.count()).toBe(1)
  })
})
```

## Modifier Combinations

Modifiers can be chained:

```ts
describe.skip.concurrent('skipped concurrent', () => {})
describe.only.shuffle('only and shuffled', () => {})
```

## Key Points

- Top-level tests belong to an implicit file suite
- Nested suites inherit parent's options (timeout, retry, concurrency, etc.)
- Hooks are scoped to their suite and nested suites
- Use `describe.concurrent` with context's `expect` for snapshots
- Shuffle order depends on `sequence.seed` config
- `describe.sequential` was removed in v5 — use `{ concurrent: false }`

<!-- 
Source references:
- https://vitest.dev/api/describe.html
-->
references/core-expect.md
---
name: expect-api
description: Assertions with matchers, asymmetric matchers, and custom matchers
---

# Expect API

Vitest uses Chai assertions with Jest-compatible API.

## Basic Assertions

```ts
import { expect, test } from 'vitest'

test('assertions', () => {
  // Equality
  expect(1 + 1).toBe(2)              // Strict equality (===)
  expect({ a: 1 }).toEqual({ a: 1 }) // Deep equality

  // Truthiness
  expect(true).toBeTruthy()
  expect(false).toBeFalsy()
  expect(null).toBeNull()
  expect(undefined).toBeUndefined()
  expect('value').toBeDefined()

  // Numbers
  expect(10).toBeGreaterThan(5)
  expect(10).toBeGreaterThanOrEqual(10)
  expect(5).toBeLessThan(10)
  expect(0.1 + 0.2).toBeCloseTo(0.3, 5)

  // Strings
  expect('hello world').toMatch(/world/)
  expect('hello').toContain('ell')

  // Arrays
  expect([1, 2, 3]).toContain(2)
  expect([{ a: 1 }]).toContainEqual({ a: 1 })
  expect([1, 2, 3]).toHaveLength(3)

  // Objects
  expect({ a: 1, b: 2 }).toHaveProperty('a')
  expect({ a: 1, b: 2 }).toHaveProperty('a', 1)
  expect({ a: { b: 1 } }).toHaveProperty('a.b', 1)
  expect({ a: 1 }).toMatchObject({ a: 1 })

  // Types
  expect('string').toBeTypeOf('string')
  expect(new Date()).toBeInstanceOf(Date)
})
```

## Negation

```ts
expect(1).not.toBe(2)
expect({ a: 1 }).not.toEqual({ a: 2 })
```

## Error Assertions

```ts
// Sync errors - wrap in function
expect(() => throwError()).toThrow()
expect(() => throwError()).toThrow('message')
expect(() => throwError()).toThrow(/pattern/)
expect(() => throwError()).toThrow(CustomError)

// Async errors - use rejects
await expect(asyncThrow()).rejects.toThrow('error')
```

## Promise Assertions

```ts
// Resolves
await expect(Promise.resolve(1)).resolves.toBe(1)
await expect(fetchData()).resolves.toEqual({ data: true })

// Rejects
await expect(Promise.reject('error')).rejects.toBe('error')
await expect(failingFetch()).rejects.toThrow()
```

## Spy/Mock Assertions

```ts
const fn = vi.fn()
fn('arg1', 'arg2')
fn('arg3')

expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledTimes(2)
expect(fn).toHaveBeenCalledWith('arg1', 'arg2')
expect(fn).toHaveBeenLastCalledWith('arg3')
expect(fn).toHaveBeenNthCalledWith(1, 'arg1', 'arg2')

expect(fn).toHaveReturned()
expect(fn).toHaveReturnedWith(value)

// v4 additions
expect(fn).toHaveBeenCalledExactlyOnceWith('arg1', 'arg2')
expect(fnA).toHaveBeenCalledBefore(fnB)
expect(fnA).toHaveBeenCalledAfter(fnB)
```

### Chai-Style Spy Assertions (4.1+)

Sinon-chai-compatible aliases, useful when migrating from Sinon:

```ts
expect(spy).to.have.been.called
expect(spy).to.have.been.calledOnce
expect(spy).to.have.been.calledWith('arg1', 'arg2')
expect(spy).to.have.been.calledOnceWith('arg')
```

### Conditional Mock Exhaustion (v5)

Assert every `vi.when` behavior was consumed:

```ts
const w = vi.when(spy).calledWith(1).thenReturnOnce('a')
spy(1)
expect(w).toHaveBeenExhausted()
```

## Asymmetric Matchers

Use inside `toEqual`, `toHaveBeenCalledWith`, etc:

```ts
expect({ id: 1, name: 'test' }).toEqual({
  id: expect.any(Number),
  name: expect.any(String),
})

expect({ a: 1, b: 2, c: 3 }).toEqual(
  expect.objectContaining({ a: 1 })
)

expect([1, 2, 3, 4]).toEqual(
  expect.arrayContaining([1, 3])
)

expect('hello world').toEqual(
  expect.stringContaining('world')
)

expect('hello world').toEqual(
  expect.stringMatching(/world$/)
)

expect({ value: null }).toEqual({
  value: expect.anything() // Matches anything except null/undefined
})

// Negate with expect.not
expect([1, 2]).toEqual(
  expect.not.arrayContaining([3])
)

// toBeOneOf - value matches any option (great for optional props)
expect(user).toEqual({
  name: expect.any(String),
  middleName: expect.toBeOneOf([expect.any(String), undefined]),
})

// schemaMatching (4.0+) - matches any Standard Schema (Zod, Valibot, ArkType)
import { z } from 'zod'
expect(payload).toEqual({
  email: expect.schemaMatching(z.string().email()),
})
expect(repo.save).toHaveBeenCalledWith(expect.schemaMatching(UserSchema))
```

## Soft Assertions

Prefer `expect.soft` for **non-critical assertions** — it marks the test failed but continues so all failures are reported together:

```ts
expect.soft(response.status).toBe(200) // non-critical, keeps going
expect.soft(response.headers.get('x-id')).toBeTruthy()
expect(response.body).toBeDefined() // critical: hard expect stops on failure
```

## Type-Narrowing Assertions (4.0+)

`expect.assert` throws at runtime **and** narrows the TypeScript type (unlike `toBeTruthy`/`toBeDefined`, which return `void`):

```ts
const user = cache.get('alice') // { id, name } | undefined
expect.assert(user)             // throws if undefined, narrows below
expect(user.name).toBe('Alice') // no `!`, no `as`

// Narrows typeof / instanceof too
expect.assert(typeof input === 'string')
input.toUpperCase()

// Chai assert helpers via the same namespace
expect.assert.isDefined(maybeUser)
expect.assert.instanceOf(error, MyError)
```

## Poll Assertions

Retry until passes:

```ts
await expect.poll(() => fetchStatus()).toBe('ready')

await expect.poll(
  () => document.querySelector('.element'),
  { interval: 100, timeout: 5000 }
).toBeTruthy()
```

## Assertion Count

```ts
test('async assertions', async () => {
  expect.assertions(2) // Exactly 2 assertions must run
  
  await doAsync((data) => {
    expect(data).toBeDefined()
    expect(data.id).toBe(1)
  })
})

test('at least one', () => {
  expect.hasAssertions() // At least 1 assertion must run
})
```

## Extending Matchers

```ts
expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling
    return {
      pass,
      message: () => 
        `expected ${received} to be within range ${floor} - ${ceiling}`,
    }
  },
})

test('custom matcher', () => {
  expect(100).toBeWithinRange(90, 110)
})
```

## Snapshot Assertions

```ts
expect(data).toMatchSnapshot()
expect(data).toMatchInlineSnapshot(`{ "id": 1 }`)
await expect(result).toMatchFileSnapshot('./expected.json')

expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot()
```

## Key Points

- Use `toBe` for primitives, `toEqual` for objects/arrays
- `toStrictEqual` checks undefined properties and array sparseness
- Always `await` async assertions (`resolves`, `rejects`, `poll`)
- Use context's `expect` in concurrent tests for correct tracking
- `toThrow` requires wrapping sync code in a function
- Use `expect.soft` for non-critical assertions; reserve hard `expect` for must-pass conditions
- Use `expect.assert` (not `toBeTruthy`) when you also need TypeScript narrowing

<!-- 
Source references:
- https://vitest.dev/api/expect.html
- https://vitest.dev/guide/recipes/type-narrowing
- https://vitest.dev/guide/recipes/schema-matching
-->
references/core-hooks.md
---
name: lifecycle-hooks
description: beforeEach, afterEach, beforeAll, afterAll, and around hooks
---

# Lifecycle Hooks

## Basic Hooks

```ts
import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest'

beforeAll(async () => {
  // Runs once before all tests in file/suite
  await setupDatabase()
})

afterAll(async () => {
  // Runs once after all tests in file/suite
  await teardownDatabase()
})

beforeEach(async () => {
  // Runs before each test
  await clearTestData()
})

afterEach(async () => {
  // Runs after each test
  await cleanupMocks()
})
```

## Cleanup Return Pattern

Return cleanup function from `before*` hooks:

```ts
beforeAll(async () => {
  const server = await startServer()
  
  // Returned function runs as afterAll
  return async () => {
    await server.close()
  }
})

beforeEach(async () => {
  const connection = await connect()
  
  // Runs as afterEach
  return () => connection.close()
})
```

## Scoped Hooks

Hooks apply to current suite and nested suites:

```ts
describe('outer', () => {
  beforeEach(() => console.log('outer before'))
  
  test('test 1', () => {}) // outer before → test
  
  describe('inner', () => {
    beforeEach(() => console.log('inner before'))
    
    test('test 2', () => {}) // outer before → inner before → test
  })
})
```

## Hook Timeout

```ts
beforeAll(async () => {
  await slowSetup()
}, 30_000) // 30 second timeout
```

## Around Hooks

Wrap tests with setup/teardown context:

```ts
import { aroundEach, test } from 'vitest'

// Wrap each test in database transaction
aroundEach(async (runTest) => {
  await db.beginTransaction()
  await runTest() // Must be called!
  await db.rollback()
})

test('insert user', async () => {
  await db.insert({ name: 'Alice' })
  // Automatically rolled back after test
})
```

### aroundAll

Wrap entire suite:

```ts
import { aroundAll, test } from 'vitest'

aroundAll(async (runSuite) => {
  console.log('before all tests')
  await runSuite() // Must be called!
  console.log('after all tests')
})
```

### Multiple Around Hooks

Nested like onion layers:

```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')
})

// Order: outer before → inner before → test → inner after → outer after
```

## Test Hooks

Inside test body:

```ts
import { onTestFailed, onTestFinished, test } from 'vitest'

test('with cleanup', () => {
  const db = connect()
  
  // Runs after test finishes (pass or fail)
  onTestFinished(() => db.close())
  
  // Only runs if test fails
  onTestFailed(({ task }) => {
    console.log('Failed:', task.result?.errors)
  })
  
  db.query('SELECT * FROM users')
})
```

### Reusable Cleanup Pattern

```ts
function useTestDb() {
  const db = connect()
  onTestFinished(() => db.close())
  return db
}

test('query users', () => {
  const db = useTestDb()
  expect(db.query('SELECT * FROM users')).toBeDefined()
})

test('query orders', () => {
  const db = useTestDb() // Fresh connection, auto-closed
  expect(db.query('SELECT * FROM orders')).toBeDefined()
})
```

## Concurrent Test Hooks

For concurrent tests, use context's hooks:

```ts
test.concurrent('concurrent', ({ onTestFinished }) => {
  const resource = allocate()
  onTestFinished(() => resource.release())
})
```

## Extended Test Hooks

With `test.extend`, hooks are type-aware and must be called on the extended `test`:

```ts
const test = base
  .extend('db', { scope: 'file' }, async ({}, { onCleanup }) => {
    const db = await createDb()
    onCleanup(() => db.close())
    return db
  })

// Test-level hooks see fixtures
test.beforeEach(({ db }) => db.seed())
test.afterEach(({ db }) => db.clear())

// Suite-level hooks (4.1+) can access file/worker-scoped fixtures
test.beforeAll(({ db }) => db.createUsers())
test.aroundAll(async (runSuite, { db }) => db.transaction(runSuite))
```

Suite-level hooks (`beforeAll`/`afterAll`/`aroundAll`) only see **file/worker-scoped** fixtures, and the **global** `beforeAll`/`afterAll` cannot access custom fixtures — use `test.beforeAll` etc.

## Hook Execution Order

Default order (stack):
1. `beforeAll` (in order)
2. `beforeEach` (in order)
3. Test
4. `afterEach` (reverse order)
5. `afterAll` (reverse order)

Configure with `sequence.hooks`:

```ts
defineConfig({
  test: {
    sequence: {
      hooks: 'list', // 'stack' (default), 'list', 'parallel'
    },
  },
})
```

## Key Points

- Hooks are not called during type checking
- Return cleanup function from `before*` to avoid `after*` duplication
- `aroundEach`/`aroundAll` must call `runTest()`/`runSuite()`
- `onTestFinished` always runs, even if test fails
- Use context hooks for concurrent tests

<!-- 
Source references:
- https://vitest.dev/api/hooks.html
-->
references/core-test-api.md
---
name: test-api
description: test/it function for defining tests with modifiers
---

# Test API

## Basic Test

```ts
import { expect, test } from 'vitest'

test('adds numbers', () => {
  expect(1 + 1).toBe(2)
})

// Alias: it
import { it } from 'vitest'

it('works the same', () => {
  expect(true).toBe(true)
})
```

## Async Tests

```ts
test('async test', async () => {
  const result = await fetchData()
  expect(result).toBeDefined()
})

// Promises are automatically awaited
test('returns promise', () => {
  return fetchData().then(result => {
    expect(result).toBeDefined()
  })
})
```

## Test Options

```ts
// Timeout (default: 5000ms)
test('slow test', async () => {
  // ...
}, 10_000)

// Or with options object
test('with options', { timeout: 10_000, retry: 2 }, async () => {
  // ...
})
```

## Test Modifiers

### Skip Tests

```ts
test.skip('skipped test', () => {
  // Won't run
})

// Conditional skip
test.skipIf(process.env.CI)('not in CI', () => {})
test.runIf(process.env.CI)('only in CI', () => {})

// Dynamic skip via context
test('dynamic skip', ({ skip }) => {
  skip(someCondition, 'reason')
  // ...
})
```

### Focus Tests

```ts
test.only('only this runs', () => {
  // Other tests in file are skipped
})
```

### Todo Tests

```ts
test.todo('implement later')

test.todo('with body', () => {
  // Not run, shows in report
})
```

### Failing Tests

```ts
test.fails('expected to fail', () => {
  expect(1).toBe(2) // Test passes because assertion fails
})
```

### Concurrent Tests

```ts
// Run tests in parallel
test.concurrent('test 1', async ({ expect }) => {
  // Use context.expect for concurrent tests
  expect(await fetch1()).toBe('result')
})

test.concurrent('test 2', async ({ expect }) => {
  expect(await fetch2()).toBe('result')
})
```

### Opt Out of Concurrency

`test.sequential` was **removed in v5**. Use `concurrent: false` to opt a test out of inherited or globally configured concurrency:

```ts
test('must run alone', { concurrent: false }, async () => {})
```

## Parameterized Tests

### test.each

```ts
test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('add(%i, %i) = %i', (a, b, expected) => {
  expect(a + b).toBe(expected)
})

// With objects
test.each([
  { a: 1, b: 1, expected: 2 },
  { a: 1, b: 2, expected: 3 },
])('add($a, $b) = $expected', ({ a, b, expected }) => {
  expect(a + b).toBe(expected)
})

// Template literal
test.each`
  a    | b    | expected
  ${1} | ${1} | ${2}
  ${1} | ${2} | ${3}
`('add($a, $b) = $expected', ({ a, b, expected }) => {
  expect(a + b).toBe(expected)
})
```

### test.for

Preferred over `.each` - doesn't spread arrays:

```ts
test.for([
  [1, 1, 2],
  [1, 2, 3],
])('add(%i, %i) = %i', ([a, b, expected], { expect }) => {
  // Second arg is TestContext
  expect(a + b).toBe(expected)
})
```

## Test Context

First argument provides context utilities:

```ts
test('with context', ({ expect, skip, task, signal, annotate }) => {
  console.log(task.name)        // Test metadata
  skip(someCondition, 'reason') // Skip dynamically
  expect(1).toBe(1)             // Context-bound expect
})

// signal (3.2+): AbortSignal aborted on timeout/cancel/bail
test('aborts on timeout', async ({ signal }) => {
  await fetch('/resource', { signal })
}, 2000)

// annotate (3.2+): attach notes shown by the reporter
test('annotated', async ({ annotate }) => {
  await annotate('see issue #123', 'issues')
})
```

## Custom Test with Fixtures

Prefer the **builder pattern** (4.1+) for automatic type inference:

```ts
import { test as base } from 'vitest'

const test = base
  .extend('db', async ({}, { onCleanup }) => {
    const db = await createDb()
    onCleanup(() => db.close()) // runs after the test/scope
    return db
  })

test('query', async ({ db }) => {
  const users = await db.query('SELECT * FROM users')
  expect(users).toBeDefined()
})
```

See [features-context](features-context.md) for fixture scopes, `test.override`, and the Playwright-compatible object syntax.

## Retry Configuration

```ts
test('flaky test', { retry: 3 }, async () => {
  // Retries up to 3 times on failure
})

// Advanced retry options
test('with delay', {
  retry: {
    count: 3,
    delay: 1000,
    condition: /timeout/i, // Only retry on timeout errors
  },
}, async () => {})
```

## Tags

Tags must be declared in config first, then applied to tests (4.1+):

```ts
test('database test', { tags: ['db', 'slow'] }, async () => {})

// Run with a tag expression:
// vitest --tagsFilter "db && !flaky"
```

See [features-test-tags](features-test-tags.md) for defining tags and filter syntax.

## Benchmarks (v5)

`bench` is no longer a top-level import — it is a [test-context fixture](features-benchmarking.md) used inside `test()`:

```ts
// file must match benchmark.include (e.g. *.bench.ts)
test('sort', async ({ bench }) => {
  await bench('Array.sort', () => [3, 1, 2].sort()).run()
})
```

## Key Points

- Pass options as the **second argument**; the 3rd-arg options object was removed in v4 (a trailing timeout number is still allowed)
- Tests with no body are marked as `todo`
- `test.only` throws in CI unless `allowOnly: true`
- Use context's `expect` for concurrent tests and snapshots
- Function name is used as test name if passed as first arg
- `test.sequential` was removed in v5 — use `{ concurrent: false }`

<!-- 
Source references:
- https://vitest.dev/api/test.html
-->
references/features-benchmarking.md
---
name: benchmarking
description: Write benchmarks with the v5 bench test-context fixture (Tinybench)
---

# Benchmarking (v5)

In v5 the benchmark API was rewritten: `bench` is **no longer a top-level import**. It is a [test-context fixture](features-context.md) used inside a regular `test()`, available only in files matched by `benchmark.include` (default `**/*.{bench,benchmark}.?(c|m)[jt]s?(x)`). Benchmarks are powered by [Tinybench](https://github.com/tinylibs/tinybench).

## Defining & Running

```ts
import { expect, test } from 'vitest'

test('parse performance', async ({ bench }) => {
  // bench() registers; .run() executes and returns the result
  const result = await bench('parse', () => {
    const data = JSON.parse('{"key":"value"}')
    use(data) // consume the result — engines may eliminate dead code
  }).run()

  expect(result.throughput.mean).toBeGreaterThan(10_000)
})
```

Run benchmarks:

```bash
vitest bench           # only benchmarks (implicitly enables them)
vitest bench parser    # filter by filename
vitest bench -t JSON   # filter by test name
```

Set `benchmark: { enabled: true }` to run them alongside regular tests in a separate isolated group.

## Comparing Implementations

```ts
test('compare parsers', async ({ bench }) => {
  const result = await bench.compare(
    bench('JSON.parse', () => { JSON.parse(input) }),
    bench('custom', { beforeEach: () => reset() }, () => { customParse(input) }),
    { iterations: 100, time: 1000 }, // shared Tinybench options (last arg)
  )

  // Assertion matchers (delta avoids flaky failures)
  expect(result.get('JSON.parse')).toBeFasterThan(result.get('custom'), { delta: 0.1 })
  expect(result.get('custom')).toBeSlowerThan(result.get('JSON.parse'))
})
```

`bench.compare` interleaves iterations to reduce environmental bias and prints a comparison table after the test.

## Storing & Replaying Baselines

```ts
test('compare against baseline', async ({ bench }) => {
  await bench.compare(
    bench('current', { writeResult: './benchmarks/parse.json' }, () => parse(input)),
    bench.from('previous', './benchmarks/parse.json'),       // reads a stored result, no run
    bench.from('remote', () => fetch(url).then(r => r.json())),
  )
})
```

- `writeResult` overwrites the JSON file on every successful run (no skip-when-cached).
- `bench.from(name, source)` reads a stored result without invoking any function.
- For multi-project workspaces, pass `{ perProject: true }` and use `${projectName}` in `writeResult` paths to collect a cross-project comparison table.

## Stability Notes

- Benchmark files run sequentially and never in parallel; `retry` and the `delta` option reduce flakiness.
- Consume the result inside the bench fn — JS engines eliminate side-effect-free code.
- In Node mode every imported binding goes through Vite's module-runner getter; store hot references locally (`const _parse = parse`), benchmark the built package, or disable `experimental.viteModuleRunner` for the bench project.

## v5 Migration

- `bench` top-level import → `({ bench })` from the test context
- `bench.skip/only/todo` removed → use `test.skip/only/todo` on the surrounding test
- `benchmark.reporters`/`outputFile`/`compare`/`outputJson` and `--compare`/`--outputJson` removed → use `--reporter=json --outputFile` (JSON now has a `benchmarks` field)

## Key Points

- Benchmarks live in `*.bench.ts` files and run inside `test()` via `{ bench }`
- Use `bench.compare` + `toBeFasterThan`/`toBeSlowerThan` (with `delta`) for relative perf
- Persist baselines with `writeResult` and replay with `bench.from`

<!--
Source references:
- https://vitest.dev/guide/benchmarking
- https://vitest.dev/guide/test-context#bench
-->
references/features-concurrency.md
---
name: concurrency-parallelism
description: Concurrent tests, parallel execution, and sharding
---

# Concurrency & Parallelism

## File Parallelism

By default, Vitest runs test files in parallel across workers:

```ts
defineConfig({
  test: {
    // Run files in parallel (default: true)
    fileParallelism: true,
    
    // Max concurrent workers (v4: replaces maxThreads/maxForks; minWorkers removed)
    maxWorkers: 4,
    
    // Pool type: 'forks' (default), 'threads', 'vmForks', 'vmThreads'
    pool: 'forks',
  },
})
```

> **v4 pool rework:** `poolOptions` was removed — all pool settings are now top-level. `singleThread`/`singleFork` become `maxWorkers: 1, isolate: false`. VM `memoryLimit` is `vmMemoryLimit`. These can now be set **per project**.

## Concurrent Tests

Run tests within a file in parallel:

```ts
// Individual concurrent tests
test.concurrent('test 1', async ({ expect }) => {
  expect(await fetch1()).toBe('result')
})

test.concurrent('test 2', async ({ expect }) => {
  expect(await fetch2()).toBe('result')
})

// All tests in suite concurrent
describe.concurrent('parallel suite', () => {
  test('test 1', async ({ expect }) => {})
  test('test 2', async ({ expect }) => {})
})
```

**Important:** Use `{ expect }` from context for concurrent tests.

## Opting Out of Concurrency

`test.sequential`/`describe.sequential` were **removed in v5**. Use `{ concurrent: false }`:

```ts
describe.concurrent('mostly parallel', () => {
  test('parallel 1', async () => {})

  // Opt this test out of inherited concurrency
  test('must run alone', { concurrent: false }, async () => {})
})

// Or an entire suite
describe('sequential suite', { concurrent: false }, () => {
  test('first', () => {})
  test('second', () => {})
})
```

Set `sequence.concurrent: true` to make all tests concurrent by default.

## Max Concurrency

Limit concurrent tests:

```ts
defineConfig({
  test: {
    maxConcurrency: 5, // Max concurrent tests per file
  },
})
```

## Isolation

Each file runs in isolated environment by default:

```ts
defineConfig({
  test: {
    // Disable isolation for faster runs (less safe)
    isolate: false,
  },
})
```

## Sharding

Split tests across machines:

```bash
# Machine 1
vitest run --shard=1/3

# Machine 2
vitest run --shard=2/3

# Machine 3
vitest run --shard=3/3
```

### CI Example (GitHub Actions)

```yaml
jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3]
    steps:
      - run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob
      
  merge:
    needs: test
    steps:
      - run: vitest --merge-reports --reporter=junit
```

### Merge Reports

```bash
# Each shard outputs blob
vitest run --shard=1/3 --reporter=blob --coverage
vitest run --shard=2/3 --reporter=blob --coverage

# Merge all blobs
vitest --merge-reports --reporter=json --coverage
```

## Test Sequence

Control test order:

```ts
defineConfig({
  test: {
    sequence: {
      // Run tests in random order
      shuffle: true,
      
      // Seed for reproducible shuffle
      seed: 12345,
      
      // Hook execution order
      hooks: 'stack', // 'stack', 'list', 'parallel'
      
      // All tests concurrent by default
      concurrent: true,

      // Order projects/groups run in (3.2+); lower runs first
      groupOrder: 0,
    },
  },
})
```

## Shuffle Tests

Randomize to catch hidden dependencies:

```ts
// Via CLI
vitest --shuffle

// Per suite
describe.shuffle('random order', () => {
  test('test 1', () => {})
  test('test 2', () => {})
  test('test 3', () => {})
})
```

## Pools (v4)

`poolOptions` was removed; pool settings are now top-level and can be set per project:

```ts
defineConfig({
  test: {
    pool: 'forks',     // 'forks' (default) | 'threads' | 'vmForks' | 'vmThreads'
    maxWorkers: 8,
    isolate: true,     // threads/forks only; vm* pools are always isolated
    vmMemoryLimit: '512MB',
  },
})
```

For per-project parallelism/isolation settings, see [advanced-projects](advanced-projects.md).

## Bail on Failure

Stop after first failure:

```bash
vitest --bail 1    # Stop after 1 failure
vitest --bail      # Stop on first failure (same as --bail 1)
```

## Key Points

- Files run in parallel by default (`pool: 'forks'`); tests within a file run sequentially unless `.concurrent`
- `concurrent` only speeds up tests that **await** (I/O, timers); pure sync tests still block the thread
- Always use context's `expect` in concurrent tests
- Use `{ concurrent: false }` (not `.sequential`) to opt out
- `maxWorkers` (not `maxThreads`/`maxForks`); `poolOptions` removed in v4
- Sharding splits tests across CI machines; `--merge-reports` combines blob results

<!-- 
Source references:
- https://vitest.dev/guide/parallelism.html
- https://vitest.dev/guide/improving-performance.html
-->
references/features-context.md
---
name: test-context-fixtures
description: Test context, custom fixtures with test.extend (builder pattern), scopes, and test.override
---

# Test Context & Fixtures

## Built-in Context

Every test receives context as its first argument:

```ts
test('context', ({ task, expect, skip, signal, annotate }) => {
  console.log(task.name)        // Test metadata (readonly)
  expect(1).toBe(1)             // Expect bound to this test
  skip(condition, 'reason')     // Skip dynamically
})
```

Properties:
- `task` — test metadata (name, file, etc.)
- `expect` — expect bound to this test (required for concurrent snapshot tests)
- `skip(condition?, message?)` — skip the test
- `signal` (3.2+) — `AbortSignal` aborted on timeout/cancel/bail
- `annotate(message, type?, attachment?)` (3.2+) — attach reporter annotations
- `onTestFinished(fn)` / `onTestFailed(fn)` — per-test cleanup/handlers
- `bench` (v5) — benchmark fixture (only in `*.bench.ts` files)

## Custom Fixtures — Builder Pattern (4.1+, recommended)

`.extend(name, options?, fixture)` infers types automatically. Use `onCleanup` for teardown:

```ts
import { test as baseTest } from 'vitest'

export const test = baseTest
  // Plain value — type inferred as { port: number; host: string }
  .extend('config', { port: 3000, host: 'localhost' })
  // Function fixture — can read previously defined fixtures
  .extend('server', async ({ config }, { onCleanup }) => {
    const server = await startServer(config)
    onCleanup(() => server.close()) // runs after test/scope ends
    return server
  })

test('uses server', ({ config, server }) => {
  expect(server.url).toContain(String(config.port))
})
```

> `onCleanup` can be called **once per fixture**. For multiple resources, split into separate fixtures.

### Fixture Options

```ts
const test = baseTest
  .extend('metrics', { auto: true }, () => new Metrics())       // runs for every test
  .extend('config', { scope: 'worker' }, () => loadConfig())    // once per worker
  .extend('db', { scope: 'file' }, async ({ config }, { onCleanup }) => {
    const db = await createDatabase(config)
    onCleanup(() => db.close())
    return db
  })
  .extend('baseUrl', { injected: true }, () => 'http://localhost:3000') // overridable via config
```

## Object Syntax (Playwright-compatible)

Uses the `use()` callback; types must be declared manually:

```ts
const test = baseTest.extend<{ page: Page; baseUrl: string }>({
  page: async ({}, use) => {
    const page = await browser.newPage()
    await use(page)        // test runs here
    await page.close()     // cleanup after
  },
  baseUrl: 'http://localhost:3000',
})
```

Tuple form sets options: `fixture: [async ({}, use) => {…}, { scope: 'file' }]`.

## Fixture Scopes (3.2+)

| Scope | Lifetime | Can access |
|-------|----------|------------|
| `test` (default) | each test | worker + file + test fixtures + built-in context |
| `file` | once per file | worker + file fixtures |
| `worker` | once per worker process | only worker fixtures |

Only `test`-scoped fixtures can access the built-in context (`task`, `expect`, …). In file/worker fixtures use `expect.getState().testPath` for the file path. By default every file is its own worker, so `file` and `worker` behave the same unless [isolation is disabled](features-concurrency.md).

## Injected Fixtures (per-project values)

```ts
// fixtures.ts
const test = baseTest.extend('url', { injected: true }, '/default')

// vitest.config.ts — provide per project
defineConfig({
  test: {
    projects: [
      { test: { name: 'prod', provide: { url: 'https://prod' } } },
    ],
  },
})
```

Read raw provided values without fixtures via `import { inject } from 'vitest'`.

## Overriding Fixtures — test.override (4.1+)

`test.override` replaces fixture values for a suite and its children (replaces the deprecated `test.scoped`):

```ts
describe('production', () => {
  test
    .override('config', { port: 8080, host: 'api.example.com' })
    .override('debug', false)        // chainable

  test('uses prod config', ({ server }) => {
    expect(server.url).toBe('http://api.example.com:8080')
  })
})

// Function override (reads other fixtures) with cleanup
test.override('db', async ({ config }, { onCleanup }) => {
  const db = await createTestDatabase(config)
  onCleanup(() => db.drop())
  return db
})
```

You cannot introduce new fixtures or change `scope`/`auto` via `override`; use `test.extend` for new fixtures.

## Composing & Hooks

Extend an already-extended test, and use type-aware hooks on the extended `test`:

```ts
import { test as dbTest } from './db-test'

export const test = dbTest.extend('user', ({ db }) => db.createUser())

test.beforeEach(({ db }) => db.seed())            // sees fixtures
test.beforeAll(({ db }) => db.migrate())          // file/worker fixtures only (4.1+)
test.aroundAll(async (run, { db }) => db.tx(run))
```

## Key Points

- Prefer the **builder pattern** — types are inferred, cleanup via `onCleanup`
- Fixtures are lazy — only initialized when destructured
- Always destructure `{ db }` (not `context.db`)
- Use `{ scope: 'file' | 'worker' }` for expensive shared resources
- Use `test.override` (not `test.scoped`) to vary fixture values per suite
- Use `{ injected: true }` + project `provide` for per-project values

<!-- 
Source references:
- https://vitest.dev/guide/test-context.html
-->
references/features-coverage.md
---
name: code-coverage
description: Code coverage with V8 or Istanbul providers
---

# Code Coverage

## Setup

```bash
# Run tests with coverage
vitest run --coverage
```

## Configuration

```ts
// vitest.config.ts
defineConfig({
  test: {
    coverage: {
      // Provider: 'v8' (default, faster) or 'istanbul' (more compatible)
      provider: 'v8',
      
      // Enable coverage
      enabled: true,
      
      // Reporters
      reporter: ['text', 'json', 'html'],
      
      // v4: define `include` to report uncovered files too.
      // Without it, only files loaded during the run are reported.
      include: ['src/**/*.{ts,tsx}'],
      
      // Exclusion is applied to files matched by `include`
      exclude: [
        '**/*.d.ts',
        '**/*.test.ts',
      ],
      
      // Thresholds
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80,
      },
    },
  },
})
```

## Providers

### V8 (Default)

```bash
npm i -D @vitest/coverage-v8
```

- Faster, no pre-instrumentation
- Uses V8's native coverage
- v4 uses **AST-based remapping** (as accurate as Istanbul); expect coverage numbers to shift when upgrading from v3
- Recommended for most projects

### Istanbul

```bash
npm i -D @vitest/coverage-istanbul
```

- Pre-instruments code
- Works in any JS runtime
- More overhead but widely compatible

## Reporters

```ts
coverage: {
  reporter: [
    'text',           // Terminal output
    'text-summary',   // Summary only
    'json',           // JSON file
    'html',           // HTML report
    'lcov',           // For CI tools
    'cobertura',      // XML format
  ],
  reportsDirectory: './coverage',
}
```

## Thresholds

Fail tests if coverage is below threshold:

```ts
coverage: {
  thresholds: {
    // Global thresholds
    lines: 80,
    functions: 75,
    branches: 70,
    statements: 80,
    
    // Per-file thresholds
    perFile: true,
    
    // Auto-update thresholds (for gradual improvement)
    autoUpdate: true,

    // v5: glob thresholds no longer inherit top-level `perFile` — set it per glob
    'src/utils/**': { lines: 80, perFile: true },
  },
}
```

## Ignoring Code

### V8

```ts
/* v8 ignore next -- @preserve */
function ignored() {
  return 'not covered'
}

/* v8 ignore start -- @preserve */
// All code here ignored
/* v8 ignore stop -- @preserve */
```

### Istanbul

```ts
/* istanbul ignore next -- @preserve */
function ignored() {}

/* istanbul ignore if -- @preserve */
if (condition) {
  // ignored
}
```

Note: `@preserve` keeps comments through esbuild.

## Package.json Scripts

```json
{
  "scripts": {
    "test": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:coverage:watch": "vitest --coverage"
  }
}
```

## Vitest UI Coverage

Enable HTML coverage in Vitest UI:

```ts
coverage: {
  enabled: true,
  reporter: ['text', 'html'],
}
```

Run with `vitest --ui` to view coverage visually.

## CI Integration

```yaml
# GitHub Actions
- name: Run tests with coverage
  run: npm run test:coverage

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage/lcov.info
```

## Coverage with Sharding

Merge coverage from sharded runs (blobs default to `.vitest/blob/`):

```bash
vitest run --shard=1/3 --coverage --reporter=blob
vitest run --shard=2/3 --coverage --reporter=blob
vitest run --shard=3/3 --coverage --reporter=blob

vitest --merge-reports --coverage --reporter=json
```

## v4 Changes

- **`coverage.all` and `coverage.extensions` removed** — only covered files are reported unless `coverage.include` is set.
- **`coverage.ignoreEmptyLines` removed**; lines without runtime code are no longer counted.
- **`coverage.experimentalAstAwareRemapping` removed** — AST remapping is the default and only mode for V8.
- Programmatic coverage APIs moved from `vitest/coverage` to `vitest/node`.

## Key Points

- V8 is faster, Istanbul is more compatible
- Use `--coverage` flag or `coverage.enabled: true`
- Define `coverage.include` to report uncovered source files
- Set thresholds to enforce minimum coverage
- Use `@preserve` comment to keep ignore hints (e.g. `/* v8 ignore next -- @preserve */`)

<!-- 
Source references:
- https://vitest.dev/guide/coverage.html
-->
references/features-filtering.md
---
name: test-filtering
description: Filter tests by name, file patterns, and tags
---

# Test Filtering

## CLI Filtering

### By File Path

```bash
# Run files containing "user"
vitest user

# Multiple patterns
vitest user auth

# Specific file
vitest src/user.test.ts

# By line number
vitest src/user.test.ts:25
```

### By Test Name

```bash
# Tests matching pattern
vitest -t "login"
vitest --testNamePattern "should.*work"

# Regex patterns
vitest -t "/user|auth/"
```

## Changed Files

```bash
# Uncommitted changes
vitest --changed

# Since specific commit
vitest --changed HEAD~1
vitest --changed abc123

# Since branch
vitest --changed origin/main
```

## Related Files

Run tests that import specific files:

```bash
vitest related src/utils.ts src/api.ts --run
```

Useful with lint-staged:

```js
// .lintstagedrc.js
export default {
  '*.{ts,tsx}': 'vitest related --run',
}
```

## Focus Tests (.only)

```ts
test.only('only this runs', () => {})

describe.only('only this suite', () => {
  test('runs', () => {})
})
```

In CI, `.only` throws error unless configured:

```ts
defineConfig({
  test: {
    allowOnly: true, // Allow .only in CI
  },
})
```

## Skip Tests

```ts
test.skip('skipped', () => {})

// Conditional
test.skipIf(process.env.CI)('not in CI', () => {})
test.runIf(!process.env.CI)('local only', () => {})

// Dynamic skip
test('dynamic', ({ skip }) => {
  skip(someCondition, 'reason')
})
```

## Tags

Tags must be declared in config, then applied to tests/suites and filtered with a tag expression:

```ts
// vitest.config.ts
defineConfig({
  test: {
    tags: [{ name: 'db' }, { name: 'slow' }, { name: 'flaky' }],
  },
})

// test file
test('database test', { tags: ['db'] }, () => {})
```

```bash
vitest --tagsFilter "db && !flaky"
vitest --tagsFilter "unit || e2e"
vitest --list-tags            # show defined tags
```

Full syntax, priority, and per-tag options: see [features-test-tags](features-test-tags.md).

## Include/Exclude Patterns

```ts
defineConfig({
  test: {
    // Test file patterns
    include: ['**/*.{test,spec}.{ts,tsx}'],
    
    // Exclude patterns
    exclude: [
      '**/node_modules/**',
      '**/e2e/**',
      '**/*.skip.test.ts',
    ],
    
    // Include source for in-source testing
    includeSource: ['src/**/*.ts'],

    // Scope discovery to a directory (faster than broad excludes)
    dir: './src',
  },
})
```

> v4 simplified default `exclude` to only `node_modules`/`.git`. Prefer `test.dir` to limit where tests are found; spread `configDefaults.exclude` to restore the old excludes.

## Watch Mode Filtering

In watch mode, press:
- `p` - Filter by filename pattern
- `t` - Filter by test name pattern
- `a` - Run all tests
- `f` - Run only failed tests

## Projects Filtering

Run specific project:

```bash
vitest --project unit
vitest --project integration --project e2e
```

## Environment-based Filtering

```ts
const isDev = process.env.NODE_ENV === 'development'
const isCI = process.env.CI

describe.skipIf(isCI)('local only tests', () => {})
describe.runIf(isDev)('dev tests', () => {})
```

## Combining Filters

```bash
# File pattern + test name + changed
vitest user -t "login" --changed

# Related files + run mode
vitest related src/auth.ts --run
```

## List Tests Without Running

```bash
vitest list                 # Show all test names
vitest list -t "user"       # Filter by name
vitest list --filesOnly     # Show only file paths
vitest list --json          # JSON output
```

## Key Points

- Use `-t` for test name pattern filtering
- `--changed` runs only tests affected by changes
- `--related` runs tests importing specific files
- Tags provide semantic test grouping
- Use `.only` for debugging, but configure CI to reject it
- Watch mode has interactive filtering

<!-- 
Source references:
- https://vitest.dev/guide/filtering.html
- https://vitest.dev/guide/cli.html
-->
references/features-mocking.md
---
name: mocking
description: Mock functions, modules, timers, and dates with vi utilities
---

# Mocking

## Mock Functions

```ts
import { expect, vi } from 'vitest'

// Create mock function
const fn = vi.fn()
fn('hello')

expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledWith('hello')

// With implementation
const add = vi.fn((a, b) => a + b)
expect(add(1, 2)).toBe(3)

// Mock return values
fn.mockReturnValue(42)
fn.mockReturnValueOnce(1).mockReturnValueOnce(2)
fn.mockResolvedValue({ data: true })
fn.mockRejectedValue(new Error('fail'))

// Mock implementation
fn.mockImplementation((x) => x * 2)
fn.mockImplementationOnce(() => 'first call')
```

## Spying on Objects

```ts
const cart = {
  getTotal: () => 100,
}

const spy = vi.spyOn(cart, 'getTotal')
cart.getTotal()

expect(spy).toHaveBeenCalled()

// Mock implementation
spy.mockReturnValue(200)
expect(cart.getTotal()).toBe(200)

// Restore original
spy.mockRestore()
```

Since v4, `vi.spyOn`/`vi.fn` can mock **constructors** — provide a `function` or `class` implementation (an arrow function throws "not a constructor"):

```ts
const Spy = vi.spyOn(cart, 'Apples').mockImplementation(class {
  getApples() { return 0 }
})
const instance = new Spy()
```

## Conditional Mocking with vi.when (v5)

Define per-argument behaviors without writing `if`/`switch` in `mockImplementation`:

```ts
vi.when(db.findById)
  .calledWith(1)
  .thenResolve({ id: 1, name: 'Ella' })
  .calledWith(2)
  .thenResolve({ id: 2, name: 'Gracie' })

// Actions: thenReturn / thenThrow / thenResolve / thenReject (+ *Once variants)
// `calledWith` supports asymmetric matchers
vi.when(sendEmail).calledWith(expect.stringContaining('@')).thenReturn({ ok: true })
```

- Behaviors match **first-in-first-out** (register specific before broad); stacked actions on one behavior consume **last-in-first-out**, with `{ times }` to limit.
- Handle unmatched calls with `{ onUnmatched: 'throw' | fn }` (default falls through to the original implementation).
- Assert all behaviors ran with `expect(w).toHaveBeenExhausted()`.

## Auto-Cleanup with `using`

In runtimes with Explicit Resource Management (Node 24+, TS 5.2+), declare spies/mocks with `using` to auto-restore when the block exits — works with `vi.spyOn`, `vi.fn`, `vi.doMock`, and `vi.when`:

```ts
it('mocks console only here', () => {
  using spy = vi.spyOn(console, 'log').mockImplementation(() => {})
  debug('message')
  expect(spy).toHaveBeenCalled()
} ) // console.log restored automatically — no afterEach
```

## Module Mocking

```ts
// vi.mock is hoisted to top of file
vi.mock('./api', () => ({
  fetchUser: vi.fn(() => ({ id: 1, name: 'Mock' })),
}))

import { fetchUser } from './api'

test('mocked module', () => {
  expect(fetchUser()).toEqual({ id: 1, name: 'Mock' })
})
```

### Partial Mock

```ts
vi.mock('./utils', async (importOriginal) => {
  const actual = await importOriginal()
  return {
    ...actual,
    specificFunction: vi.fn(),
  }
})
```

### Auto-mock with Spy

```ts
// Keep implementation but spy on calls
vi.mock('./calculator', { spy: true })

import { add } from './calculator'

test('spy on module', () => {
  const result = add(1, 2) // Real implementation
  expect(result).toBe(3)
  expect(add).toHaveBeenCalledWith(1, 2)
})
```

### Manual Mocks (__mocks__)

```
src/
  __mocks__/
    axios.ts      # Mocks 'axios'
  api/
    __mocks__/
      client.ts   # Mocks './client'
    client.ts
```

```ts
// Just call vi.mock with no factory
vi.mock('axios')
vi.mock('./api/client')
```

## Dynamic Mocking (vi.doMock)

Not hoisted - use for dynamic imports:

```ts
test('dynamic mock', async () => {
  vi.doMock('./config', () => ({
    apiUrl: 'http://test.local',
  }))
  
  const { apiUrl } = await import('./config')
  expect(apiUrl).toBe('http://test.local')
  
  vi.doUnmock('./config')
})
```

## Mock Timers

```ts
import { afterEach, beforeEach, vi } from 'vitest'

beforeEach(() => {
  vi.useFakeTimers()
})

afterEach(() => {
  vi.useRealTimers()
})

test('timers', () => {
  const fn = vi.fn()
  setTimeout(fn, 1000)
  
  expect(fn).not.toHaveBeenCalled()
  
  vi.advanceTimersByTime(1000)
  expect(fn).toHaveBeenCalled()
})

// Other timer methods
vi.runAllTimers()           // Run all pending timers
vi.runOnlyPendingTimers()   // Run only currently pending
vi.advanceTimersToNextTimer() // Advance to next timer
```

### Async Timer Methods

```ts
test('async timers', async () => {
  vi.useFakeTimers()
  
  let resolved = false
  setTimeout(() => Promise.resolve().then(() => { resolved = true }), 100)
  
  await vi.advanceTimersByTimeAsync(100)
  expect(resolved).toBe(true)
})
```

## Mock Dates

```ts
vi.setSystemTime(new Date('2024-01-01'))
expect(new Date().getFullYear()).toBe(2024)

vi.useRealTimers() // Restore
```

## Mock Globals

```ts
vi.stubGlobal('fetch', vi.fn(() => 
  Promise.resolve({ json: () => ({ data: 'mock' }) })
))

// Restore
vi.unstubAllGlobals()
```

## Mock Environment Variables

```ts
vi.stubEnv('API_KEY', 'test-key')
expect(import.meta.env.API_KEY).toBe('test-key')

// Restore
vi.unstubAllEnvs()
```

## Clearing Mocks

```ts
const fn = vi.fn()
fn()

fn.mockClear()       // Clear call history
fn.mockReset()       // Clear history + implementation
fn.mockRestore()     // Restore original (for spies)

// Global
vi.clearAllMocks()
vi.resetAllMocks()
vi.restoreAllMocks()
```

## Config Auto-Reset

```ts
// vitest.config.ts
defineConfig({
  test: {
    clearMocks: true,    // Clear before each test
    mockReset: true,     // Reset before each test
    restoreMocks: true,  // Restore after each test
    unstubEnvs: true,    // Restore env vars
    unstubGlobals: true, // Restore globals
  },
})
```

## Hoisted Variables for Mocks

```ts
const mockFn = vi.hoisted(() => vi.fn())

vi.mock('./module', () => ({
  getData: mockFn,
}))

import { getData } from './module'

test('hoisted mock', () => {
  mockFn.mockReturnValue('test')
  expect(getData()).toBe('test')
})
```

## v4 Behavior Changes

- `vi.fn().getMockName()` returns `'vi.fn()'` (was `'spy'`); snapshots show `[MockFunction]` instead of `[MockFunction spy]`.
- `vi.restoreAllMocks` (and `restoreMocks: true`) now **only restore `vi.spyOn` spies**; automocks are unaffected. `.mockRestore` still resets a mock's implementation/state.
- `vi.fn().mock.invocationCallOrder` starts at `1` (Jest parity).
- Automocked getters return `undefined` by default; automocked methods can't be restored.

## Key Points

- Prefer `vi.mock` for module mocking (hoisted - called before imports)
- Use `vi.doMock` for dynamic, non-hoisted mocking
- Use `vi.when` for argument-specific behaviors; `using` for scoped auto-restore
- Use `{ spy: true }` to keep implementation but track calls
- `vi.hoisted` lets you reference variables in mock factories

<!-- 
Source references:
- https://vitest.dev/guide/mocking.html
- https://vitest.dev/api/vi.html
- https://vitest.dev/guide/recipes/conditional-mocking
- https://vitest.dev/guide/recipes/explicit-resources
-->
references/features-reporters.md
---
name: reporters
description: Built-in reporters, default selection, and CI/output configuration
---

# Reporters

Select reporters via `--reporter` or `reporters` config. Configuring `reporters` **replaces** the default list — spread `configDefaults.reporters` to keep them.

```ts
import { configDefaults, defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    reporters: ['verbose', ['junit', { suiteName: 'UI tests' }]],
    // keep defaults and add one:
    // reporters: ['json', ...configDefaults.reporters],
  },
})
```

## Default Selection

When `reporters` is unset, Vitest auto-selects:

- `default` for normal terminal runs
- **`minimal` (alias `agent`)** when it detects an **AI coding agent** — only failed tests + errors, no summary or passing logs, optimized to cut token usage
- `github-actions` is added when `process.env.GITHUB_ACTIONS === 'true'`

## Built-in Reporters

| Reporter | Use |
|----------|-----|
| `default` | Summary + collapses passing files; prints full tree for single/failing file |
| `verbose` | One line per finished test (flat list in v4); only reporter that shows annotations on pass |
| `tree` | Like `default` but always shows each test (the old v3 verbose) |
| `dot` | One dot per test; details only for failures |
| `minimal` / `agent` | Failures only; best for AI/LLM workflows |
| `junit` | JUnit XML (templated, see below) |
| `json` | Jest-compatible JSON; includes `coverageMap` when coverage enabled |
| `tap` / `tap-flat` | TAP (nested / flat) |
| `html` | Interactive UI report (needs `@vitest/ui`) |
| `blob` | Serialized results for `--merge-reports` |
| `github-actions` | Workflow annotations + job summary |
| `hanging-process` | Lists processes preventing exit (debugging) |

> v4 removed the `basic` reporter (equivalent to `['default', { summary: false }]`). The old `verbose` flat behavior moved here; use `tree` for the nested view.

## Output Files

```bash
vitest --reporter=json --outputFile=./test-output.json
```

```ts
defineConfig({
  test: {
    reporters: ['junit', 'json'],
    outputFile: { junit: './junit.xml', json: './report.json' },
  },
})
```

## JUnit Templating

```ts
reporters: [['junit', {
  suiteNameTemplate: '{title}',     // {title} {filename} {basename} {displayName}
  classnameTemplate: '{classname}', // {classname} {title} {suitename} {filename} ...
  titleTemplate: '{title}',
  ancestorSeparator: ' > ',
  addFileAttribute: true,
}]]
```

`{filename}` is the **relative** path (use `{basename}` for the bare name). Templates can also be functions receiving all variables.

## HTML Report (v5 paths)

The HTML reporter writes a directory via `outputDir` (default `.vitest`); the entry is `<outputDir>/index.html`. Use `singleFile: true` for a self-contained shareable file (large; coverage not inlined).

```ts
reporters: [['html', { singleFile: true }]]
```

## Blob & Merge (CI/sharding)

Blobs default to `.vitest/blob/`. Label environments with `VITEST_BLOB_LABEL` or the reporter `label` option:

```bash
vitest run --reporter=blob --outputFile=reports/blob-1.json
vitest --merge-reports=reports --reporter=junit --reporter=default
```

Blob reports don't include file attachments — merge `attachmentsDir` (`.vitest/attachments/`) separately. `--reporter=blob`/`--merge-reports` don't work in watch mode.

## Key Points

- Configuring `reporters` replaces defaults — spread `configDefaults.reporters` to keep them
- The `minimal`/`agent` reporter is auto-selected for AI agents and minimizes token usage
- Use `tree` for the nested per-test view (old v3 `verbose`)
- v5 artifacts (blob, attachments, HTML) live under `.vitest/`

<!--
Source references:
- https://vitest.dev/guide/reporters
- https://vitest.dev/config/reporters
-->
references/features-snapshots.md
---
name: snapshot-testing
description: Snapshot testing with file, inline, and file snapshots
---

# Snapshot Testing

Snapshot tests capture output and compare against stored references.

## Basic Snapshot

```ts
import { expect, test } from 'vitest'

test('snapshot', () => {
  const result = generateOutput()
  expect(result).toMatchSnapshot()
})
```

First run creates `.snap` file:

```js
// __snapshots__/test.spec.ts.snap
exports['snapshot 1'] = `
{
  "id": 1,
  "name": "test"
}
`
```

## Inline Snapshots

Stored directly in test file:

```ts
test('inline snapshot', () => {
  const data = { foo: 'bar' }
  expect(data).toMatchInlineSnapshot()
})
```

Vitest updates the test file:

```ts
test('inline snapshot', () => {
  const data = { foo: 'bar' }
  expect(data).toMatchInlineSnapshot(`
    {
      "foo": "bar",
    }
  `)
})
```

## File Snapshots

Compare against explicit file:

```ts
test('render html', async () => {
  const html = renderComponent()
  await expect(html).toMatchFileSnapshot('./expected/component.html')
})
```

## Snapshot Hints

Add descriptive hints:

```ts
test('multiple snapshots', () => {
  expect(header).toMatchSnapshot('header')
  expect(body).toMatchSnapshot('body content')
  expect(footer).toMatchSnapshot('footer')
})
```

## Object Shape Matching

Match partial structure:

```ts
test('shape snapshot', () => {
  const data = { 
    id: Math.random(), 
    created: new Date(),
    name: 'test' 
  }
  
  expect(data).toMatchSnapshot({
    id: expect.any(Number),
    created: expect.any(Date),
  })
})
```

## Error Snapshots

```ts
test('error message', () => {
  expect(() => {
    throw new Error('Something went wrong')
  }).toThrowErrorMatchingSnapshot()
})

test('inline error', () => {
  expect(() => {
    throw new Error('Bad input')
  }).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`)
})
```

## Updating Snapshots

```bash
# Update all snapshots
vitest -u
vitest --update

# In watch mode, press 'u' to update failed snapshots
```

In CI (`process.env.CI`), Vitest **never writes** snapshots: mismatches, missing snapshots, and **obsolete snapshots** (entries no longer matching any test) all fail the run.

## Visual & ARIA Snapshots (Browser Mode)

```ts
import { expect, test } from 'vitest'
import { page } from 'vitest/browser' // v4: import from 'vitest/browser'

test('button looks correct', async () => {
  await expect(page.getByRole('button')).toMatchScreenshot('primary-button')
})

// ARIA snapshot — assert the accessibility tree (4.1+, experimental)
test('nav structure', async () => {
  await expect.element(page.getByRole('navigation')).toMatchAriaInlineSnapshot(`
    - navigation "Main":
      - link "Home"
  `)
})
```

## Custom Snapshot Matchers (4.1+)

Build matchers on the composable `Snapshots` helpers from `vitest` (replaces importing from `jest-snapshot`):

```ts
import { expect, Snapshots } from 'vitest'

const { toMatchSnapshot, toMatchInlineSnapshot } = Snapshots

expect.extend({
  toMatchTrimmedSnapshot(received: string, length: number) {
    return toMatchSnapshot.call(this, received.slice(0, length))
  },
  toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) {
    return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot)
  },
})
```

The inline snapshot string must be the **last** argument. File snapshot matchers must be `async`.

## Custom Serializers

Add custom snapshot formatting:

```ts
expect.addSnapshotSerializer({
  test(val) {
    return val && typeof val.toJSON === 'function'
  },
  serialize(val, config, indentation, depth, refs, printer) {
    return printer(val.toJSON(), config, indentation, depth, refs)
  },
})
```

Or via config:

```ts
// vitest.config.ts
defineConfig({
  test: {
    snapshotSerializers: ['./my-serializer.ts'],
  },
})
```

## Snapshot Format Options

```ts
defineConfig({
  test: {
    snapshotFormat: {
      printBasicPrototype: false, // Don't print Array/Object prototypes (Vitest default)
      escapeString: false,
      printShadowRoot: true,      // v4 default: custom elements print their shadow root
    },
  },
})
```

## Concurrent Test Snapshots

Use context's expect:

```ts
test.concurrent('concurrent 1', async ({ expect }) => {
  expect(await getData()).toMatchSnapshot()
})

test.concurrent('concurrent 2', async ({ expect }) => {
  expect(await getOther()).toMatchSnapshot()
})
```

## Snapshot File Location

Default: `__snapshots__/<test-file>.snap`

Customize:

```ts
defineConfig({
  test: {
    resolveSnapshotPath: (testPath, snapExtension) => {
      return testPath.replace('__tests__', '__snapshots__') + snapExtension
    },
  },
})
```

## Key Points

- Commit snapshot files to version control
- Review snapshot changes in code review
- Use hints for multiple snapshots in one test
- Use `toMatchFileSnapshot` for large outputs (HTML, JSON)
- Inline snapshots auto-update in test file
- Use context's `expect` for concurrent tests
- CI fails on obsolete snapshots; clean them with `--update`
- v4 prints custom-element shadow roots; disable via `snapshotFormat.printShadowRoot: false`

<!-- 
Source references:
- https://vitest.dev/guide/snapshot.html
- https://vitest.dev/api/expect.html#tomatchsnapshot
- https://vitest.dev/guide/browser/aria-snapshots
-->
references/features-test-tags.md
---
name: test-tags
description: Label tests with tags to filter runs and apply shared runner options
---

# Test Tags (4.1+)

Tags label tests so you can filter what runs and apply shared options (timeout, retry) to a *category* of tests that span many files. Reach for tags over projects when the category needs different timeouts/retries (not different pools/environments).

## Defining Tags

Tags **must be declared in config** — using an undefined tag throws unless `strictTags: false`. Each tag can carry options applied to every test marked with it:

```ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    tags: [
      { name: 'frontend', description: 'Frontend tests.' },
      { name: 'db', description: 'Database queries.', timeout: 60_000 },
      {
        name: 'flaky',
        retry: process.env.CI ? 3 : 0,
        timeout: 30_000,
        priority: 1, // lower priority number wins on conflicts
      },
    ],
    strictTags: true, // default: error on unknown tags
  },
})
```

Type-safe tag names (augment `TestTags`, include the file in your tsconfig):

```ts
import 'vitest'

declare module 'vitest' {
  interface TestTags {
    tags: 'frontend' | 'backend' | 'db' | 'flaky'
  }
}
```

## Applying Tags

```ts
import { describe, test } from 'vitest'

test('renders homepage', { tags: ['frontend'] }, () => {})

// Tags inherit from the parent suite
describe('API endpoints', { tags: ['backend'] }, () => {
  test('validates input', { tags: ['validation'] }, () => {
    // has both "backend" (inherited) and "validation"
  })
})
```

Tag every test in a file with a JSDoc `@module-tag` at the top of the file (applies to **all** tests in the file, not just the next one):

```ts
/**
 * @module-tag admin/pages/dashboard
 */
test('dashboard renders', () => {})
```

### Option conflict resolution

When several tags set the same option on a test, `priority` wins first (lower number), then array order. Options on the test itself always win:

```ts
test('flaky db test', { tags: ['flaky', 'db'] }) // timeout 30_000 (flaky priority 1), retry 3
test('override', { tags: ['flaky', 'db'], timeout: 120_000 }) // timeout 120_000, retry 3
```

## Filtering by Tag

Use `--tagsFilter` with an expression:

```bash
vitest --tagsFilter "frontend"
vitest --tagsFilter "db && !flaky"
vitest --tagsFilter "(unit || e2e) && !slow"
vitest --tagsFilter "api/*"            # wildcard
vitest --list-tags                     # list defined tags (=json for JSON)
```

Operators: `and`/`&&`, `or`/`||`, `not`/`!`, `*` wildcard, `()` grouping. Precedence: `not` > `and` > `or`. Multiple `--tagsFilter` flags combine with AND. Tag names can't be `and`/`or`/`not` or contain special chars/spaces.

Programmatic: pass `tagsFilter: ['frontend and backend']` to `startVitest`/`createVitest`.

## Checking the Filter at Runtime

Skip expensive setup when no matching tests are scheduled:

```ts
import { beforeAll, TestRunner } from 'vitest'

beforeAll(async () => {
  if (TestRunner.matchesTags(['db'])) {
    await seedDatabase()
  }
})
```

Returns `true` when the active `--tagsFilter` would include a test with those tags (or when no filter is active).

## Key Points

- Tags must be declared in config; the CLI filter is `--tagsFilter` (not `--tags`)
- Tags inherit from parent suites and `@module-tag` JSDoc comments
- Use tags for cross-cutting categories with shared timeout/retry; use [projects](advanced-projects.md) for different pools/environments
- `TestRunner.matchesTags` gates expensive `globalSetup`/`beforeAll` work

<!--
Source references:
- https://vitest.dev/guide/test-tags
- https://vitest.dev/config/tags
-->