Vitest Testing Setup
What We Did
Section titled “What We Did”Added Vitest as the testing framework for the monorepo, following the official Turborepo with-vitest example. This provides a consistent testing setup across all packages and apps with Turborepo caching integration.
Why Vitest
Section titled “Why Vitest”Key reasons:
- Fast: Built on Vite, uses native ES modules for instant test startup
- TypeScript-first: Works with TypeScript out of the box, no configuration needed
- Turborepo integration: Test results are cached, only changed packages re-run tests
- Watch mode: Multi-project watch mode for development
- jsdom built-in: Easy browser environment simulation for React testing
Alternatives considered:
- Jest: More mature but slower, requires more configuration for TypeScript/ESM
- Bun test: Built into Bun but less ecosystem support for React Testing Library
- Playwright: Better for E2E, overkill for unit tests
Commands Used
Section titled “Commands Used”# Run all tests via Turborepo (cached)bun run test
# Watch mode for developmentbun test:watch
# Run tests for specific packageturbo run test --filter=@repo/ui
# Run with coverageturbo run test -- --coverageImplementation Details
Section titled “Implementation Details”Package Structure
Section titled “Package Structure”packages/vitest-config/├── package.json├── tsconfig.json└── src/ └── index.ts # Shared configuration export
packages/ui/├── vitest.config.ts # Uses shared config + jsdom└── tests/ └── utils.test.ts # Example test
apps/web/└── vitest.config.ts # Uses shared config + jsdom + React plugin
vitest.config.ts # Root config for multi-project watch modeShared Configuration (@repo/vitest-config)
Section titled “Shared Configuration (@repo/vitest-config)”import type { UserConfig } from "vitest/config"
export const sharedConfig: UserConfig = { test: { globals: true, coverage: { provider: "v8", reporter: ["text", "json", "html"], reportsDirectory: "./coverage", }, },}Package Configuration (Node.js packages)
Section titled “Package Configuration (Node.js packages)”import { sharedConfig } from "@repo/vitest-config"import { defineConfig } from "vitest/config"
export default defineConfig({ ...sharedConfig, test: { ...sharedConfig.test, environment: "jsdom", },})App Configuration (Next.js)
Section titled “App Configuration (Next.js)”import { sharedConfig } from "@repo/vitest-config"import react from "@vitejs/plugin-react"import { defineConfig } from "vitest/config"
export default defineConfig({ plugins: [react()], ...sharedConfig, test: { ...sharedConfig.test, environment: "jsdom", passWithNoTests: true, alias: { "@/": new URL("./src/", import.meta.url).pathname, }, },})Turborepo Task Configuration
Section titled “Turborepo Task Configuration”{ "tasks": { "test": { "dependsOn": ["transit"], "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/vitest.config.ts"], "outputs": ["coverage/**"] }, "transit": { "dependsOn": ["^transit"] } }}Why transit pattern?
- Tests can run in parallel (don’t need built output from dependencies)
- But cache must invalidate when dependency source code changes
transitcreates dependency relationships without matching any script, allowing parallel execution with correct cache invalidation
Root Configuration (Watch Mode)
Section titled “Root Configuration (Watch Mode)”import { sharedConfig } from "@repo/vitest-config"import { defineConfig } from "vitest/config"
export default defineConfig({ ...sharedConfig, test: { ...sharedConfig.test, projects: [ { root: "./packages", test: { ...sharedConfig.test, include: ["**/tests/**/*.test.{ts,tsx}", "**/src/**/*.test.{ts,tsx}"], }, }, { root: "./apps", test: { ...sharedConfig.test, environment: "jsdom", include: ["**/tests/**/*.test.{ts,tsx}", "**/src/**/*.test.{ts,tsx}"], }, }, ], },})Key Dependencies
Section titled “Key Dependencies”Root package.json:
vitest: ^3.0.0 - Test runner@repo/vitest-config: * - Shared configuration
@repo/ui package:
vitest: ^3.0.0 - Test runnerjsdom: ^26.0.0 - Browser environment simulation@testing-library/react: ^16.0.0 - React testing utilities
apps/web:
vitest: ^3.0.0 - Test runner@vitejs/plugin-react: ^4.4.0 - React JSX transform for Vitestjsdom: ^26.0.0 - Browser environment simulation@testing-library/react: ^16.0.0 - React testing utilities
Context for AI
Section titled “Context for AI”When working with tests in this codebase:
- Shared config: Import from
@repo/vitest-configto get consistent settings - Environment: Use
jsdomfor React/DOM testing,nodefor pure utilities - File location: Tests go in
tests/directory or colocated as*.test.ts - Watch mode: Use
bun test:watchfor development (runs root multi-project config) - CI mode: Use
bun run testfor Turborepo caching
Adding Tests to a New Package
Section titled “Adding Tests to a New Package”- Add dependencies to
package.json:
{ "devDependencies": { "@repo/vitest-config": "*", "vitest": "^3.0.0" }, "scripts": { "test": "vitest run", "test:watch": "vitest --watch" }}- Create
vitest.config.ts:
import { sharedConfig } from "@repo/vitest-config"import { defineConfig } from "vitest/config"
export default defineConfig({ ...sharedConfig, test: { ...sharedConfig.test, environment: "node", // or "jsdom" for UI },})- Create tests in
tests/directory
Example Test
Section titled “Example Test”import { describe, expect, test } from "vitest"
import { cn } from "../src/lib/utils"
describe("cn utility", () => { test("merges class names", () => { expect(cn("foo", "bar")).toBe("foo bar") })
test("handles conditional classes", () => { expect(cn("foo", false && "bar", "baz")).toBe("foo baz") })
test("merges Tailwind classes correctly", () => { expect(cn("p-4", "p-2")).toBe("p-2") })})Outcomes
Section titled “Outcomes”Before
Section titled “Before”- No testing framework configured
- No shared test configuration
- No CI caching for test results
- Vitest configured for all packages/apps
@repo/vitest-configprovides shared settings- Tests cached by Turborepo (only changed packages re-test)
- Watch mode available for development
- React Testing Library ready for component tests
Testing/Verification
Section titled “Testing/Verification”# Run all testsbun run test
# Should see output like:# @repo/ui:test: ✓ tests/utils.test.ts (5 tests)# web:test: No test files found, exiting with code 0
# Verify caching works (run again)bun run test# Should see: cache hit, replaying logsExpected results:
@repo/uitests pass (5 tests for cn utility)apps/webpasses with no tests (passWithNoTests: true)- Second run shows cache hits
Next Steps
Section titled “Next Steps”- Add component tests for
@repo/uicomponents using React Testing Library - Add integration tests for web app pages/features
- Configure coverage thresholds in CI
- Add visual regression testing with Playwright (for E2E)