Portal компоненты: модали, тосты

В React часто возникает необходимость отображать элементы, которые должны быть визуально “выведены” за пределы текущего контекста компонента, сохраняя при этом его логику. Примером таких элементов являются модальные окна и тосты. В этой главе рассматривается подход, который используется для создания и тестирования таких компонентов с помощью React Testing Library и возможностей React Portals.

Что такое Portal в React

React Portal позволяет рендерить дочерние элементы в другом DOM-узле, которое находится за пределами родительского компонента. Это полезно, когда необходимо, чтобы компонент, например, модальное окно или тост, был размещён в другом месте DOM-дерева, но при этом продолжал использовать контекст родительского компонента.

Использование портала позволяет избежать таких проблем, как неправильное позиционирование в случае использования модальных окон, всплывающих подсказок, контекстных меню и т.п. Они могут быть рендерены в body или в любой другой контейнер, что обеспечит правильное отображение поверх других элементов страницы.

Пример использования Portals:

import ReactDOM from 'react-dom';

const Modal = ({ children }) => {
  return ReactDOM.createPortal(
    <div className="modal">
      {children}
    </div>,
    document.body
  );
};

Здесь компонент Modal рендерится в body вне зависимости от того, где он был вызван в компонентном дереве.

Тестирование модальных окон с использованием React Testing Library

Для тестирования компонентов, использующих Portals, важно понимать, что стандартные методы тестирования, такие как getByText, могут не сработать сразу, так как элементы могут быть расположены вне стандартной области видимости. React Testing Library предоставляет полезные методы, которые позволяют взаимодействовать с такими компонентами.

Проверка наличия модального окна

Пример теста для проверки, что модальное окно появляется в DOM:

import { render, screen, fireEvent } from '@testing-library/react';
import Modal from './Modal';

test('проверка отображения модального окна', () => {
  render(
    <Modal>
      <h1>Модальное окно</h1>
    </Modal>
  );

  // Проверка, что модальное окно отобразилось в DOM
  expect(screen.getByText('Модальное окно')).toBeInTheDocument();
});

В данном примере, getByText находит элемент с текстом внутри модального окна, даже если оно рендерится с использованием Portals.

Тестирование взаимодействий с модальным окном

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

import { render, screen, fireEvent } from '@testing-library/react';
import Modal from './Modal';

test('закрытие модального окна при нажатии на кнопку закрытия', () => {
  const { rerender } = render(
    <Modal>
      <button>Закрыть</button>
    </Modal>
  );

  // Имитируем клик по кнопке
  fireEvent.click(screen.getByText('Закрыть'));

  // Проверка, что модальное окно больше не отображается
  expect(screen.queryByText('Модальное окно')).toBeNull();
});

Для имитации клика по элементу используется fireEvent, а для проверки, что элемент больше не отображается в DOM, применяем queryByText. Это важно, потому что queryByText возвращает null, если элемент не найден, в отличие от getByText, который выбрасывает ошибку.

Тосты и уведомления

Тосты (или уведомления) — это короткие всплывающие сообщения, которые обычно исчезают через несколько секунд. Часто их используют для отображения статусов или предупреждений в интерфейсе.

Тестирование тостов имеет схожие принципы, однако важно учитывать временной аспект — они появляются и исчезают в течение времени. Это требует использования асинхронных методов тестирования.

Пример тестирования тостов

Для реализации и тестирования простого тоста, который появляется на экране и исчезает через некоторое время, можно воспользоваться асинхронным ожиданием с использованием waitFor.

import { render, screen, waitFor } from '@testing-library/react';
import Toast from './Toast';

test('отображение тоста и его скрытие через время', async () => {
  render(<Toast message="Тост появился!" />);

  // Проверяем, что тост отобразился на экране
  expect(screen.getByText('Тост появился!')).toBeInTheDocument();

  // Ждём исчезновения тоста
  await waitFor(() => {
    expect(screen.queryByText('Тост появился!')).toBeNull();
  }, { timeout: 3000 });
});

В этом примере waitFor используется для ожидания, пока тост исчезнет. Важно установить правильное время таймаута, которое соответствует длительности отображения тоста.

Тосты с асинхронными действиями

Тосты могут быть связаны с асинхронными операциями, например, с запросами на сервер. В таких случаях важно проверить как компонент реагирует на изменение состояния.

import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import Toast from './Toast';

test('тост отображается после успешного запроса', async () => {
  const fetchSuccess = jest.fn().mockResolvedValue('Успех');
  
  render(<Toast fetchData={fetchSuccess} />);

  // Имитируем действие, которое вызовет асинхронный запрос
  fireEvent.click(screen.getByText('Получить тост'));

  // Ожидаем появления тоста
  await waitFor(() => {
    expect(screen.getByText('Успешно!')).toBeInTheDocument();
  });
});

Здесь мы тестируем компонент тоста, который отображается после выполнения асинхронного запроса. Для мокирования асинхронных функций используется jest.fn().mockResolvedValue.

Обработка состояния и асинхронных событий

При тестировании компонентов, использующих асинхронные события, таких как получение данных с сервера или задержка перед исчезновением тоста, важно использовать такие методы, как waitFor, findBy, и асинхронные утверждения. Эти инструменты позволяют ожидать появления и исчезновения элементов в DOM, что критично для корректного тестирования динамически изменяющихся компонентов.

Учет стилей при тестировании модальных окон и тостов

Стилизованные компоненты также поддаются тестированию. Например, модальное окно может иметь уникальные стили для отображения, такие как затемнение фона или центровка окна. В таких случаях можно проверить наличие нужных классов:

import { render, screen } from '@testing-library/react';
import Modal from './Modal';

test('проверка стиля модального окна', () => {
  render(<Modal />);
  
  const modalElement = screen.getByRole('dialog');
  
  // Проверка, что модальное окно имеет нужный стиль
  expect(modalElement).toHaveStyle('background-color: rgba(0, 0, 0, 0.5)');
});

В этом тесте проверяется наличие конкретного стиля у модального окна. React Testing Library позволяет работать с DOM-элементами, как с обычными элементами, добавляя возможности для проверки стилей с помощью метода toHaveStyle.

Заключение

Portal компоненты, такие как модальные окна и тосты, играют ключевую роль в взаимодействии с пользователем в современных интерфейсах. Использование React Testing Library помогает обеспечить корректность их работы, даже если они рендерятся в другом месте DOM. Важно понимать, как взаимодействовать с такими элементами и правильно тестировать их отображение и функциональность с учётом асинхронных операций, времени жизни компонентов и их стилей.