File size: 5,790 Bytes
c0af099 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | # Testing with React Router
## Overview
React Router components and hooks require a routing context to function. In tests, we need to provide this context while maintaining control over the routing state.
This guide covers the two main approaches used in the OpenHands frontend:
1. **`createRoutesStub`** - Creates a complete route structure for testing components with their actual route configuration, loaders, and nested routes.
2. **`MemoryRouter`** - Provides a minimal routing context for components that just need router hooks to work.
Choose your approach based on what your component actually needs from the router.
## When to Use Each Approach
### `createRoutesStub` (Recommended)
Use `createRoutesStub` when your component:
- Relies on route parameters (`useParams`)
- Uses loader data (`useLoaderData`) or `clientLoader`
- Has nested routes or uses `<Outlet />`
- Needs to test navigation between routes
> [!NOTE]
> `createRoutesStub` is intended for unit testing **reusable components** that depend on router context. For testing full route/page components, consider E2E tests (Playwright, Cypress) instead.
```typescript
import { createRoutesStub } from "react-router";
import { render } from "@testing-library/react";
const RouterStub = createRoutesStub([
{
Component: MyRouteComponent,
path: "/conversations/:conversationId",
},
]);
render(<RouterStub initialEntries={["/conversations/123"]} />);
```
**With nested routes and loaders:**
```typescript
const RouterStub = createRoutesStub([
{
Component: SettingsScreen,
clientLoader,
path: "/settings",
children: [
{
Component: () => <div data-testid="llm-settings" />,
path: "/settings",
},
{
Component: () => <div data-testid="mcp-settings" />,
path: "/settings/mcp",
},
],
},
]);
render(<RouterStub initialEntries={["/settings/mcp"]} />);
```
> [!TIP]
> When using `clientLoader` from a Route module, you may encounter type mismatches. Use `@ts-expect-error` as a workaround:
```typescript
import { clientLoader } from "@/routes/settings";
const RouterStub = createRoutesStub([
{
path: "/settings",
Component: SettingsScreen,
// @ts-expect-error: loader types won't align between test and app code
loader: clientLoader,
},
]);
```
### `MemoryRouter`
Use `MemoryRouter` when your component:
- Only needs basic routing context to render
- Uses `<Link>` components but you don't need to test navigation
- Doesn't depend on specific route parameters or loaders
```typescript
import { MemoryRouter } from "react-router";
import { render } from "@testing-library/react";
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
```
**With initial route:**
```typescript
render(
<MemoryRouter initialEntries={["/some/path"]}>
<MyComponent />
</MemoryRouter>
);
```
## Anti-patterns to Avoid
### Using `BrowserRouter` in tests
`BrowserRouter` interacts with the actual browser history API, which can cause issues in test environments:
```typescript
// ❌ Avoid
render(
<BrowserRouter>
<MyComponent />
</BrowserRouter>
);
// ✅ Use MemoryRouter instead
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
```
### Mocking router hooks when `createRoutesStub` would work
Mocking hooks like `useParams` directly can be brittle and doesn't test the actual routing behavior:
```typescript
// ❌ Avoid when possible
vi.mock("react-router", async () => {
const actual = await vi.importActual("react-router");
return {
...actual,
useParams: () => ({ conversationId: "123" }),
};
});
// ✅ Prefer createRoutesStub - tests real routing behavior
const RouterStub = createRoutesStub([
{
Component: MyComponent,
path: "/conversations/:conversationId",
},
]);
render(<RouterStub initialEntries={["/conversations/123"]} />);
```
## Common Patterns
### Combining with `QueryClientProvider`
Many components need both routing and TanStack Query context:
```typescript
import { createRoutesStub } from "react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
const RouterStub = createRoutesStub([
{
Component: MyComponent,
path: "/",
},
]);
render(<RouterStub />, {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
),
});
```
### Testing navigation behavior
Verify that user interactions trigger the expected navigation:
```typescript
import { createRoutesStub } from "react-router";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
const RouterStub = createRoutesStub([
{
Component: HomeScreen,
path: "/",
},
{
Component: () => <div data-testid="settings-screen" />,
path: "/settings",
},
]);
render(<RouterStub initialEntries={["/"]} />);
const user = userEvent.setup();
await user.click(screen.getByRole("link", { name: /settings/i }));
expect(screen.getByTestId("settings-screen")).toBeInTheDocument();
```
## See Also
### Codebase Examples
- [settings.test.tsx](routes/settings.test.tsx) - `createRoutesStub` with nested routes and loaders
- [root-layout.test.tsx](routes/root-layout.test.tsx) - `createRoutesStub` with `initialEntries` navigation
- [chat-interface.test.tsx](components/chat/chat-interface.test.tsx) - `MemoryRouter` usage
### Official Documentation
- [React Router Testing Guide](https://reactrouter.com/start/framework/testing) - Official guide on testing with `createRoutesStub`
- [MemoryRouter API](https://reactrouter.com/api/declarative-routers/MemoryRouter) - API reference for `MemoryRouter`
|