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`