Cypress’s built-in trigger command is surprisingly powerful for simulating drag-and-drop interactions, even though it’s not explicitly designed for it.

Let’s imagine we have a simple Kanban board where we can drag tasks between columns.

Here’s a basic HTML structure:

<div class="kanban-board">
  <div class="column" id="todo">
    <h2>To Do</h2>
    <div class="task" draggable="true" id="task-1">Task 1</div>
    <div class="task" draggable="true" id="task-2">Task 2</div>
  </div>
  <div class="column" id="in-progress">
    <h2>In Progress</h2>
  </div>
</div>

And some CSS to make it look like columns:

.kanban-board {
  display: flex;
}
.column {
  flex: 1;
  border: 1px solid #ccc;
  margin: 10px;
  padding: 10px;
}
.task {
  background-color: #f9f9f9;
  border: 1px solid #eee;
  padding: 10px;
  margin-bottom: 5px;
  cursor: grab;
}

Now, let’s write a Cypress test to drag Task 1 from To Do to In Progress.

describe('Kanban Board Drag and Drop', () => {
  beforeEach(() => {
    // Visit the page with our Kanban board
    cy.visit('/kanban.html'); // Assuming your HTML is served from this path
  });

  it('should allow dragging a task from To Do to In Progress', () => {
    const taskSelector = '#task-1';
    const todoColumnSelector = '#todo';
    const inProgressColumnSelector = '#in-progress';

    // Get the elements involved
    cy.get(taskSelector).then(($task) => {
      cy.get(todoColumnSelector).then(($todoColumn) => {
        cy.get(inProgressColumnSelector).then(($inProgressColumn) => {

          // Calculate the coordinates for the drag operation
          const taskOffset = $task.offset();
          const taskWidth = $task.outerWidth();
          const taskHeight = $task.outerHeight();
          const targetOffset = $inProgressColumn.offset();

          // The drag start event needs to be triggered on the draggable element
          $task.trigger('dragstart', {
            dataTransfer: {
              setData: (format, data) => {
                // Simulate the browser's dataTransfer API
                $task[0].dataset.draggedData = data; // Store data on the element for retrieval
              },
              getData: (format) => {
                return $task[0].dataset.draggedData;
              }
            }
          });

          // Now, we simulate the drop event on the target column
          // We need to provide coordinates relative to the viewport
          const dropX = targetOffset.left + $inProgressColumn.outerWidth() / 2;
          const dropY = targetOffset.top + $inProgressColumn.outerHeight() / 2;

          $inProgressColumn.trigger('drop', {
            dataTransfer: {
              // The drop event's dataTransfer should be able to retrieve the data
              getData: (format) => {
                return $task[0].dataset.draggedData; // Retrieve the data we set during dragstart
              }
            },
            clientX: dropX, // x-coordinate relative to the viewport
            clientY: dropY  // y-coordinate relative to the viewport
          });

          // We also need to trigger dragover and dragenter on the target
          // to simulate the browser's default behavior that allows dropping.
          // The exact coordinates might need tweaking based on your layout.
          $inProgressColumn.trigger('dragenter', {
            dataTransfer: {
              getData: () => $task[0].dataset.draggedData
            },
            clientX: dropX,
            clientY: dropY
          });
          $inProgressColumn.trigger('dragover', {
            dataTransfer: {
              getData: () => $task[0].dataset.draggedData,
              dropEffect: 'move' // Indicate that a move operation is allowed
            },
            clientX: dropX,
            clientY: dropY
          });

          // Finally, trigger dragend on the original task element
          $task.trigger('dragend');

          // Assertion: Check if the task has moved to the new column
          cy.get(inProgressColumnSelector).contains('Task 1').should('exist');
          cy.get(todoColumnSelector).contains('Task 1').should('not.exist');
        });
      });
    });
  });
});

This approach leverages the browser’s native Drag and Drop API events (dragstart, dragenter, dragover, drop, dragend). We manually trigger these events on the DOM elements and provide the necessary dataTransfer object and coordinates.

The key is simulating the dataTransfer object. In a real browser, this object is automatically created and managed. Here, we create a mock dataTransfer object with setData and getData methods. We use dataset attributes on the element itself to pass data from dragstart to drop.

The clientX and clientY properties on the event object are crucial for the browser to determine where the drop occurred, especially if your drop zones have specific hit areas.

The dragover event needs dataTransfer.dropEffect = 'move' to signal that the element can indeed be moved. Without this, the drop event might not fire.

After the drag and drop simulation, we assert that the task is no longer in its original column and now exists in the target column.

The next challenge is often dealing with more complex drag-and-drop libraries that might abstract these native events or rely on specific DOM structures.

Want structured learning?

Take the full Cypress course →