Testing React Router v7 routes no longer requires juggling between mock setups and full integration suites. A hybrid approach leverages the framework’s own utilities while running tests in a real browser environment. This method balances speed with accuracy, eliminating the need for separate test stacks or headless environments.
Why existing testing paths fall short
The official React Router documentation outlines two primary testing strategies, each with notable limitations. The first relies on createRoutesStub paired with @testing-library/react, which excels for isolated components consuming router hooks but explicitly warns against testing full route components. The warning stems from incompatibility with framework-mode Route.* types, effectively halting progress for route-level validation.
The second path directs developers toward Playwright or Cypress for end-to-end testing. While effective for simulating user journeys, this approach disrupts the development workflow. It introduces a separate test infrastructure, lengthens feedback cycles, and shifts testing into a QA-centric rather than developer-centric paradigm. The result is slower iteration and reduced agility during debugging.
Bridging the gap with real-time testing
A third option exists: using createRoutesStub within the actual browser environment. This approach renders tests in the live development server, against the real DOM, and maintains the developer loop. The TWD tool facilitates this by integrating the framework’s utility into a real-time testing sidebar. Tests run against the active page without requiring jsdom, headless Chrome, or separate processes.
The sidebar displays test results alongside the application, enabling immediate feedback. Developers can write route-level tests with full access to hooks like useLoaderData, useParams, and useMatches, all operating within the same rendering context as the application.
Setting up the TWD integration
Installation begins with adding two development dependencies.
npm install --save-dev twd-jsNext, initialize the tool in the project’s public directory.
npx twd-js init public --saveThe initialization step prepares the environment for real-time testing. For Vite-based projects, a plugin must be added to the configuration file to enable TWD’s functionality.
// vite.config.ts
import { reactRouter } from '@react-router/dev/vite'
import { defineConfig } from 'vite'
import { twd } from 'twd-js/vite-plugin'
export default defineConfig({
plugins: [
reactRouter(),
twd({
testFilePattern: '/**/*.twd.test.{ts,tsx}',
}),
],
})A minor adjustment ensures compatibility with React Router’s SSR mode. The framework renders HTML from app/root.tsx, bypassing Vite’s index.html transformation. To inject the TWD sidebar script, add a conditional block within the <head> section.
// app/root.tsx
<head>
<Meta />
<Links />
{import.meta.env.DEV && (
<script type="module" src="/@id/virtual:twd/init" />
)}
</head>The import.meta.env.DEV guard ensures the script is excluded from production builds. With the setup complete, running npm run dev launches the application with the TWD sidebar active.
Creating a dedicated test mounting route
To mount tests, a dedicated route serves as a container within the application. This route, typically named /testing, provides a clean DOM node for React to render test components.
// app/routes/testing-page.tsx
export default function TestPage() {
return (
<div
data-testid="testing-page"
style={{ minHeight: '100vh' }}
/>
)
}Add this route to the application’s route table. A utility function simplifies test setup by navigating to the /testing route and creating a fresh React root before each test.
// app/twd-tests/utils.ts
import { createRoot } from 'react-dom/client'
import { twd, screenDomGlobal } from 'twd-js'
let root: ReturnType<typeof createRoot> | undefined
export async function setupReactRoot() {
if (root) {
root.unmount()
root = undefined
}
await twd.visit('/testing')
const container = await screenDomGlobal.findByTestId('testing-page')
root = createRoot(container)
return root
}This utility handles root cleanup and recreation, ensuring a pristine environment for each test case.
Writing route-level tests with real DOM access
Route-level tests now mirror the patterns recommended by React Router’s documentation but execute within the live application. The following example demonstrates testing a todo list page, including loader data and form submissions.
// app/twd-tests/todoList.twd.test.tsx
import { twd, expect, userEvent, screenDom } from 'twd-js'
import { describe, it, beforeEach } from 'twd-js/runner'
import { createRoutesStub, useLoaderData, useParams, useMatches } from 'react-router'
import TodoListPage from '~/routes/todolist'
import todoListMock from './mocks/todoList.json'
import { setupReactRoot } from './utils'
describe('Todo List page', () => {
let root: Awaited<ReturnType<typeof setupReactRoot>>
beforeEach(async () => {
root = await setupReactRoot()
})
it('renders todos from the loader', async () => {
const Stub = createRoutesStub([
{
path: '/',
Component: () => {
const loaderData = useLoaderData()
const params = useParams()
const matches = useMatches() as any
return (
<TodoListPage
loaderData={loaderData}
params={params}
matches={matches}
/>
)
},
loader() {
return { todos: todoListMock }
},
},
])
root.render(<Stub />)
await twd.wait(300)
const firstTodoTitle = await screenDom.getByText('Learn TWD')
twd.should(firstTodoTitle, 'be.visible')
const firstTodoDate = await screenDom.getByText('Date: 2024-12-20')
twd.should(firstTodoDate, 'be.visible')
})
it('submits the create-todo action with the right payload', async () => {
let payload: Record<string, string> | null = null
const Stub = createRoutesStub([
{
path: '/todos',
Component: () => null,
loader() {
return { todos: [] }
},
async action({ request }) {
const formData = await request.formData()
payload = Object.fromEntries(formData) as Record<string, string>
return null
},
},
])
root.render(<Stub initialEntries={['/todos']} />)
const titleInput = await screenDom.getByLabelText('Title')
await userEvent.type(titleInput, 'Test Todo')
const submitButton = await screenDom.getByRole('button', { name: 'Create Todo' })
await userEvent.click(submitButton)
expect(payload).to.deep.equal({ title: 'Test Todo' })
})
})The test suite validates both data loading and form interactions, leveraging the real DOM and React Router’s full feature set. This approach eliminates gaps between mocked tests and actual user experiences while preserving rapid feedback loops.
As React Router v7 evolves, maintaining a balance between thorough testing and development efficiency remains critical. Tools like TWD bridge this divide, enabling developers to validate routes in the environment where they ultimately run.
AI summary
React Router v7 projelerinizdeki loader, action ve form bileşenlerini TWD kullanarak gerçek tarayıcıda ve canlı geliştirme ortamında test edin. Kurulum adımları ve pratik örneklerle hızlı geri bildirim alın.