Drag and drop в формах

Cypress не имеет встроенной команды для прямого перетаскивания элементов, как это реализовано в некоторых других фреймворках, поэтому для тестирования функционала drag and drop требуется использование низкоуровневых событий браузера: mousedown, mousemove и mouseup. Эти события эмулируют поведение пользователя при перетаскивании элементов на странице.

Для начала необходимо определить целевой элемент и место назначения. Например:

cy.get('#draggable')       // элемент, который нужно перетащить
  .trigger('mousedown', { which: 1 }) // имитация нажатия левой кнопкой мыши
cy.get('#droppable')       // место, куда нужно перенести элемент
  .trigger('mousemove')    // перемещение мыши
  .trigger('mouseup', { force: true }) // отпускание мыши

Важный момент — использование { force: true } для mouseup, когда элемент может быть перекрыт другими элементами, что часто встречается в современных UI с анимациями.


Работа с координатами

Для более точного контроля над перемещением можно использовать координаты мыши. Cypress позволяет передавать clientX и clientY в событии:

cy.get('#draggable')
  .trigger('mousedown', { which: 1, pageX: 100, pageY: 100 })
  .trigger('mousemove', { pageX: 200, pageY: 200 })
  .trigger('mouseup')

Использование координат полезно при тестировании сложных интерфейсов с сетками, таблицами или drag and drop внутри канбан-досок, где позиция цели критична.


Cypress и сторонние плагины

Для упрощения работы с drag and drop существуют готовые плагины, например cypress-drag-drop. После установки через npm:

npm install --save-dev cypress-drag-drop

Необходимо подключить плагин в cypress/support/commands.js:

import 'cypress-drag-drop'

После этого можно использовать команду .drag():

cy.get('#draggable').drag('#droppable')

Этот метод автоматически обрабатывает все события мыши и корректно работает даже с элементами, которые имеют анимацию или динамическую подгрузку.


Тестирование сложных сценариев

Drag and drop с ограничениями

Некоторые элементы могут иметь ограничения по оси или родительскому контейнеру. В таких случаях координаты движения должны учитывать границы:

cy.get('#draggable')
  .trigger('mousedown', { which: 1 })
  .trigger('mousemove', { clientX: 150, clientY: 0 }) // только горизонтальное движение
  .trigger('mouseup')

Drag and drop внутри iframe

Для работы с iframe необходимо сначала получить доступ к его содержимому:

cy.get('iframe')
  .its('0.contentDocument.body')
  .then(cy.wrap)
  .find('#draggable')
  .trigger('mousedown', { which: 1 })

Такой подход обеспечивает корректное взаимодействие с элементами внутри встроенных фреймов.


Ассерты после перетаскивания

После выполнения drag and drop необходимо проверить состояние страницы. Наиболее часто проверяют:

  • Позицию элемента
  • Изменение CSS-классов
  • Текст или содержимое контейнера

Пример проверки позиции через CSS:

cy.get('#draggable')
  .should('have.css', 'transform')
  .and('match', /translate\(200px, 200px\)/)

Проверка успешного сброса в контейнер:

cy.get('#droppable')
  .should('contain', 'Dropped!')

Работа с анимациями и задержками

Многие drag and drop интерфейсы имеют плавные анимации, что может привести к флейки-тестам. Для стабильности используют:

  • { force: true } при trigger
  • wait() для ожидания анимации
  • Настройку animationDistanceThreshold в Cypress конфиге

Пример:

cy.get('#draggable')
  .trigger('mousedown', { which: 1, force: true })
cy.wait(500) // ожидание анимации
cy.get('#droppable')
  .trigger('mousemove', { force: true })
  .trigger('mouseup', { force: true })

Советы по оптимизации тестов

  1. Минимизировать использование координат, если можно опираться на контейнеры и целевые элементы.
  2. Сохранять стабильность тестов, избегая хардкодинга пикселей, если интерфейс может меняться.
  3. Использовать плагины для упрощения drag and drop в сложных приложениях.
  4. Разделять логику теста и работу с DOM, создавая вспомогательные команды в cypress/support/commands.js.

Расширение функционала через кастомные команды

Можно создать собственную команду для drag and drop, чтобы сократить дублирование кода:

Cypress.Commands.add('dragTo', { prevSubject: 'element' }, (subject, target) => {
  cy.wrap(subject)
    .trigger('mousedown', { which: 1 })
  cy.get(target)
    .trigger('mousemove')
    .trigger('mouseup', { force: true })
})

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

cy.get('#draggable').dragTo('#droppable')

Такой подход делает тесты более читаемыми и поддерживаемыми.