From 9049b957925d367ec5bff54a38c1a5453e936ee3 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:21:55 +0400 Subject: [PATCH] docs(frontend): React Router testing guide (#12145) --- frontend/__tests__/router.md | 227 +++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 frontend/__tests__/router.md diff --git a/frontend/__tests__/router.md b/frontend/__tests__/router.md new file mode 100644 index 0000000000..b23b4364e7 --- /dev/null +++ b/frontend/__tests__/router.md @@ -0,0 +1,227 @@ +# 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 `` +- 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(); +``` + +**With nested routes and loaders:** + +```typescript +const RouterStub = createRoutesStub([ + { + Component: SettingsScreen, + clientLoader, + path: "/settings", + children: [ + { + Component: () =>
, + path: "/settings", + }, + { + Component: () =>
, + path: "/settings/integrations", + }, + ], + }, +]); + +render(); +``` + +> [!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 `` 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( + + + +); +``` + +**With initial route:** + +```typescript +render( + + + +); +``` + +## 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( + + + +); + +// ✅ Use MemoryRouter instead +render( + + + +); +``` + +### 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(); +``` + +## 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(, { + wrapper: ({ children }) => ( + + {children} + + ), +}); +``` + +### 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: () =>
, + path: "/settings", + }, +]); + +render(); + +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](__tests__/routes/settings.test.tsx) - `createRoutesStub` with nested routes and loaders +- [home-screen.test.tsx](__tests__/routes/home-screen.test.tsx) - `createRoutesStub` with navigation testing +- [chat-interface.test.tsx](__tests__/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`