React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

vitestsetupnext.jstestingjsdomhappy-domcoverage

Vitest Setup with Next.js

Configure Vitest as a fast, modern test runner for your Next.js App Router project.

Recipe

Quick-reference recipe card -- copy-paste ready.

# Install dependencies
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
 
export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./vitest.setup.ts"],
    include: ["**/*.test.{ts,tsx}"],
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
// package.json scripts
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

When to reach for this: When starting a new Next.js project and you want a fast, Vite-native test runner with near-instant HMR-based watch mode.

Working Example

// src/components/greeting.tsx
interface GreetingProps {
  name: string;
}
 
export function Greeting({ name }: GreetingProps) {
  return <h1>Hello, {name}!</h1>;
}
// src/components/greeting.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Greeting } from "./greeting";
 
describe("Greeting", () => {
  it("renders the name", () => {
    render(<Greeting name="Alice" />);
    expect(screen.getByRole("heading")).toHaveTextContent("Hello, Alice!");
  });
 
  it("updates when name prop changes", () => {
    const { rerender } = render(<Greeting name="Alice" />);
    expect(screen.getByRole("heading")).toHaveTextContent("Hello, Alice!");
 
    rerender(<Greeting name="Bob" />);
    expect(screen.getByRole("heading")).toHaveTextContent("Hello, Bob!");
  });
});

What this demonstrates:

  • Minimal Vitest config with React plugin and jsdom
  • Setup file that loads jest-dom matchers for Vitest
  • Path alias matching @/ from tsconfig
  • A simple component test using Testing Library

Deep Dive

How It Works

  • Vitest uses Vite's transform pipeline under the hood, so JSX/TSX files are compiled via @vitejs/plugin-react without needing a separate Babel config
  • The environment: "jsdom" setting creates a simulated browser DOM in Node.js for each test file
  • globals: true makes describe, it, expect available without importing them (matching Jest conventions)
  • The setup file runs before every test file -- loading @testing-library/jest-dom/vitest adds matchers like toBeInTheDocument() and toHaveTextContent()
  • Watch mode uses Vite's module graph to only re-run tests affected by changed files, making it much faster than Jest's watch mode

Variations

Using happy-dom instead of jsdom:

// vitest.config.ts
export default defineConfig({
  test: {
    environment: "happy-dom", // faster but less complete DOM implementation
  },
});

Per-file environment override:

// @vitest-environment happy-dom
import { describe, it } from "vitest";
// This file uses happy-dom regardless of global config

Coverage with v8 provider:

npm install -D @vitest/coverage-v8
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: "v8",
      reporter: ["text", "html", "lcov"],
      include: ["src/**/*.{ts,tsx}"],
      exclude: ["src/**/*.test.{ts,tsx}", "src/**/*.d.ts"],
    },
  },
});

App Router path aliases from tsconfig:

// vitest.config.ts — match tsconfig paths exactly
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
 
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./vitest.setup.ts"],
  },
});
npm install -D vite-tsconfig-paths

TypeScript Notes

// If using globals: true, add vitest types to tsconfig
// tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals", "@testing-library/jest-dom"]
  }
}

Gotchas

  • Missing @vitejs/plugin-react -- Without this plugin, JSX in test files fails to transform. Vitest does not automatically handle JSX like Jest with Babel does. Fix: Always include react() in the plugins array.

  • Path aliases not resolving -- If you use @/components/... imports, Vitest does not read tsconfig.json paths by default. Fix: Either set alias manually in vitest.config.ts or use vite-tsconfig-paths.

  • globals: true but TypeScript errors -- TypeScript does not know about Vitest globals unless you add "vitest/globals" to your compilerOptions.types. Fix: Update tsconfig.json as shown above.

  • jsdom vs happy-dom -- happy-dom is faster but lacks some DOM APIs (getBoundingClientRect, IntersectionObserver). If tests fail with happy-dom, switch to jsdom.

  • Next.js server-only code -- Vitest cannot run code that uses next/headers, next/cache, or other Node-only Next.js APIs in a jsdom environment. Fix: Mock those modules or test them separately.

Alternatives

AlternativeUse WhenDon't Use When
Jest with next/jestYou have an existing Jest setup or need the broader Jest ecosystemYou want faster watch mode and native ESM support
Playwright Component TestingYou need real browser rendering for component testsYou want fast unit tests that run in Node
Bun test runnerYour project uses Bun as its runtimeYou need the Vitest/Jest ecosystem of matchers and plugins

FAQs

Why do I need @vitejs/plugin-react in Vitest config?
  • Vitest does not transform JSX by default like Jest with Babel does.
  • Without the react() plugin, any JSX in test or source files will fail to compile.
  • Always include react() in the plugins array of vitest.config.ts.
What is the difference between jsdom and happy-dom environments?
  • jsdom is a more complete browser DOM implementation but slower.
  • happy-dom is faster but lacks some APIs like getBoundingClientRect and IntersectionObserver.
  • If tests fail under happy-dom, switch to jsdom.
How do I make describe, it, and expect available globally without imports?

Set globals: true in vitest.config.ts and add "vitest/globals" to your tsconfig.json compilerOptions.types array.

What does the vitest.setup.ts file do?
  • It runs before every test file.
  • Importing @testing-library/jest-dom/vitest adds matchers like toBeInTheDocument() and toHaveTextContent().
How do I resolve @/ path aliases in Vitest?

Either set alias manually in vitest.config.ts:

alias: {
  "@": path.resolve(__dirname, "./src"),
}

Or install and use vite-tsconfig-paths as a plugin.

Gotcha: Why does TypeScript complain about expect and describe even though tests run fine?
  • globals: true makes them available at runtime, but TypeScript does not know about them.
  • You must add "vitest/globals" to compilerOptions.types in tsconfig.json.
How do I set up code coverage with Vitest?
npm install -D @vitest/coverage-v8

Then add coverage.provider: "v8" and your desired reporters in vitest.config.ts.

Can I override the test environment on a per-file basis?

Yes. Add a comment at the top of the test file:

// @vitest-environment happy-dom

This overrides the global environment setting for that file only.

Gotcha: Why do my tests fail when importing next/headers or next/cache?
  • These are Node-only Next.js APIs that do not work in a jsdom environment.
  • You must mock those modules in your tests or test them in a separate environment.
How do I type the vitest.config.ts file correctly in TypeScript?

Import defineConfig from vitest/config:

import { defineConfig } from "vitest/config";
export default defineConfig({ /* ... */ });

This gives you full type-checking and autocompletion for all Vitest config options.

What is the difference between vitest and vitest run?
  • vitest starts watch mode, re-running tests when files change.
  • vitest run executes all tests once and exits -- use this in CI.
How does Vitest watch mode know which tests to re-run?

It uses Vite's module graph to track dependencies. Only tests affected by changed files are re-run, making it much faster than Jest's watch mode.