| name | vitest-testing |
| description | Working on Vitest itself — the 4.x runner layer rather than the tests' content, covering config, the `vi` API, pools, coverage, and Browser Mode, plus driving it without hanging a session. Triggers on `vitest.config`, `vi.mock`, `vi.useFakeTimers`, `expect.poll`, `toMatchInlineSnapshot`, `test.extend`, `projects`, `pool`, `browser.instances`, `toMatchScreenshot`, `*.test-d.ts`, `import.meta.vitest`, or a config key Vitest 4 silently ignores. For what to assert and how a spec is shaped, use a unit-testing capability; for journeys and server lifecycle, an end-to-end one. Lookup-first where the option surface moved from v3. |
| user-invocable | false |
Vitest Testing
This skill equips you to configure, run, debug, and review a Vitest suite: the runner's own surface — its config file, its vi API, its pools and reporters, its coverage providers, and its Browser Mode — plus the operational rules an agent needs to drive it without hanging a session or burning a context window.
It is the runner-specific layer. A tool-agnostic unit-testing capability owns what to assert, how to name a spec, what makes a fixture good, and whether a behavior deserves a unit test at all; those questions have the same answers whatever runner executes them. This skill owns the mechanism underneath: which option, which file, which vi call, which flag. Where a rule here touches judgment — which dependency is worth mocking, how much of a payload to pin, whether a snapshot earns its place — it points at the tool-agnostic owner rather than answering it.
Two neighbours are disclaimed explicitly. An end-to-end capability owns user journeys, the locator fallback hierarchy, server lifecycle, and scenario coverage even when the runner underneath is Vitest; this skill owns the runner configuration that suite runs on. A component-development capability owns test-hook conventions such as data-testid and testID; this skill owns the Browser Mode machinery that queries them. A project on a different runner should reach for that runner's own capability, not this one.
Version posture is lookup-first. Rules are verified against Vitest 4.1.10 (Node >= 20, Vite >= 6) and name the release they were checked against. Vitest 4 moved or removed roughly two dozen configuration options and silently ignores the old names rather than erroring, so a v3-era answer copied off the open web fails invisibly — the suite runs, the option does nothing. Wherever a surface is known to have moved, consult the installed version's own documentation at https://vitest.dev rather than recalling an option name; each reference below names the upstream page it was verified against.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Running Vitest as an Agent
See running-as-an-agent.md for:
vitest run or --no-watch for every non-interactive run; bare vitest enters watch mode and never exits
- a project
test script defined as vitest or vitest --watch, which hands that hang to anyone who runs it
- narrowing with a path,
-t, file.test.ts:42, --project, --changed, or --related
- the
minimal reporter's token cost argument, and the custom reporters array that disables its auto-selection
- reading the reported diff and stack frame instead of re-running the suite to see the failure again
- third-party Vitest MCP servers, and why adding one needs the human's approval
Version Discipline
See version-discipline.md for:
- Vitest 4 ignoring an unrecognized
test key instead of erroring, so a v3-era option runs green and does nothing
workspace → projects, maxThreads/maxForks → maxWorkers, singleThread → maxWorkers: 1 + isolate: false
poolOptions.* flattened to the top level, and deps.external/inline → server.deps.*
coverage.all, environmentMatchGlobs, poolMatchGlobs, minWorkers, and the basic reporter, all removed
getMockName() now returning "vi.fn()", which rewrites every snapshot containing a mock
- which features need 4.1.0, 4.1.1, 4.1.3, or 4.1.4 — tags, the fixture builder, custom snapshot matchers, ARIA snapshots
Configuration
See configuration.md for:
defineConfig imported from vitest/config, not from vite, which does not type the test block
- v4's default exclusions shrinking to
node_modules and .git, so dist is now scanned unless excluded
globals: false and explicit imports as the posture, and the vitest/globals types entry globals: true then needs
clearMocks, mockReset, restoreMocks, unstubEnvs, unstubGlobals as config rather than a remembered hook
testTimeout at 5,000 ms, and raising the narrowest scope — per-test or hookTimeout — instead of the suite
dangerouslyIgnoreUnhandledErrors disabling a real check, and the experimental block as a separate risk class
Test Projects
See test-projects.md for:
projects as the one mechanism, replacing workspace, poolMatchGlobs, and environmentMatchGlobs together
- glob, config path, and inline entries,
! negation, and the packages/!(business) bracket idiom
- the
vitest.config.* / vitest.<name>.config.* naming rule a file must match to be picked up at all
- projects not inheriting the root config —
extends: true or mergeConfig, or shared setup files are simply absent
coverage, reporters, and resolveSnapshotPath being process-wide and therefore illegal in a project
- splitting a
node project from a browser one, or unit from integration with different isolation
Test Environment
See environments.md for:
node, jsdom, happy-dom, edge-runtime, and DOM startup cost paid per file
- the
// @vitest-environment jsdom docblock for a handful of files rather than the whole suite
- a dependency importing CSS or an asset failing under a DOM shim until the whole chain is in
server.deps.inline
viteEnvironment as v4's replacement for transformMode in a custom environment
- "passes under jsdom, breaks in the browser" as the signal to move to Browser Mode rather than stub further
Run Lifecycle
See run-lifecycle.md for:
globalSetup running in the main process with a different global scope, so only provide/inject crosses it
setupFiles running in the worker, which is why mock registration and expect.extend belong there
- setup files re-running under
isolate: false while the modules they import stay cached
globalSetup not re-running in watch mode, so a seeded database keeps the previous run's state
forceRerunTriggers and watchTriggerPatterns for a non-imported file that should invalidate a run
Test API
See test-api.md for:
- test options in the second argument; the v3 third-argument form is removed and silently unread
skip, skipIf, runIf, only, todo, fails, concurrent, and sequential's deprecation
test.each spreading an array case while test.for does not, and for supplying the test context
aroundEach/aroundAll and the runTest call without which nothing inside runs
onTestFinished/onTestFailed as the per-test cleanup that survives concurrency
retry's error predicate, for absorbing one failure mode rather than every error the test can raise
Fixtures and Test Context
See fixtures-and-context.md for:
context.expect being required for a snapshot inside a concurrent test
- the 4.1
test.extend builder inferring types, and onCleanup being callable only once per fixture
- test, file, and worker scope, and a fixture reaching only its own scope or a longer-lived one
- only destructured fixtures being initialized, so
{ database } and context.database are not equivalent
test.override for a suite's value, and injected fixtures fed from config.provide per project
Assertions and Async Tests
See assertions-and-async.md for:
toEqual and toStrictEqual disagreeing exactly on a present-undefined key and a sparse array
expect.objectContaining, expect.stringMatching, expect.closeTo, and expect.schemaMatching for Standard Schema
.resolves/.rejects failing the test in v4 when unawaited, where v3 passed silently
vi.waitFor vs vi.waitUntil vs expect.poll, and expect.poll rejecting snapshots, .rejects, and toThrow
expect.hasAssertions() for a test whose assertions sit inside a callback
expect.extend's MatcherResult, and why branching pass on isNot cancels out
Function Mocks and Spies
See function-mocks.md for:
- a spy seeing only calls made after it is installed, so import-time work is invisible to it
mockClear keeping the implementation, mockReset restoring the original, mockRestore restoring the descriptor
clearAllMocks/resetAllMocks/restoreAllMocks not reaching automocked modules at all
mockThrow/mockThrowOnce, withImplementation, and the Once variants for a call sequence
invocationCallOrder starting at 1 in v4, and preferring the mock matchers over reading mock.calls
vi.defineHelper so a shared assertion helper reports the caller's line
Module Mocking
See module-mocking.md for:
vi.mock hoisting above imports, leaving a module-level const undefined when the factory runs
vi.hoisted for a value the factory closes over, and importOriginal needing an await
- automocking emptying arrays and returning
undefined from methods, versus { spy: true } keeping real behavior
__mocks__ staying inert until a vi.mock call names the module
- a call between two functions in the same file being unmockable, and the extract-or-inject refactor that fixes it
server.deps.inline when vi.mock against an externalized dependency has no effect
Timers and Ambient State
See timers-and-ambient-state.md for:
- a fake clock left installed leaking into later files in the same worker
- the
*Async advance variants being required whenever a scheduled callback awaits
runAllTimers throwing at loopLimit on code that reschedules itself
vi.setSystemTime freezing a drifting snapshot, and TZ still governing formatted output
vi.stubGlobal/vi.stubEnv persisting across tests unless unstubGlobals/unstubEnvs are set
memfs through __mocks__/fs.cjs and __mocks__/fs/promises.cjs, mocking both specifiers
Network Mocking
See network-mocking.md for:
- intercepting with MSW rather than replacing
fetch, so the request path under test stays real
server.listen() / resetHandlers() / close() across beforeAll, afterEach, and afterAll
onUnhandledRequest: "error" as what stops an unmatched request reaching the real network
- why an end-to-end test deliberately wants the real dependency instead
Snapshots
See snapshots.md for:
- inline for a diff-reviewable value,
.snap for larger output, file snapshots for markup, ARIA for structure
- errors rendering as
[Error: message] rather than the bare message
-u accepting new, all, and none, and --update=new leaving existing snapshots alone
- CI refusing to write a snapshot, so a missing one fails rather than being created
- an obsolete snapshot failing CI, which catches a deleted test whose stored output stayed
Coverage
See coverage.md for:
@vitest/coverage-v8 and @vitest/coverage-istanbul needing explicit installation in v4
- only imported files being reported unless
coverage.include names the source globs
autoUpdate rewriting a threshold after a run, so a drop is recorded rather than rejected
- ignore hints needing
-- @preserve or esbuild strips them before the provider sees them
DEBUG=vitest:coverage when collection itself is what is slow
Performance and Parallelism
See performance-and-parallelism.md for:
- files across workers versus tests within a file, and which axis a given slowdown sits on
forks as the compatible default, and threads producing worker-termination and segfault failures
isolate: false as the biggest win, what module-level state survives it, and vmThreads forbidding it
- concurrency only helping tests that wait, since synchronous tests still share one thread
- reading the transform, collect, setup, and environment timings before changing any setting
- sharding splitting files rather than cases, so one slow file cannot be sharded
Filtering and Tags
See filtering-and-tags.md for:
- a positional path filter,
-t, and file.test.ts:10 needing the full filename
allowOnly at its default failing a CI run that contains .only
- declaring tags in config before use, with
timeout, retry, and priority per tag
--tags-filter expressions with and/or/not, wildcards, and not > and > or precedence
@module-tag applying to a whole file rather than one test
- filters applying per file, so Vitest still loads every file to find matches
Reporters and CI Output
See reporters-and-ci.md for:
minimal, aliased agent, printing only failures and their errors
- its auto-selection on
std-env AI-agent detection or an AI_AGENT variable
- any custom
reporters array skipping that detection, and github-actions needing listing for the same reason
blob with --shard, merged by --merge-reports, as the only correct way to report a sharded run
outputFile as a path or a per-reporter map
context.annotate severities, and the default reporter printing them only for failed tests
Browser Mode
See browser-mode.md for:
- CSS layout, computed style, and real event dispatch as the bug class a DOM shim cannot produce
- v4 providers being objects from
@vitest/browser-playwright, -webdriverio, or -preview, not strings
- imports moving to
vitest/browser from the removed @vitest/browser/context
await expect.element(...) as the retrying assertion, where the synchronous expect does not
userEvent from vitest/browser driving real input, distinct from Testing Library's synthetic events
alert/confirm blocking the thread, and sealed ESM namespaces needing { spy: true } to observe an export
Visual Regression
See visual-regression.md for:
toMatchScreenshot writing a reference on first run and failing, into a committed __screenshots__/
allowedMismatchedPixelRatio versus allowedMismatchedPixels, where the stricter limit wins
- stable-screenshot detection reshooting until two frames agree
- fonts, GPU, and headless mode making a local screenshot differ from CI's on an unchanged page
toMatchAriaSnapshot asserting the accessibility tree instead, and surviving a font bump
Type and In-Source Tests
See types-and-in-source.md for:
*.test-d.ts with expectTypeOf/assertType, run only under --typecheck
- type tests being analyzed rather than executed, so a
test.each name is never evaluated
import.meta.vitest with includeSource, and "vitest/importMeta" in the tsconfig types
- the production-build
define: { "import.meta.vitest": "undefined" } without which those tests ship to users
- the documentation scoping in-source tests to small utilities, not components or e2e
Debugging and Failure Modes
See debugging-and-failure-modes.md for:
--inspect-brk --no-file-parallelism --test-timeout=0 together, and what each flag removes
--isolate false keeping a debugger attached across watch reruns
- the
hanging-process reporter and --detect-async-leaks for a run that will not exit
Failed to Terminate Worker from Node's fetch under threads, fixed by forks
- segfaults from native modules under
threads, fixed the same way
Cannot find module from an unresolved path alias, and ssr.resolve.conditions for ignored export conditions
Suite Hygiene
See suite-hygiene.md for:
expect-expect, no-focused-tests, and valid-expect from @vitest/eslint-plugin
allowOnly and obsolete-snapshot failures as the two guards against an abandoned edit
restoreMocks/unstubEnvs/unstubGlobals moving hygiene out of every author's memory
--detect-async-leaks for a handle that outlives the test that opened it
- whether CI invokes
vitest run rather than a form that can enter watch mode, and whether a changed config key still exists in the installed version
Extending Vitest
See extending-vitest.md for:
- the removed v3 reporter hooks (
onCollected, onTaskUpdate, onFinished) producing silence, not an error
viteEnvironment in a custom environment, and the rewritten v4 pool interface
startVitest, createVitest, and vitest.state.getTestModules() from vitest/node
experimental.openTelemetry needing an sdkPath whose module default-exports an unstarted SDK
- the per-worker startup cost tracing adds unless the run is not isolated