11 KiB
11 KiB
TanStack Router Testing Examples
Complete examples for testing TanStack Router navigation, routes, and data loading.
Test Setup
Router Test Configuration
// src/test/router-utils.tsx
import { ReactElement } from 'react';
import { render } from '@testing-library/react';
import { createMemoryHistory, RouterProvider, createRootRoute, createRoute, createRouter } from '@tanstack/react-router';
export function renderWithRouter(
ui: ReactElement,
{ initialEntries = ['/'] } = {}
) {
const rootRoute = createRootRoute({
component: () => ui,
});
const router = createRouter({
routeTree: rootRoute,
history: createMemoryHistory({ initialEntries }),
});
return render(<RouterProvider router={router} />);
}
Example 1: Testing Route Navigation
Component with Navigation
// src/components/Navigation.tsx
import { Link } from '@tanstack/react-router';
export function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users">Users</Link>
</nav>
);
}
Test Suite
// src/components/Navigation.test.tsx
import { describe, it, expect } from 'vitest';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createRootRoute, createRoute, createRouter, RouterProvider } from '@tanstack/react-router';
import { render } from '@testing-library/react';
import { Navigation } from './Navigation';
function Home() {
return <div>Home Page</div>;
}
function About() {
return <div>About Page</div>;
}
function Users() {
return <div>Users Page</div>;
}
describe('Navigation', () => {
it('navigates between routes', async () => {
const user = userEvent.setup();
// Create routes
const rootRoute = createRootRoute({
component: () => (
<>
<Navigation />
<div id="content" />
</>
),
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: Home,
});
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: About,
});
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users',
component: Users,
});
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute, usersRoute]);
const router = createRouter({ routeTree });
render(<RouterProvider router={router} />);
// Initially on home
expect(screen.getByText('Home Page')).toBeInTheDocument();
// Navigate to About
await user.click(screen.getByRole('link', { name: /about/i }));
expect(await screen.findByText('About Page')).toBeInTheDocument();
// Navigate to Users
await user.click(screen.getByRole('link', { name: /users/i }));
expect(await screen.findByText('Users Page')).toBeInTheDocument();
});
});
Example 2: Testing Route Parameters
Component Using Route Params
// src/pages/UserProfile.tsx
import { useParams } from '@tanstack/react-router';
export function UserProfile() {
const { userId } = useParams({ from: '/users/$userId' });
return (
<div>
<h1>User Profile</h1>
<p data-testid="user-id">User ID: {userId}</p>
</div>
);
}
Test Suite
// src/pages/UserProfile.test.tsx
import { describe, it, expect } from 'vitest';
import { screen, render } from '@testing-library/react';
import { createRootRoute, createRoute, createRouter, RouterProvider } from '@tanstack/react-router';
import { createMemoryHistory } from '@tanstack/react-router';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('displays correct user ID from route params', () => {
const rootRoute = createRootRoute();
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserProfile,
});
const routeTree = rootRoute.addChildren([userRoute]);
const history = createMemoryHistory({ initialEntries: ['/users/123'] });
const router = createRouter({ routeTree, history });
render(<RouterProvider router={router} />);
expect(screen.getByTestId('user-id')).toHaveTextContent('User ID: 123');
});
it('updates when route params change', () => {
const rootRoute = createRootRoute();
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserProfile,
});
const routeTree = rootRoute.addChildren([userRoute]);
const history = createMemoryHistory({ initialEntries: ['/users/123'] });
const router = createRouter({ routeTree, history });
const { rerender } = render(<RouterProvider router={router} />);
expect(screen.getByTestId('user-id')).toHaveTextContent('User ID: 123');
// Navigate to different user
history.push('/users/456');
rerender(<RouterProvider router={router} />);
expect(screen.getByTestId('user-id')).toHaveTextContent('User ID: 456');
});
});
Example 3: Testing Protected Routes
Protected Route Component
// src/components/ProtectedRoute.tsx
import { useAuth } from '../hooks/useAuth';
import { Navigate } from '@tanstack/react-router';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <>{children}</>;
}
Test Suite
// src/components/ProtectedRoute.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { screen, render } from '@testing-library/react';
import { createRootRoute, createRoute, createRouter, RouterProvider } from '@tanstack/react-router';
import { ProtectedRoute } from './ProtectedRoute';
import * as useAuthModule from '../hooks/useAuth';
vi.mock('../hooks/useAuth');
function Dashboard() {
return <div>Dashboard</div>;
}
function Login() {
return <div>Login Page</div>;
}
describe('ProtectedRoute', () => {
it('renders children when authenticated', () => {
vi.mocked(useAuthModule.useAuth).mockReturnValue({
isAuthenticated: true,
user: { id: '1', name: 'Alice' },
login: vi.fn(),
logout: vi.fn(),
});
const rootRoute = createRootRoute();
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
component: () => (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
),
});
const loginRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/login',
component: Login,
});
const routeTree = rootRoute.addChildren([dashboardRoute, loginRoute]);
const router = createRouter({ routeTree });
render(<RouterProvider router={router} />);
expect(screen.getByText('Dashboard')).toBeInTheDocument();
});
it('redirects to login when not authenticated', async () => {
vi.mocked(useAuthModule.useAuth).mockReturnValue({
isAuthenticated: false,
user: null,
login: vi.fn(),
logout: vi.fn(),
});
const rootRoute = createRootRoute();
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
component: () => (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
),
});
const loginRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/login',
component: Login,
});
const routeTree = rootRoute.addChildren([dashboardRoute, loginRoute]);
const router = createRouter({ routeTree });
render(<RouterProvider router={router} />);
// Should redirect and show login
expect(await screen.findByText('Login Page')).toBeInTheDocument();
expect(screen.queryByText('Dashboard')).not.toBeInTheDocument();
});
});
Example 4: Testing Route Loaders
Route with Loader
// src/routes/user.tsx
import { createRoute } from '@tanstack/react-router';
interface User {
id: string;
name: string;
email: string;
}
export const userRoute = createRoute({
path: '/users/$userId',
loader: async ({ params }) => {
const response = await fetch(`/api/users/${params.userId}`);
if (!response.ok) throw new Error('User not found');
return response.json() as Promise<User>;
},
component: function UserPage({ useLoaderData }) {
const user = useLoaderData();
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
},
});
Test Suite
// src/routes/user.test.tsx
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { screen, render } from '@testing-library/react';
import { createRootRoute, createRouter, RouterProvider } from '@tanstack/react-router';
import { createMemoryHistory } from '@tanstack/react-router';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { userRoute } from './user';
const server = setupServer(
http.get('/api/users/:userId', ({ params }) => {
return HttpResponse.json({
id: params.userId,
name: 'Alice Johnson',
email: 'alice@example.com',
});
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('userRoute', () => {
it('loads and displays user data', async () => {
const rootRoute = createRootRoute();
const routeTree = rootRoute.addChildren([userRoute]);
const history = createMemoryHistory({ initialEntries: ['/users/123'] });
const router = createRouter({ routeTree, history });
render(<RouterProvider router={router} />);
// Wait for loader to complete
expect(await screen.findByText('Alice Johnson')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
it('handles loader error', async () => {
server.use(
http.get('/api/users/:userId', () => {
return new HttpResponse(null, { status: 404 });
})
);
const rootRoute = createRootRoute();
const routeTree = rootRoute.addChildren([userRoute]);
const history = createMemoryHistory({ initialEntries: ['/users/999'] });
const router = createRouter({ routeTree, history });
render(<RouterProvider router={router} />);
// Should show error (TanStack Router handles this automatically)
expect(await screen.findByText(/error/i)).toBeInTheDocument();
});
});
Key Takeaways
- Memory History: Use
createMemoryHistoryfor controlled navigation in tests - Route Setup: Build complete route trees with
createRouterfor realistic tests - Params Testing: Pass
initialEntriesto test routes with params - Protected Routes: Mock authentication context to test access control
- Loaders: Use MSW to mock loader data fetching
Next: TanStack Table Testing | Previous: Query Testing