chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
@@ -0,0 +1,5 @@
# Generation Info
- **Source:** `sources/vitest`
- **Git SHA:** `4a7321e10672f00f0bb698823a381c2cc245b8f7`
- **Generated:** 2026-01-28
@@ -0,0 +1,53 @@
---
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.1.28'
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 3.x, generated at 2026-01-28.
## Core
| Topic | Description | Reference |
| ------------- | --------------------------------------------------------------- | -------------------------------------------- |
| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) |
| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) |
| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) |
| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) |
| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) |
| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) |
## Features
| Topic | Description | Reference |
| ------------ | -------------------------------------------------------------- | ---------------------------------------------------------- |
| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) |
| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) |
| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) |
| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) |
| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) |
| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) |
## Advanced
| Topic | Description | Reference |
| ------------ | ------------------------------------------------------- | ------------------------------------------------------------ |
| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) |
| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) |
| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) |
| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) |
@@ -0,0 +1,264 @@
---
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:
```ts
defineConfig({
test: {
browser: {
enabled: true,
name: 'chromium', // or 'firefox', 'webkit'
provider: 'playwright',
},
},
});
```
## 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
-->
@@ -0,0 +1,300 @@
---
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
vitest --project unit --project e2e
# Exclude project
vitest --project.ignore 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
});
```
## Project Isolation
Each project runs in its own thread pool by default:
```ts
defineConfig({
test: {
projects: [
{
test: {
name: 'isolated',
isolate: true, // Full isolation
pool: 'forks',
},
},
],
},
});
```
## 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
- Each project can have different environment, config
- Use glob patterns for monorepo packages
- Run specific projects with `--project` flag
- Use `provide` to inject config values into tests
- Projects inherit from root config unless overridden
<!--
Source references:
- https://vitest.dev/guide/projects.html
-->
@@ -0,0 +1,242 @@
---
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).toMatchTypeOf<{ 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' }).toMatchTypeOf<User>();
```
## Equality vs Matching
```ts
interface A {
x: number;
}
interface B {
x: number;
y: string;
}
// toMatchTypeOf - subset matching
expectTypeOf<B>().toMatchTypeOf<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.toMatchTypeOf<{ name: string }>();
});
});
```
## Key Points
- Use `.test-d.ts` for type-only tests
- `expectTypeOf` for type assertions
- `toMatchTypeOf` for subset matching
- `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
-->
@@ -0,0 +1,249 @@
---
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
```
## Reset Modules
```ts
// Clear module cache
vi.resetModules();
// Wait for dynamic imports
await vi.dynamicImportSettled();
```
## Fake Timers
```ts
vi.useFakeTimers();
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
- Fake timers require explicit setup and teardown
- `vi.waitFor` retries until assertion passes
<!--
Source references:
- https://vitest.dev/api/vi.html
-->
@@ -0,0 +1,167 @@
---
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
```
## Common Options
```bash
# Configuration
--config <path> # Path to config file
--project <name> # Run specific project
# Filtering
--testNamePattern, -t # Run tests matching pattern
--changed # Run tests for changed files
--changed HEAD~1 # Tests for last commit changes
# Reporters
--reporter <name> # default, verbose, dot, json, html
--reporter=html --outputFile=report.html
# 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
--sequence.shuffle # Randomize test order
# Watch mode
--no-watch # Disable watch mode
--standalone # Start without running tests
# 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:
```bash
# Machine 1
vitest run --shard=1/3 --reporter=blob
# Machine 2
vitest run --shard=2/3 --reporter=blob
# Machine 3
vitest run --shard=3/3 --reporter=blob
# Merge reports
vitest --merge-reports --reporter=junit
```
## 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
<!--
Source references:
- https://vitest.dev/guide/cli.html
-->
@@ -0,0 +1,177 @@
---
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/**'],
// Test timeout in ms
testTimeout: 5000,
// Hook timeout in ms
hookTimeout: 10000,
// Enable watch mode by default
watch: true,
// Coverage configuration
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'html'],
include: ['src/**/*.ts'],
},
// Run tests in isolation (each file in separate process)
isolate: true,
// Pool for running tests: 'threads', 'forks', 'vmThreads'
pool: 'threads',
// Number of threads/processes
poolOptions: {
threads: {
maxThreads: 4,
minThreads: 1,
},
},
// Automatically clear mocks between tests
clearMocks: true,
// Restore mocks between tests
restoreMocks: true,
// Retry failed tests
retry: 0,
// Stop after first failure
bail: 0,
},
});
```
## 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
- `process.env.VITEST` is set to `true` when running tests
- Test config uses `test` property, rest is Vite config
<!--
Source references:
- https://vitest.dev/guide/#configuring-vitest
- https://vitest.dev/config/
-->
@@ -0,0 +1,193 @@
---
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 }) => {});
});
```
### Sequential in Concurrent
```ts
describe.concurrent('parallel', () => {
test('concurrent 1', async () => {});
describe.sequential('must be sequential', () => {
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
All modifiers can be chained:
```ts
describe.skip.concurrent('skipped concurrent', () => {});
describe.only.shuffle('only and shuffled', () => {});
describe.concurrent.skip('equivalent', () => {});
```
## Key Points
- Top-level tests belong to an implicit file suite
- Nested suites inherit parent's options (timeout, retry, 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
<!--
Source references:
- https://vitest.dev/api/describe.html
-->
@@ -0,0 +1,207 @@
---
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);
```
## 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]));
```
## Soft Assertions
Continue test after failure:
```ts
expect.soft(1).toBe(2); // Marks test failed but continues
expect.soft(2).toBe(3); // Also runs
// All failures reported at end
```
## 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
<!--
Source references:
- https://vitest.dev/api/expect.html
-->
@@ -0,0 +1,245 @@
---
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:
```ts
const test = base.extend<{ db: Database }>({
db: async ({}, use) => {
const db = await createDb();
await use(db);
await db.close();
},
});
// These hooks know about `db` fixture
test.beforeEach(({ db }) => {
db.seed();
});
test.afterEach(({ db }) => {
db.clear();
});
```
## 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
-->
@@ -0,0 +1,237 @@
---
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');
});
```
### Sequential Tests
```ts
// Force sequential in concurrent context
test.sequential('must run alone', 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 }) => {
console.log(task.name); // Test name
skip(someCondition); // Skip dynamically
expect(1).toBe(1); // Context-bound expect
});
```
## Custom Test with Fixtures
```ts
import { test as base } from 'vitest';
const test = base.extend({
db: async ({}, use) => {
const db = await createDb();
await use(db);
await db.close();
},
});
test('query', async ({ db }) => {
const users = await db.query('SELECT * FROM users');
expect(users).toBeDefined();
});
```
## 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
```ts
test('database test', { tags: ['db', 'slow'] }, async () => {});
// Run with: vitest --tags db
```
## Key Points
- 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
<!--
Source references:
- https://vitest.dev/api/test.html
-->
@@ -0,0 +1,250 @@
---
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,
// Number of worker threads
maxWorkers: 4,
minWorkers: 1,
// Pool type: 'threads', 'forks', 'vmThreads'
pool: 'threads',
},
});
```
## 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.
## Sequential in Concurrent Context
Force sequential execution:
```ts
describe.concurrent('mostly parallel', () => {
test('parallel 1', async () => {});
test('parallel 2', async () => {});
test.sequential('must run alone 1', async () => {});
test.sequential('must run alone 2', async () => {});
});
// Or entire suite
describe.sequential('sequential suite', () => {
test('first', () => {});
test('second', () => {});
});
```
## 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,
},
},
});
```
## Shuffle Tests
Randomize to catch hidden dependencies:
```ts
// Via CLI
vitest --sequence.shuffle
// Per suite
describe.shuffle('random order', () => {
test('test 1', () => {})
test('test 2', () => {})
test('test 3', () => {})
})
```
## Pool Options
### Threads (Default)
```ts
defineConfig({
test: {
pool: 'threads',
poolOptions: {
threads: {
maxThreads: 8,
minThreads: 2,
isolate: true,
},
},
},
});
```
### Forks
Better isolation, slower:
```ts
defineConfig({
test: {
pool: 'forks',
poolOptions: {
forks: {
maxForks: 4,
isolate: true,
},
},
},
});
```
### VM Threads
Full VM isolation per file:
```ts
defineConfig({
test: {
pool: 'vmThreads',
},
});
```
## 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
- Use `.concurrent` for parallel tests within file
- Always use context's `expect` in concurrent tests
- Sharding splits tests across CI machines
- Use `--merge-reports` to combine sharded results
- Shuffle tests to find hidden dependencies
<!--
Source references:
- https://vitest.dev/guide/features.html#running-tests-concurrently
- https://vitest.dev/guide/improving-performance.html
-->
@@ -0,0 +1,240 @@
---
name: test-context-fixtures
description: Test context, custom fixtures with test.extend
---
# Test Context & Fixtures
## Built-in Context
Every test receives context as first argument:
```ts
test('context', ({ task, expect, skip }) => {
console.log(task.name); // Test name
expect(1).toBe(1); // Context-bound expect
skip(); // Skip test dynamically
});
```
### Context Properties
- `task` - Test metadata (name, file, etc.)
- `expect` - Expect bound to this test (important for concurrent tests)
- `skip(condition?, message?)` - Skip the test
- `onTestFinished(fn)` - Cleanup after test
- `onTestFailed(fn)` - Run on failure only
## Custom Fixtures with test.extend
Create reusable test utilities:
```ts
import { test as base } from 'vitest';
// Define fixture types
interface Fixtures {
db: Database;
user: User;
}
// Create extended test
export const test = base.extend<Fixtures>({
// Fixture with setup/teardown
db: async ({}, use) => {
const db = await createDatabase();
await use(db); // Provide to test
await db.close(); // Cleanup
},
// Fixture depending on another fixture
user: async ({ db }, use) => {
const user = await db.createUser({ name: 'Test' });
await use(user);
await db.deleteUser(user.id);
},
});
```
Using fixtures:
```ts
test('query user', async ({ db, user }) => {
const found = await db.findUser(user.id);
expect(found).toEqual(user);
});
```
## Fixture Initialization
Fixtures only initialize when accessed:
```ts
const test = base.extend({
expensive: async ({}, use) => {
console.log('initializing'); // Only runs if test uses it
await use('value');
},
});
test('no fixture', () => {}); // expensive not called
test('uses fixture', ({ expensive }) => {}); // expensive called
```
## Auto Fixtures
Run fixture for every test:
```ts
const test = base.extend({
setup: [
async ({}, use) => {
await globalSetup();
await use();
await globalTeardown();
},
{ auto: true }, // Always run
],
});
```
## Scoped Fixtures
### File Scope
Initialize once per file:
```ts
const test = base.extend({
connection: [
async ({}, use) => {
const conn = await connect();
await use(conn);
await conn.close();
},
{ scope: 'file' },
],
});
```
### Worker Scope
Initialize once per worker:
```ts
const test = base.extend({
sharedResource: [
async ({}, use) => {
await use(globalResource);
},
{ scope: 'worker' },
],
});
```
## Injected Fixtures (from Config)
Override fixtures per project:
```ts
// test file
const test = base.extend({
apiUrl: ['/default', { injected: true }],
});
// vitest.config.ts
defineConfig({
test: {
projects: [
{
test: {
name: 'prod',
provide: { apiUrl: 'https://api.prod.com' },
},
},
],
},
});
```
## Scoped Values per Suite
Override fixture for specific suite:
```ts
const test = base.extend({
environment: 'development',
});
describe('production tests', () => {
test.scoped({ environment: 'production' });
test('uses production', ({ environment }) => {
expect(environment).toBe('production');
});
});
test('uses default', ({ environment }) => {
expect(environment).toBe('development');
});
```
## Extended Test Hooks
Type-aware hooks with fixtures:
```ts
const test = base.extend<{ db: Database }>({
db: async ({}, use) => {
const db = await createDb();
await use(db);
await db.close();
},
});
// Hooks know about fixtures
test.beforeEach(({ db }) => {
db.seed();
});
test.afterEach(({ db }) => {
db.clear();
});
```
## Composing Fixtures
Extend from another extended test:
```ts
// base-test.ts
export const test = base.extend<{ db: Database }>({
db: async ({}, use) => {
/* ... */
},
});
// admin-test.ts
import { test as dbTest } from './base-test';
export const test = dbTest.extend<{ admin: User }>({
admin: async ({ db }, use) => {
const admin = await db.createAdmin();
await use(admin);
},
});
```
## Key Points
- Use `{ }` destructuring to access fixtures
- Fixtures are lazy - only initialize when accessed
- Return cleanup function from fixtures
- Use `{ auto: true }` for setup fixtures
- Use `{ scope: 'file' }` for expensive shared resources
- Fixtures compose - extend from extended tests
<!--
Source references:
- https://vitest.dev/guide/test-context.html
-->
@@ -0,0 +1,202 @@
---
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'],
// Files to include
include: ['src/**/*.{ts,tsx}'],
// Files to exclude
exclude: ['node_modules/', 'tests/', '**/*.d.ts', '**/*.test.ts'],
// Report uncovered files
all: true,
// 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
- 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,
},
}
```
## 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:
```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
```
## Key Points
- V8 is faster, Istanbul is more compatible
- Use `--coverage` flag or `coverage.enabled: true`
- Include `all: true` to see uncovered files
- Set thresholds to enforce minimum coverage
- Use `@preserve` comment to keep ignore hints
<!--
Source references:
- https://vitest.dev/guide/coverage.html
-->
@@ -0,0 +1,208 @@
---
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
Filter by custom tags:
```ts
test('database test', { tags: ['db'] }, () => {});
test('slow test', { tags: ['slow', 'integration'] }, () => {});
```
Run tagged tests:
```bash
vitest --tags db
vitest --tags "db,slow" # OR
vitest --tags db --tags slow # OR
```
Configure allowed tags:
```ts
defineConfig({
test: {
tags: ['db', 'slow', 'integration'],
strictTags: true, // Fail on unknown tags
},
});
```
## 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'],
},
});
```
## 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
-->
@@ -0,0 +1,272 @@
---
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();
```
## 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');
});
```
## Key Points
- `vi.mock` is hoisted - called before imports
- Use `vi.doMock` for dynamic, non-hoisted mocking
- Always restore mocks to avoid test pollution
- 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
-->
@@ -0,0 +1,207 @@
---
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
```
## 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
escapeString: false,
},
},
});
```
## 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
<!--
Source references:
- https://vitest.dev/guide/snapshot.html
- https://vitest.dev/api/expect.html#tomatchsnapshot
-->