Навигация между страницами в тестах

В React-приложениях с использованием маршрутизации (React Router, Next.js или других библиотек) тестирование компонентов, зависящих от навигации, требует особого подхода. Важно проверять не только рендеринг компонентов, но и корректное изменение URL, переходы между страницами и реакцию интерфейса на маршруты.

Настройка тестовой среды для маршрутизации

Для тестирования навигации необходимо обернуть компоненты в провайдер маршрутизатора. В случае React Router используется MemoryRouter, который позволяет управлять историей переходов в тестовой среде.

import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import App from './App';

render(
  <MemoryRouter initialEntries={['/']}>
    <App />
  </MemoryRouter>
);

Ключевые моменты:

  • initialEntries задаёт начальный путь, с которого начинается тест.
  • MemoryRouter не взаимодействует с настоящим URL браузера, что делает тесты изолированными.
  • Для проверки навигации можно использовать history или библиотеку user-event для симуляции кликов по ссылкам.

Проверка переходов по ссылкам

Для эмуляции кликов по ссылкам используется библиотека @testing-library/user-event. Она позволяет воспроизводить пользовательские действия максимально близко к реальному поведению браузера.

import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import App from './App';

render(
  <MemoryRouter initialEntries={['/']}>
    <App />
  </MemoryRouter>
);

userEvent.click(screen.getByText('О нас'));
expect(screen.getByText('Страница о компании')).toBeInTheDocument();

Особенности тестирования:

  • Проверяется не только факт клика, но и рендер компонента, связанного с целевым маршрутом.
  • Тест остаётся изолированным: нет зависимости от реального DOM-истории.

Навигация с использованием useNavigate и программных переходов

Компоненты могут использовать useNavigate для программной навигации. Для тестирования таких переходов важно проверять конечное состояние приложения после вызова функции.

import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import NavigationButton from './NavigationButton';

render(
  <MemoryRouter initialEntries={['/']}>
    <NavigationButton />
  </MemoryRouter>
);

userEvent.click(screen.getByRole('button', { name: /Перейти/i }));
expect(screen.getByText('Целевая страница')).toBeInTheDocument();

Подчёркивается:

  • Важно проверять результат навигации через DOM, а не через внутреннее состояние history.
  • Это делает тесты независимыми от реализации маршрутизации и более устойчивыми к изменениям.

Тестирование редиректов и защищённых маршрутов

Для компонентов, защищённых авторизацией, часто используется редирект. Проверка требует моделирования состояния пользователя.

import { render, screen } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import ProtectedPage from './ProtectedPage';
import LoginPage from './LoginPage';

const isAuthenticated = false;

render(
  <MemoryRouter initialEntries={['/protected']}>
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route path="/protected" element={isAuthenticated ? <ProtectedPage /> : <LoginPage />} />
    </Routes>
  </MemoryRouter>
);

expect(screen.getByText('Страница входа')).toBeInTheDocument();

Ключевые моменты:

  • Симулируется условие авторизации без необходимости реальной проверки сервера.
  • Тест проверяет поведение навигации при разных состояниях приложения.

Проверка истории переходов

MemoryRouter позволяет отслеживать историю переходов для более сложных сценариев.

import { render, screen } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import App from './App';

const historyEntries = ['/home'];

render(
  <MemoryRouter initialEntries={historyEntries}>
    <App />
  </MemoryRouter>
);

userEvent.click(screen.getByText('Профиль'));
expect(screen.getByText('Страница профиля')).toBeInTheDocument();

Особенности:

  • initialEntries задаёт стек истории.
  • Можно проверять последовательность переходов, используя initialIndex и последовательно вызывая userEvent.click.

Эмуляция навигации с query-параметрами и динамическими маршрутами

React Router поддерживает маршруты с параметрами (/user/:id). Тестирование таких страниц требует передачи конкретных значений параметров в initialEntries.

import { render, screen } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import UserPage from './UserPage';

render(
  <MemoryRouter initialEntries={['/user/42']}>
    <Routes>
      <Route path="/user/:id" element={<UserPage />} />
    </Routes>
  </MemoryRouter>
);

expect(screen.getByText('Пользователь 42')).toBeInTheDocument();

Важно учитывать:

  • Параметры маршрута можно проверять через компонент или через URL.
  • Для query-параметров удобно использовать window.location.search или библиотеку query-string.

Асинхронные переходы

Некоторые страницы могут загружать данные после навигации. В этом случае используется findBy* из React Testing Library, чтобы дождаться появления элементов.

import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import App from './App';
import userEvent from '@testing-library/user-event';

render(
  <MemoryRouter initialEntries={['/']}>
    <App />
  </MemoryRouter>
);

userEvent.click(screen.getByText('Новости'));
const article = await screen.findByText('Главная новость');
expect(article).toBeInTheDocument();

Особенности:

  • Асинхронное ожидание важно при загрузке данных с сервера.
  • findBy* автоматически ждёт появления элемента до таймаута.

Рекомендации по организации тестов навигации

  • Использовать MemoryRouter вместо BrowserRouter для изоляции тестов.
  • Проверять переходы через DOM, а не через внутреннюю историю, для устойчивости тестов.
  • Симулировать пользовательские действия с помощью user-event вместо прямого вызова функций маршрутизатора.
  • Разделять тесты на сценарии: простые клики, программные переходы, защищённые маршруты, асинхронные загрузки.

Эти подходы позволяют создавать надёжные тесты React-приложений с навигацией, обеспечивая проверку функциональности маршрутов без зависимости от внешней среды браузера.