Работа с drag and drop

Drag and Drop (перетаскивание элементов) является одной из более сложных интерактивных функциональностей для тестирования в React-приложениях. Стандартные методы взаимодействия с DOM, такие как fireEvent.click или fireEvent.change, часто недостаточны для корректной симуляции полного процесса перетаскивания. React Testing Library предоставляет инструменты для работы с событиями, но реализация drag and drop требует понимания нативных событий браузера и их последовательности.

Основные события Drag and Drop

Для реализации drag and drop браузер использует несколько событий:

  • dragstart — возникает, когда начинается перетаскивание элемента.
  • drag — непрерывно срабатывает во время перемещения элемента.
  • dragenter — срабатывает, когда перетаскиваемый элемент входит в область другого элемента.
  • dragover — срабатывает при нахождении элемента над зоной, в которую его можно сбросить. Для разрешения drop необходимо вызвать event.preventDefault().
  • dragleave — возникает, когда элемент покидает область цели.
  • drop — происходит, когда элемент сбрасывается в целевой контейнер.
  • dragend — срабатывает по завершении операции, независимо от успешного сброса.

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

Симуляция Drag and Drop в React Testing Library

React Testing Library не предоставляет встроенного метода dragAndDrop, поэтому приходится использовать низкоуровневые события через fireEvent или userEvent. Последовательность действий выглядит следующим образом:

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

test('Перетаскивание элемента', async () => {
  render(<MyComponent />);
  const draggable = screen.getByTestId('draggable');
  const droppable = screen.getByTestId('droppable');

  fireEvent.dragStart(draggable);
  fireEvent.dragEnter(droppable);
  fireEvent.dragOver(droppable);
  fireEvent.drop(droppable);
  fireEvent.dragEnd(draggable);

  expect(droppable).toHaveTextContent('Элемент перемещён');
});

Важный момент: dragOver должен содержать preventDefault, иначе drop не сработает. В тестах это можно эмулировать вручную:

fireEvent.dragOver(droppable, { preventDefault: () => {} });

Использование DataTransfer

HTML5 Drag and Drop работает с объектом DataTransfer, который хранит данные перетаскиваемого элемента. Без передачи этого объекта многие библиотеки и кастомные компоненты не смогут корректно обработать drop. Пример с передачей DataTransfer:

const data = new DataTransfer();

fireEvent.dragStart(draggable, { dataTransfer: data });
fireEvent.dragEnter(droppable, { dataTransfer: data });
fireEvent.dragOver(droppable, { dataTransfer: data });
fireEvent.drop(droppable, { dataTransfer: data });
fireEvent.dragEnd(draggable, { dataTransfer: data });

Использование DataTransfer обеспечивает правильное взаимодействие с компонентами, которые ожидают доступ к данным перетаскиваемого элемента через event.dataTransfer.

Альтернатива через userEvent

Библиотека @testing-library/user-event обеспечивает более высокоуровневую имитацию пользовательских действий. Для drag and drop можно использовать комбинацию pointer событий:

await userEvent.pointer([
  { keys: '[MouseLeft]', target: draggable, type: 'pointerDown' },
  { target: droppable, type: 'pointerMove' },
  { keys: '[/MouseLeft]', target: droppable, type: 'pointerUp' },
]);

Такой подход ближе к реальному поведению пользователя и учитывает нюансы браузерного API. Особенно полезен при тестировании интеграции с библиотеками drag and drop, такими как react-beautiful-dnd или dnd-kit.

Особенности работы с библиотеками drag and drop

  • react-beautiful-dnd требует правильного контекста Droppable и Draggable. Тесты должны рендерить всю структуру, иначе события не обработаются.
  • dnd-kit использует кастомные события и внутренние хуки. В тестах достаточно симулировать pointer-события через userEvent, передавая координаты перемещения.
  • Проверка результата должна происходить через изменение состояния DOM или вызовы колбэков, а не через внутренние свойства компонента.

Проверка визуального и логического результата

Drag and drop чаще всего меняет порядок элементов или перемещает их в другую область. Проверка включает:

  • Содержимое целевого контейнера (toHaveTextContent, toContainElement).
  • Порядок элементов (container.querySelectorAll и сравнение массивов).
  • Вызовы callback-функций (jest.fn() для проверки onDragEnd или onDrop).

Частые ошибки и нюансы

  1. Пропуск dragOver с preventDefault — drop не срабатывает.
  2. Отсутствие DataTransfer — многие кастомные компоненты игнорируют событие.
  3. Использование click вместо pointer — не симулирует реальные действия пользователя.
  4. Проверка состояния до завершения drag-end — тест может вернуть ложноположительный результат.

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

  • Всегда эмулировать полную цепочку событий.
  • Использовать DataTransfer для совместимости с HTML5 API.
  • Предпочтительнее userEvent.pointer для интеграционных тестов.
  • Проверка должна быть ориентирована на итоговое состояние интерфейса или вызываемые колбэки.

Drag and drop требует внимательного подхода, так как простая имитация клика или изменения DOM не отражает реальное поведение. Сочетание fireEvent и userEvent обеспечивает гибкость и точность тестов, а использование DataTransfer и правильной последовательности событий гарантирует корректную симуляцию.