Vitest Setup
Detect the project type, generate the right Vitest configuration, and produce working test infrastructure. Not a reference card — this skill creates files.
Workflow
-
Detect — scan the project to determine type and existing setup
-
Configure — generate vitest.config.ts tailored to the environment
-
Scaffold — create test setup, utilities, and a sample test
-
Wire up — add package.json scripts and TypeScript config
Step 1: Detect Project Type
Read these files to determine the project:
package.json → dependencies, scripts, type field tsconfig.json → paths, compiler options wrangler.toml → Cloudflare Workers project vite.config.ts → existing Vite setup (extend, don't replace) vitest.config.ts → already configured? just fill gaps jest.config.* → migration candidate src/ → source structure
Classify as one of:
Type Signals Environment
Cloudflare Workers wrangler.toml, @cloudflare/workers-types, cloudflare vite plugin node with Workers-specific setup
React (Vite) @vitejs/plugin-react, react-dom jsdom or happy-dom
React (SSR/TanStack Start) @tanstack/start, vinxi Split: node for server, jsdom for client
Node/Hono API hono, express, no react-dom node
Library exports field, no framework deps node
If a vite.config.ts already exists, extend it rather than creating a separate vitest.config.ts — Vitest reads Vite config natively.
Step 2: Install Dependencies
Generate the install command based on detected type:
Base (always)
pnpm add -D vitest
React projects — add jsdom and Testing Library
pnpm add -D @vitest/coverage-v8 jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
Workers projects — add Cloudflare test utilities
pnpm add -D @vitest/coverage-v8 @cloudflare/vitest-pool-workers
Node/Hono projects
pnpm add -D @vitest/coverage-v8
If migrating from Jest, also remove:
pnpm remove jest ts-jest @types/jest jest-environment-jsdom babel-jest
Use the project's package manager (check for pnpm-lock.yaml, yarn.lock, bun.lockb, or package-lock.json).
Step 3: Generate vitest.config.ts
Cloudflare Workers
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({ test: { globals: true, poolOptions: { workers: { wrangler: { configPath: "./wrangler.toml" }, }, }, }, });
If the project uses the Cloudflare Vite plugin (@cloudflare/vite-plugin ), integrate into the existing vite.config.ts instead:
/// <reference types="vitest/config" /> import { defineConfig } from "vite"; import { cloudflare } from "@cloudflare/vite-plugin";
export default defineConfig({ plugins: [cloudflare()], test: { globals: true, }, });
React (Vite)
/// <reference types="vitest/config" /> import { defineConfig } from "vite"; import react from "@vitejs/plugin-react";
export default defineConfig({ plugins: [react()], test: { globals: true, environment: "jsdom", setupFiles: ["./src/test/setup.ts"], css: true, }, });
If a vite.config.ts already exists, add the test block to it rather than creating a new file.
Node / Hono API
import { defineConfig } from "vitest/config";
export default defineConfig({ test: { globals: true, environment: "node", }, });
With Coverage (add to any config)
test: { // ... existing config coverage: { provider: "v8", reporter: ["text", "html", "lcov"], exclude: [ "node_modules/", "/.config.", "/*.d.ts", "/test/", ], }, },
Step 4: Generate Test Setup File
Create src/test/setup.ts (React projects only):
import "@testing-library/jest-dom/vitest";
That single import adds all the custom matchers (toBeInTheDocument, toHaveTextContent, etc.) and registers the Vitest expect.extend automatically.
Step 5: Add TypeScript Config
Add to tsconfig.json compilerOptions:
{ "compilerOptions": { "types": ["vitest/globals"] } }
For projects with multiple tsconfig files (e.g. tsconfig.app.json + tsconfig.node.json), add to the one that covers test files — usually the root tsconfig.json or create a tsconfig.test.json that extends it.
Step 6: Add Package.json Scripts
{ "scripts": { "test": "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage", "test:ui": "vitest --ui" } }
Don't overwrite existing scripts — merge with what's there.
Step 7: Generate Sample Test
Write one test file that demonstrates the right patterns for this specific project. Place it next to real source code, not in a separate tests directory.
For a Hono API route (e.g. src/routes/health.ts ):
import { describe, it, expect } from "vitest"; import { app } from "../index";
describe("GET /health", () => { it("returns 200 with status ok", async () => { const res = await app.request("/health"); expect(res.status).toBe(200);
const body = await res.json();
expect(body).toEqual({ status: "ok" });
}); });
For a React component (e.g. src/components/Button.tsx ):
import { describe, it, expect } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Button } from "./Button";
describe("Button", () => { it("renders with label", () => { render(<Button>Click me</Button>); expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument(); });
it("calls onClick when clicked", async () => { const user = userEvent.setup(); const handleClick = vi.fn(); render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledOnce();
}); });
For a utility function (e.g. src/utils/format.ts ):
import { describe, it, expect } from "vitest"; import { formatCurrency } from "./format";
describe("formatCurrency", () => { it("formats whole numbers", () => { expect(formatCurrency(1000)).toBe("$1,000.00"); });
it("formats decimals", () => { expect(formatCurrency(49.9)).toBe("$49.90"); });
it("handles zero", () => { expect(formatCurrency(0)).toBe("$0.00"); }); });
Pick a real file from the project to test. Don't invent a fake module — the sample test should run immediately after setup.
Step 8: Verify
Run the tests to confirm everything works:
pnpm test:run
If it fails, diagnose and fix. Common issues:
Error Fix
Cannot find module 'vitest'
Check install completed, check node_modules/.vitest exists
ReferenceError: describe is not defined
Add globals: true to config, or add types: ["vitest/globals"] to tsconfig
document is not defined
Wrong environment — set environment: "jsdom" for React tests
Cannot use import.meta
Ensure vitest.config uses .ts extension and project has "type": "module" or Vite handles transforms
Workers bindings undefined Use @cloudflare/vitest-pool-workers instead of plain vitest, check wrangler.toml path
Mocking Reference
These patterns are for writing tests after setup is complete. Include them in the sample test or a src/test/examples.test.ts if the user asks for mocking examples.
Module mocking (vi.mock)
import { vi, describe, it, expect } from "vitest"; import { getUser } from "./api";
vi.mock("./api", () => ({ getUser: vi.fn(), }));
it("mocks a module function", async () => { vi.mocked(getUser).mockResolvedValue({ id: 1, name: "Test" }); const user = await getUser(1); expect(user.name).toBe("Test"); expect(getUser).toHaveBeenCalledWith(1); });
Spy on methods (vi.spyOn)
it("spies on console.warn", () => { const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); doSomethingThatWarns(); expect(spy).toHaveBeenCalledOnce(); spy.mockRestore(); });
Fake timers
import { vi, beforeEach, afterEach, it, expect } from "vitest";
beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-15T10:00:00Z")); });
afterEach(() => { vi.useRealTimers(); });
it("uses controlled time", () => { expect(new Date().toISOString()).toBe("2026-01-15T10:00:00.000Z"); });
Global stubs
it("stubs fetch", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ data: "test" }), }); vi.stubGlobal("fetch", mockFetch);
const res = await fetch("/api/data"); expect(mockFetch).toHaveBeenCalledWith("/api/data");
vi.unstubAllGlobals(); });
Snapshot testing
it("matches snapshot", () => { const result = generateConfig({ debug: true }); expect(result).toMatchSnapshot(); });
it("matches inline snapshot", () => {
expect({ status: "ok", count: 3 }).toMatchInlineSnapshot( { "count": 3, "status": "ok", } );
});
Parameterized tests
describe.each([
{ input: "hello", expected: "HELLO" },
{ input: "world", expected: "WORLD" },
{ input: "", expected: "" },
])("toUpperCase($input)", ({ input, expected }) => {
it(returns ${expected}, () => {
expect(input.toUpperCase()).toBe(expected);
});
});
Jest Migration
When the detected project has Jest (jest.config.* , @types/jest , ts-jest in dependencies):
-
Generate the vitest.config.ts using the steps above
-
Update imports in existing test files:
// Before import { jest } from "@jest/globals"; jest.mock("./api"); jest.fn(); jest.spyOn(obj, "method");
// After import { vi } from "vitest"; vi.mock("./api"); vi.fn(); vi.spyOn(obj, "method");
- Remove Jest packages:
pnpm remove jest ts-jest @types/jest jest-environment-jsdom babel-jest @jest/globals
Update tsconfig — replace "types": ["jest"] with "types": ["vitest/globals"]
Run tests and fix any remaining issues
Key replacements:
Jest Vitest
jest.fn()
vi.fn()
jest.mock()
vi.mock()
jest.spyOn()
vi.spyOn()
jest.useFakeTimers()
vi.useFakeTimers()
jest.clearAllMocks()
vi.clearAllMocks()
jest.requireActual()
vi.importActual()
@jest/globals
vitest
jest.config.js
vitest.config.ts
Workspace Setup (Monorepos)
For monorepo projects with multiple packages:
// vitest.workspace.ts import { defineWorkspace } from "vitest/config";
export default defineWorkspace([ "packages/*/vitest.config.ts", ]);
Each package gets its own config. The workspace file just points to them.
What This Skill Produces
After running, the project should have:
-
vitest.config.ts (or test block added to existing vite.config.ts)
-
src/test/setup.ts (React projects)
-
Updated tsconfig.json with vitest/globals type
-
Updated package.json with test scripts
-
At least one passing sample test against real source code
-
Dependencies installed
The tests should pass on first run. If they don't, fix them before finishing.