Testing Library

Testing Library — это инструмент для тестирования пользовательских интерфейсов, который ориентирован на взаимодействие с компонентами так, как это делает пользователь. В случае с Radix UI это особенно важно, так как библиотека предоставляет низкоуровневые, полностью управляемые компоненты, часто скрывающие внутренние элементы через aria-* атрибуты или portal-рендеринг. Понимание специфики работы Testing Library позволяет создавать надежные тесты без хрупкой зависимости от структуры DOM.

Подход к тестированию компонентов Radix UI

Radix UI предоставляет примитивы, которые управляют состоянием компонентов (например, Dialog, Popover, DropdownMenu). Важно понимать, что напрямую проверять внутренние DOM-структуры нежелательно, вместо этого ориентируются на:

  • Видимые эффекты взаимодействия: открытие/закрытие модальных окон, отображение тултипов, переключение состояний.
  • ARIA-атрибуты: проверка наличия aria-expanded, aria-selected, aria-hidden и других для подтверждения корректной работы интерактивных элементов.
  • События пользователя: клики, фокусировка, навигация с клавиатуры.

Настройка Testing Library с Radix UI

  1. Установка зависимостей:
npm install @testing-library/react @testing-library/jest-dom @testing-library/user-event
  1. Импорт необходимых инструментов:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { Dialog } from '@radix-ui/react-dialog';
  1. Подключение вспомогательных утилит:

Testing Library предоставляет методы, такие как getByRole, queryByText, findByLabelText, которые позволяют находить элементы по семантике, а не по конкретной структуре DOM. Это критично для Radix UI, где компоненты могут использовать порталы.

Примеры тестирования отдельных компонентов

Тестирование Dialog

Dialog в Radix UI обычно рендерится в portal. Прямое обращение к DOM без использования семантики может привести к ошибкам. Правильный подход:

test('открытие и закрытие диалога', async () => {
  render(
    <Dialog>
      <Dialog.Trigger>Открыть диалог</Dialog.Trigger>
      <Dialog.Content>Содержимое диалога</Dialog.Content>
    </Dialog>
  );

  const trigger = screen.getByRole('button', { name: /открыть диалог/i });
  await userEvent.click(trigger);

  const content = await screen.findByText(/содержимое диалога/i);
  expect(content).toBeVisible();

  // Закрытие диалога через ESC
  await userEvent.keyboard('{Escape}');
  expect(content).not.toBeVisible();
});

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

  • Используется findByText, так как рендер через портал может быть асинхронным.
  • Проверка видимости через toBeVisible вместо прямого доступа к DOM.

Тестирование Sel ect (DropdownMenu)

import { DropdownMenu, DropdownMenuTrigger, DropdownMenuItem } fr om '@radix-ui/react-dropdown-menu';

test('выбор элемента в DropdownMenu', async () => {
  render(
    <DropdownMenu>
      <DropdownMenuTrigger>Меню</DropdownMenuTrigger>
      <DropdownMenuItem>Элемент 1</DropdownMenuItem>
      <DropdownMenuItem>Элемент 2</DropdownMenuItem>
    </DropdownMenu>
  );

  const trigger = screen.getByRole('button', { name: /меню/i });
  await userEvent.click(trigger);

  const item = screen.getByText(/элемент 2/i);
  await userEvent.click(item);

  expect(item).toHaveFocus();
});

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

  • DropdownMenu рендерится через портал, поэтому нельзя использовать container.querySelector.
  • Используются методы поиска по тексту и роли для имитации реального взаимодействия пользователя.

Работа с асинхронными состояниями

Многие компоненты Radix UI используют анимации или отложенные рендеры. Для тестирования следует применять:

  • findBy* методы для асинхронного поиска элементов.
  • waitFor для ожидания изменений состояния или появления элементов:
import { waitFor } from '@testing-library/react';

await waitFor(() => expect(screen.getByText(/содержимое/i)).toBeVisible());

Это предотвращает ложные отрицательные результаты тестов.

Проверка ARIA-атрибутов

Radix UI активно использует ARIA для управления доступностью. В тестах важно проверять корректность этих атрибутов:

const trigger = screen.getByRole('button', { name: /меню/i });
expect(trigger).toHaveAttribute('aria-expanded', 'false');

await userEvent.click(trigger);
expect(trigger).toHaveAttribute('aria-expanded', 'true');

Интеграция с User Event

@testing-library/user-event обеспечивает более реалистичное моделирование действий пользователя, чем fireEvent. Для Radix UI это критично при работе с:

  • Фокусировкой и клавиатурной навигацией
  • Перетаскиванием и кликами по элементам, которые могут быть скрыты через порталы
  • Множественными последовательными действиями (например, открытие меню → выбор элемента → закрытие меню)

Рекомендации по написанию тестов для Radix UI

  1. Использовать семантические селекторы (getByRole, getByLabelText) вместо CSS-классов.
  2. Проверять видимые эффекты взаимодействия, а не внутреннюю структуру DOM.
  3. Работать с асинхронными методами findBy* и waitFor при работе с порталами и анимациями.
  4. Проверять ARIA-атрибуты для подтверждения корректного состояния компонентов.
  5. Имитация действий пользователя должна проходить через userEvent, а не fireEvent, чтобы сохранить естественность сценариев.

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