Configuring Vitest for Next.js App Router
Root Cause Analysis: Why Default Vitest Fails with Next.js App Router
Next.js App Router enforces strict server/client component boundaries and mandates ESM-only module resolution. Default Vitest configurations assume a unified Node.js or browser environment, which immediately breaks when the runner encounters next/navigation, next/dynamic, or next/font. The core conflict originates from Vite’s dependency pre-bundling mechanism, which bypasses Next.js internal transforms. This bypass triggers SyntaxError: Cannot use import statement outside a module and causes hydration mismatches during test initialization. Proper alignment requires explicit environment isolation and dependency inlining, as documented in foundational Vitest Configuration & Setup guidelines. To resolve these failures, platform teams must map exact CI failure signatures to ESM/CJS boundary violations and use client directive leakage. Without explicit configuration, Vite attempts to bundle Next.js internals as CommonJS, violating the App Router’s strict module graph and causing cascading runtime exceptions.
Reproducible Setup & Exact Configuration Patterns
Establish a deterministic testing baseline by implementing a strict configuration matrix. The following patterns guarantee consistent execution across local environments and CI runners by eliminating window is not defined and ReferenceError: document is not defined during component mounting. Aligning these patterns with established Component & Integration Testing Frameworks ensures cross-project consistency.
vitest.config.ts must explicitly target jsdom, inline Next.js internals to prevent pre-bundling collisions, and resolve project aliases.
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/setup.ts'],
deps: {
inline: ['next']
},
alias: {
'@/': './src/'
}
}
});
src/setup.ts registers DOM matchers, polyfills missing browser APIs, and establishes global mock hoists.
import '@testing-library/jest-dom';
import { vi } from 'vitest';
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn()
}));
__mocks__/next/navigation.ts provides a strict, type-safe stub for routing primitives.
import { vi } from 'vitest';
export const useRouter = vi.fn(() => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
pathname: '/'
}));
export const usePathname = vi.fn(() => '/');
export const useSearchParams = vi.fn(() => new URLSearchParams());
Exclude next/font and next/image from Vite transforms by adding them to deps.inline or transform.ignorePatterns. This prevents compilation crashes and ensures deterministic asset resolution during test execution.
Pipeline Stability & Fast Resolution Strategies
Achieve pipeline stability by enforcing deterministic execution paths and eliminating race conditions. Configure Vitest to persist cache directories, apply strict timeouts for async data fetching, and isolate flaky hydration tests. When scaling across large monorepos, integrate Vitest into broader Component & Integration Testing Frameworks to standardize runner behavior and reduce cross-team configuration drift.
CI execution must enforce cache persistence, verbose reporting, and strict timeout boundaries:
vitest run --cache.dir .vitest-cache --reporter=verbose --testTimeout=5000
Flaky hydration tests are mitigated by wrapping render calls in explicit DOM mutation assertions. Avoid relying on implicit React reconciliation cycles; instead, enforce deterministic waits:
import { render, screen, waitFor } from '@testing-library/react';
import { act } from 'react';
test('renders async data without hydration mismatch', async () => {
await act(async () => {
render(<AsyncComponent />);
});
await waitFor(() => expect(screen.getByRole('heading')).toBeInTheDocument(), { timeout: 2000 });
});
Implement snapshot versioning with --updateSnapshot gated by mandatory PR review to prevent silent regression drift. Enforce test.concurrent only for independent integration suites to avoid shared state contamination. Target sub-second feedback loops by isolating expensive network mocks and leveraging vi.mock hoisting at the module boundary.
Validation Matrix & Troubleshooting Protocol
Deploy a decision matrix correlating runtime errors to exact configuration patches and runtime overrides for rapid incident resolution. Platform teams can self-serve fixes without escalating to framework maintainers by following these deterministic overrides. Reference the Vitest Configuration & Setup documentation for baseline environment constraints.
| Error Signature | Root Cause | Exact Mitigation |
|---|---|---|
TypeError: Cannot read properties of undefined (reading 'pathname') |
Missing mock hoisting or incorrect import path | Verify vi.mock('next/navigation', { hoisted: true }) is placed at the top of the test file before any imports. |
SyntaxError: Unexpected token 'export' |
Vite attempting to transform CJS/ESM hybrid Next.js internals | Add the offending package to deps.inline or exclude it via transform.ignorePatterns. |
Warning: Text content did not match. Server: "..." Client: "..." |
Non-deterministic hydration or missing use client directive |
Wrap client-only components in suppressHydrationWarning or ensure setup.ts polyfills match SSR output exactly. |
Apply transform.ignorePatterns to bypass problematic compiled directories:
// vitest.config.ts addition
transform: {
ignorePatterns: [/node_modules\/next\/dist\/compiled\//]
}
Enforce strict mock hoisting for Next.js link components to prevent ESM resolution failures:
vi.mock('next/link', () => ({
default: ({ children, href }) => <a href={href}>{children}</a>
}));
Define rollback procedures for breaking Next.js minor upgrades that invalidate existing Vite transforms. Maintain a version-locked vitest.config.ts and run a baseline suite before upgrading next or vite packages. Target performance benchmarks: <50ms per component test, <2s for integration suites, and zero unhandled promise rejections in CI logs. Adhering to these constraints ensures deterministic execution and aligns testing architecture with modern JavaScript standards.