This guide covers testing TanStack Router applications that use file-based routing, including testing route generation, file-based route components, and file-based routing patterns.
Test file-based routing by setting up route mocking utilities, testing generated route trees, and implementing patterns specific to file-based route structures and conventions.
File-based routing testing differs from code-based routing testing in several key ways:
For file-based routing testing, you'll need the same base dependencies as regular router testing:
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
Create vitest.config.ts with file-based routing support:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite({
// Configure for test environment
routesDirectory: './src/routes',
generatedRouteTree: './src/routeTree.gen.ts',
disableLogging: true,
}),
react(),
],
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
typecheck: { enabled: true },
watch: false,
// Ensure route tree is generated before tests
globals: true,
},
})
Create src/test/file-route-utils.tsx:
import React from 'react'
import { render, RenderOptions } from '@testing-library/react'
import {
createRouter,
RouterProvider,
createMemoryHistory,
} from '@tanstack/react-router'
// Import the generated route tree
import { routeTree } from '../routeTree.gen'
// Create test router with generated route tree
export function createTestRouterFromFiles(initialLocation = '/') {
const router = createRouter({
routeTree,
history: createMemoryHistory({
initialEntries: [initialLocation],
}),
context: {
// Add any required context for your routes
},
})
return router
}
// Custom render function for file-based routes
interface RenderWithFileRoutesOptions extends Omit<RenderOptions, 'wrapper'> {
initialLocation?: string
routerContext?: any
}
export function renderWithFileRoutes(
ui: React.ReactElement,
{
initialLocation = '/',
routerContext = {},
...renderOptions
}: RenderWithFileRoutesOptions = {},
) {
const router = createRouter({
routeTree,
history: createMemoryHistory({
initialEntries: [initialLocation],
}),
context: routerContext,
})
function Wrapper({ children }: { children: React.ReactNode }) {
return <RouterProvider router={router}>{children}</RouterProvider>
}
return {
...render(ui, { wrapper: Wrapper, ...renderOptions }),
router,
}
}
// Helper to test specific file routes
export function createMockFileRoute(
path: string,
component: React.ComponentType,
) {
// This is useful for isolated testing when you don't want to use the full route tree
return {
path,
component,
// Add other common route properties as needed
}
}
import { describe, it, expect } from 'vitest'
import { routeTree } from '../routeTree.gen'
describe('Generated Route Tree', () => {
it('should generate route tree from file structure', () => {
// Test that route tree exists and has expected structure
expect(routeTree).toBeDefined()
expect(routeTree.children).toBeDefined()
})
it('should include all expected routes', () => {
// Get all route paths from the generated tree
const getAllRoutePaths = (tree: any, paths: string[] = []): string[] => {
if (tree.path) {
paths.push(tree.path)
}
if (tree.children) {
tree.children.forEach((child: any) => {
getAllRoutePaths(child, paths)
})
}
return paths
}
const routePaths = getAllRoutePaths(routeTree)
// Test that expected routes are present
expect(routePaths).toContain('/')
expect(routePaths).toContain('/about')
// Add assertions for your specific routes
})
it('should have correct route hierarchy', () => {
// Test parent-child relationships
const homeRoute = routeTree.children?.find(
(child: any) => child.path === '/',
)
expect(homeRoute).toBeDefined()
// Test for specific route structure based on your file organization
// For example, if you have /posts/$postId routes:
// const postsRoute = routeTree.children?.find((child: any) => child.path === '/posts')
// expect(postsRoute?.children).toBeDefined()
})
})
import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('File Route Conventions', () => {
it('should render index route at root path', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/',
})
// Test that the index route component renders
// This depends on what your src/routes/index.tsx exports
expect(screen.getByText('Welcome Home!')).toBeInTheDocument()
})
it('should handle route parameters from filename', () => {
// If you have a route like src/routes/posts/$postId.tsx
renderWithFileRoutes(<div />, {
initialLocation: '/posts/123',
})
// Test that parameter is correctly parsed from file-based route
expect(screen.getByText(/Post.*123/)).toBeInTheDocument()
})
it('should handle nested routes from directory structure', () => {
// If you have src/routes/dashboard/settings.tsx
renderWithFileRoutes(<div />, {
initialLocation: '/dashboard/settings',
})
expect(screen.getByText(/Settings/)).toBeInTheDocument()
})
it('should handle layout routes', () => {
// If you have src/routes/_layout.tsx
renderWithFileRoutes(<div />, {
initialLocation: '/some-nested-route',
})
// Test that layout is rendered for nested routes
expect(screen.getByTestId('layout-header')).toBeInTheDocument()
})
})
import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { createFileRoute } from '@tanstack/react-router'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('Individual Route Components', () => {
it('should test about route component', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/about',
})
expect(screen.getByText('About')).toBeInTheDocument()
})
it('should test route with loader data', () => {
// For a route like src/routes/posts/index.tsx with loader
renderWithFileRoutes(<div />, {
initialLocation: '/posts',
})
// Wait for loader data to load
expect(screen.getByText(/Posts List/)).toBeInTheDocument()
})
it('should test route with search params validation', () => {
// For a route with validateSearch in src/routes/search.tsx
renderWithFileRoutes(<div />, {
initialLocation: '/search?q=react&page=1',
})
expect(screen.getByDisplayValue('react')).toBeInTheDocument()
})
})
import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('Route-Specific Hooks', () => {
it('should test useParams in parameterized route', () => {
// Create a test component that uses Route.useParams()
function TestComponent() {
// This would be available in the actual route component
const params = Route.useParams()
return <div data-testid="param-value">{params.postId}</div>
}
renderWithFileRoutes(<TestComponent />, {
initialLocation: '/posts/abc123',
})
expect(screen.getByTestId('param-value')).toHaveTextContent('abc123')
})
it('should test useLoaderData in route with loader', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/posts/123',
})
// Test that loader data is available in the component
expect(screen.getByText(/Post Title/)).toBeInTheDocument()
})
it('should test useSearch in route with search validation', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/search?q=typescript&sort=date',
})
// Test that search params are correctly parsed
expect(screen.getByDisplayValue('typescript')).toBeInTheDocument()
expect(screen.getByText(/sorted by date/)).toBeInTheDocument()
})
})
import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('File-Based Route Navigation', () => {
it('should navigate between file-based routes', async () => {
const user = userEvent.setup()
const { router } = renderWithFileRoutes(<div />, {
initialLocation: '/',
})
// Initial state - should be on home route
expect(screen.getByText('Welcome Home!')).toBeInTheDocument()
expect(router.state.location.pathname).toBe('/')
// Click navigation link
await user.click(screen.getByRole('link', { name: /about/i }))
// Should navigate to about route
expect(screen.getByText('About')).toBeInTheDocument()
expect(router.state.location.pathname).toBe('/about')
})
it('should handle dynamic route navigation', async () => {
const user = userEvent.setup()
renderWithFileRoutes(<div />, {
initialLocation: '/posts',
})
// Click on a post link (assuming your posts route renders links)
await user.click(screen.getByRole('link', { name: /View Post 1/i }))
// Should navigate to dynamic post route
expect(screen.getByText(/Post 1 Details/)).toBeInTheDocument()
})
it('should handle nested route navigation', async () => {
const user = userEvent.setup()
renderWithFileRoutes(<div />, {
initialLocation: '/dashboard',
})
// Navigate to nested route
await user.click(screen.getByRole('link', { name: /settings/i }))
expect(screen.getByText(/Dashboard Settings/)).toBeInTheDocument()
})
})
import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('Programmatic Navigation', () => {
it('should programmatically navigate between file routes', async () => {
const user = userEvent.setup()
const { router } = renderWithFileRoutes(<div />, {
initialLocation: '/',
})
// Trigger programmatic navigation (button in your component)
await user.click(screen.getByRole('button', { name: /Go to Posts/i }))
expect(router.state.location.pathname).toBe('/posts')
})
it('should navigate with search params', async () => {
const user = userEvent.setup()
const { router } = renderWithFileRoutes(<div />, {
initialLocation: '/search',
})
// Trigger search with params
await user.type(screen.getByRole('textbox'), 'test query')
await user.click(screen.getByRole('button', { name: /search/i }))
expect(router.state.location.search).toMatchObject({
q: 'test query',
})
})
})
import { describe, it, expect, vi } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('File-Based Route Guards', () => {
it('should redirect unauthenticated users from protected routes', () => {
// Mock unauthenticated state
const mockAuth = { isAuthenticated: false, user: null }
renderWithFileRoutes(<div />, {
initialLocation: '/dashboard',
routerContext: { auth: mockAuth },
})
// Should redirect to login (based on your beforeLoad implementation)
expect(screen.getByText(/Please log in/)).toBeInTheDocument()
})
it('should allow authenticated users to access protected routes', () => {
const mockAuth = {
isAuthenticated: true,
user: { id: '1', name: 'John' },
}
renderWithFileRoutes(<div />, {
initialLocation: '/dashboard',
routerContext: { auth: mockAuth },
})
expect(screen.getByText(/Welcome to Dashboard/)).toBeInTheDocument()
})
})
import { describe, it, expect, vi } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('File-Based Route Loaders', () => {
it('should load data for route with loader', async () => {
// Mock the API function used in your route loader
const mockFetchPost = vi.fn().mockResolvedValue({
id: '123',
title: 'Test Post',
content: 'Test content',
})
// If your route loader uses a global API function, mock it
vi.mock('../api/posts', () => ({
fetchPost: mockFetchPost,
}))
renderWithFileRoutes(<div />, {
initialLocation: '/posts/123',
})
await waitFor(() => {
expect(screen.getByText('Test Post')).toBeInTheDocument()
})
expect(mockFetchPost).toHaveBeenCalledWith('123')
})
it('should handle loader errors', async () => {
const mockFetchPost = vi.fn().mockRejectedValue(new Error('Post not found'))
vi.mock('../api/posts', () => ({
fetchPost: mockFetchPost,
}))
renderWithFileRoutes(<div />, {
initialLocation: '/posts/invalid',
})
await waitFor(() => {
expect(screen.getByText(/Error.*Post not found/)).toBeInTheDocument()
})
})
})
import { describe, it, expect } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('File Route Validation', () => {
it('should validate search parameters', () => {
// Test with valid search params
renderWithFileRoutes(<div />, {
initialLocation: '/search?q=react&page=1&sort=date',
})
expect(screen.getByDisplayValue('react')).toBeInTheDocument()
expect(screen.getByText(/Page 1/)).toBeInTheDocument()
})
it('should handle invalid search parameters', () => {
// Test with invalid search params (e.g., invalid page number)
renderWithFileRoutes(<div />, {
initialLocation: '/search?page=invalid&sort=unknown',
})
// Should fall back to defaults based on your validation schema
expect(screen.getByText(/Page 1/)).toBeInTheDocument() // default page
})
it('should validate route parameters', () => {
// Test with valid route param
renderWithFileRoutes(<div />, {
initialLocation: '/posts/123',
})
expect(screen.getByText(/Post 123/)).toBeInTheDocument()
})
})
import { describe, it, expect, vi } from 'vitest'
import { screen } from '@testing-library/react'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('File Route Error Handling', () => {
it('should handle component errors with error boundary', () => {
// Mock console.error to avoid noise in test output
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
// Force an error in a route component
vi.mock('../routes/error-prone.tsx', () => ({
Route: {
component: () => {
throw new Error('Test error')
},
},
}))
renderWithFileRoutes(<div />, {
initialLocation: '/error-prone',
})
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument()
consoleSpy.mockRestore()
})
it('should handle loader errors with error component', async () => {
const mockFailingLoader = vi
.fn()
.mockRejectedValue(new Error('Load failed'))
vi.mock('../api/data', () => ({
loadData: mockFailingLoader,
}))
renderWithFileRoutes(<div />, {
initialLocation: '/data-route',
})
expect(screen.getByText(/Failed to load data/)).toBeInTheDocument()
})
})
import { describe, it, expect } from 'vitest'
import { useNavigate } from '@tanstack/react-router'
import { renderWithFileRoutes } from '../test/file-route-utils'
describe('Generated Route Types', () => {
it('should provide type-safe navigation', () => {
function TestComponent() {
const navigate = useNavigate()
const handleNavigate = () => {
// This should be type-safe based on your generated routes
navigate({
to: '/posts/$postId',
params: { postId: '123' },
search: { tab: 'comments' },
})
}
return (
<button onClick={handleNavigate} data-testid="navigate-btn">
Navigate
</button>
)
}
const { router } = renderWithFileRoutes(<TestComponent />, {
initialLocation: '/',
})
// Test the navigation works correctly
const button = screen.getByTestId('navigate-btn')
fireEvent.click(button)
expect(router.state.location.pathname).toBe('/posts/123')
expect(router.state.location.search).toEqual({ tab: 'comments' })
})
})
import { describe, it, expect } from 'vitest'
import { routeTree } from '../routeTree.gen'
describe('Route Tree Development', () => {
it('should regenerate routes when files change', () => {
// This test ensures your route tree is properly generated
// You can add specific assertions based on your file structure
expect(routeTree).toBeDefined()
expect(typeof routeTree.children).toBe('object')
// Test specific routes exist
const routes = getAllRouteIds(routeTree)
expect(routes).toContain('/')
expect(routes).toContain('/about')
// Add assertions for your specific routes
})
// Helper function to get all route IDs from tree
function getAllRouteIds(tree: any, ids: string[] = []): string[] {
if (tree.id) {
ids.push(tree.id)
}
if (tree.children) {
Object.values(tree.children).forEach((child: any) => {
getAllRouteIds(child, ids)
})
}
return ids
}
})
Create e2e/file-routing.spec.ts:
import { test, expect } from '@playwright/test'
test.describe('File-Based Route E2E', () => {
test('should navigate through file-based route structure', async ({
page,
}) => {
await page.goto('/')
// Test home route (from src/routes/index.tsx)
await expect(page.locator('h3')).toContainText('Welcome Home!')
// Navigate to about route (from src/routes/about.tsx)
await page.click('text=About')
await expect(page).toHaveURL('/about')
await expect(page.locator('h3')).toContainText('About')
// Test browser navigation
await page.goBack()
await expect(page).toHaveURL('/')
})
test('should handle dynamic routes from file structure', async ({ page }) => {
await page.goto('/posts')
// Click on a dynamic post link (from src/routes/posts/$postId.tsx)
await page.click('[data-testid="post-link-1"]')
await expect(page).toHaveURL('/posts/1')
await expect(page.locator('h1')).toContainText('Post 1')
})
test('should handle nested routes', async ({ page }) => {
await page.goto('/dashboard')
// Navigate to nested route (from src/routes/dashboard/settings.tsx)
await page.click('text=Settings')
await expect(page).toHaveURL('/dashboard/settings')
await expect(page.locator('h2')).toContainText('Settings')
})
})
// src/test/mock-file-routes.tsx
import { createFileRoute } from '@tanstack/react-router'
// Mock individual route for isolated testing
export const createMockFileRoute = (
path: string,
component: React.ComponentType,
options: any = {},
) => {
return createFileRoute(path)({
component,
...options,
})
}
// Common test route components
export const TestHomeRoute = createMockFileRoute('/', () => (
<div data-testid="home">Home Page</div>
))
export const TestAboutRoute = createMockFileRoute('/about', () => (
<div data-testid="about">About Page</div>
))
export const TestDynamicRoute = createMockFileRoute('/posts/$postId', () => {
const { postId } = Route.useParams()
return <div data-testid="post">Post {postId}</div>
})
import { describe, it, expect } from 'vitest'
describe('Route Discovery', () => {
it('should discover all routes from file structure', () => {
// Test that your route tree includes all expected routes
// This helps catch when routes are accidentally not being generated
const expectedRoutes = [
'/',
'/about',
'/posts',
'/posts/$postId',
'/dashboard',
'/dashboard/settings',
]
expectedRoutes.forEach((routePath) => {
const routeExists = checkRouteExists(routeTree, routePath)
expect(routeExists).toBe(true)
})
})
})
function checkRouteExists(tree: any, path: string): boolean {
// Implementation to check if route exists in tree
// This depends on your route tree structure
return true // Simplified
}
src/
├── routes/
│ ├── __root.tsx
│ ├── index.tsx
│ ├── about.tsx
│ ├── posts/
│ │ ├── index.tsx
│ │ └── $postId.tsx
├── test/
│ ├── setup.ts
│ ├── file-route-utils.tsx
│ └── routes/
│ ├── index.test.tsx
│ ├── about.test.tsx
│ └── posts/
│ ├── index.test.tsx
│ └── $postId.test.tsx
// Test file for each route file
describe('Posts Route (/posts)', () => {
it('should render posts list', () => {
renderWithFileRoutes(<div />, {
initialLocation: '/posts',
})
expect(screen.getByText(/Posts/)).toBeInTheDocument()
})
it('should handle loading state', () => {
// Test pending state for route with loader
})
it('should handle error state', () => {
// Test error handling for route
})
})
// Test route groups
describe('Dashboard Routes', () => {
describe('/dashboard', () => {
// Dashboard index tests
})
describe('/dashboard/settings', () => {
// Settings route tests
})
})
Problem: Route tree not found in tests
Error: Cannot find module '../routeTree.gen'
Solution: Ensure route tree generation in test setup:
// vitest.config.ts
export default defineConfig({
plugins: [
TanStackRouterVite(), // Ensure this runs before tests
react(),
],
test: {
setupFiles: ['./src/test/setup.ts'],
},
})
Problem: Routes not updating in tests after file changes
Solution: Clear module cache in test setup:
// src/test/setup.ts
beforeEach(() => {
vi.clearAllMocks()
// Clear route tree cache if needed
delete require.cache[require.resolve('../routeTree.gen')]
})
Problem: Type errors in tests with generated routes
Solution: Ensure proper TypeScript configuration:
{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"],
"moduleResolution": "bundler"
},
"include": ["src/**/*", "src/routeTree.gen.ts"]
}
After setting up file-based route testing, you might want to: