Выбор из dropdown и select элементов

React Testing Library (RTL) предоставляет мощные средства для тестирования взаимодействий с формами, включая выпадающие списки (<select>) и кастомные dropdown-компоненты. Важным аспектом является проверка не только визуального отображения, но и корректного обновления состояния приложения после выбора элемента.


Поиск элементов <select> и <option>

Для начала необходимо корректно найти элементы на странице. RTL рекомендует ориентироваться на роль и текст, а не на классы или id:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyForm from './MyForm';

render(<MyForm />);

// Находим сам SELECT по роли
const selectElement = screen.getByRole('combobox');

// Проверка наличия конкретной опции
const optionElement = screen.getByRole('option', { name: 'Опция 1' });

Ключевой момент: для <select> роль всегда combobox, а для <option>option. Это универсально для всех браузеров и компонентов.


Симуляция выбора опции

Выбор значения в <select> можно осуществить с помощью userEvent.selectOptions, что корректно триггерит все события (change, input) и обновляет состояние:

import { render, screen } FROM '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyForm from './MyForm';

render(<MyForm />);

const selectElement = screen.getByRole('combobox');

// Выбор одной опции
userEvent.selectOptions(selectElement, 'Опция 2');

// Проверка, что выбранная опция активна
expect(screen.getByRole('option', { name: 'Опция 2' }).selected).toBe(true);

Если нужно выбрать несколько опций в множественном SELECT (<SELECT multiple>), можно передать массив:

userEvent.selectOptions(selectElement, ['Опция 1', 'Опция 3']);
expect(screen.getByRole('option', { name: 'Опция 1' }).selected).toBe(true);
expect(screen.getByRole('option', { name: 'Опция 3' }).selected).toBe(true);

Тестирование кастомных dropdown

Кастомные dropdown, реализованные через div/span и контролируемые стейтом, не имеют стандартной роли combobox или option. В таких случаях важно искать элементы по тексту или test-id:

render(<CustomDropdown />);

// Открываем dropdown
const toggle = screen.getByText('Выберите элемент');
userEvent.click(toggle);

// Выбираем элемент
const item = screen.getByText('Элемент 2');
userEvent.click(item);

// Проверка обновленного значения
expect(screen.getByText('Элемент 2')).toBeInTheDocument();

Для кастомных dropdown часто требуется сначала симулировать клик по кнопке открытия, а затем клик по элементу списка. RTL не имеет встроенной функции selectOptions для таких случаев.


Асинхронные dropdown

Если dropdown загружает данные асинхронно (например, с сервера), необходимо использовать findBy или waitFor:

import { render, screen, waitFor } FROM '@testing-library/react';
import userEvent FROM '@testing-library/user-event';
import AsyncDropdown from './AsyncDropdown';

render(<AsyncDropdown />);

userEvent.click(screen.getByText('Выберите элемент'));

// Ждем появления асинхронной опции
const option = await screen.findByText('Асинхронная опция');
userEvent.click(option);

expect(screen.getByText('Асинхронная опция')).toBeInTheDocument();

await screen.findByText автоматически ждет появления элемента в DOM до таймаута по умолчанию. Использование waitFor позволяет обернуть проверки, которые могут не произойти мгновенно.


Проверка вызова обработчиков

Очень важно проверять, что при выборе элемента вызываются правильные функции:

const handleChange = jest.fn();
render(<MyForm onCha nge={handleChange} />);

const selectElement = screen.getByRole('combobox');
userEvent.selectOptions(selectElement, 'Опция 1');

expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(expect.any(Object));

Здесь проверяется, что обработчик события onChange срабатывает один раз и получает объект события.


Практические рекомендации

  • Предпочтительно использовать роли: combobox и option для стандартных <select>.
  • Для кастомных dropdown использовать текст или data-testid, избегая зависимости от классов.
  • Асинхронные данные требуют findBy или waitFor.
  • Проверка состояния и вызова обработчиков важнее визуальной проверки.
  • Множественный select требует передачи массива выбранных значений.
  • События change триггерятся только при использовании userEvent.selectOptions, не fireEvent.change — это гарантирует реалистичное поведение.

Использование этих подходов позволяет покрывать тестами все виды dropdown-выборов, от простых <select> до сложных кастомных компонентов с асинхронной подгрузкой данных, сохраняя стабильность тестов и реалистичность пользовательских сценариев.