How to implement drag and drop in cypress test?

Dispatching MouseEvents seems to be the only way to test Angular Material drag and drop.

You should also be aware of the following issue, which tests in Protractor but also applies to this Cypress test

CDK DragDrop Regression between 7.0.0-beta.2 and 7.0.0-rc.2: Protractor tests stopped working #13642,

It seems that (for want of a better explanation) an additional nudge is needed on the mousemove.

The steps given as a workaround (Protractor syntax),

private async dragAndDrop ( $element, $destination ) {
  await browser.actions().mouseMove( $element ).perform();
  await browser.actions().mouseDown( $element ).perform();
  await browser.actions().mouseMove( {x: 10, y: 0 } ).perform();
  await browser.actions().mouseMove( $destination ).perform();
  return browser.actions().mouseUp().perform();
}

can be translated into a Cypress test, the simplest form I found is

it('works (simply)', () => {
  const draggable = Cypress.$('#cdk-drop-list-0 > :nth-child(1)')[0]  // Pick up this
  const droppable = Cypress.$('#cdk-drop-list-1 > :nth-child(4)')[0]  // Drop over this

  const coords = droppable.getBoundingClientRect()
  draggable.dispatchEvent(new MouseEvent('mousedown'));
  draggable.dispatchEvent(new MouseEvent('mousemove', {clientX: 10, clientY: 0}));
  draggable.dispatchEvent(new MouseEvent('mousemove', {
    clientX: coords.x+10,   
    clientY: coords.y+10  // A few extra pixels to get the ordering right
  }));
  draggable.dispatchEvent(new MouseEvent('mouseup'));

  cy.get('#cdk-drop-list-1').should('contain', 'Get to work');
  cy.get('#cdk-drop-list-1 > .cdk-drag').eq(3).should('contain', 'Get to work');

});

Notes

  • The problem in the referenced issue is not limited to Protractor. If you remove the first mousemove in the Cypress test, it also fails.
  • The cy.get(..).trigger() syntax does not seem to work with Angular, but native dispatchEvent() does.
  • Dragging over a specific element in the target list (as opposed to just dropping on the list) gives precise positioning within the target list.
  • dragstart, dragend may not be appropriate for Angular Material, as the code shows the event received is type CdkDragDrop rather than a DataTransfer object.
  • If content is asynchronously fetched, you may have to switch from Cypress.$(...) to cy.get(...).then(el => {...}), to take advantage of cypress’ auto retry in commands.
  • I had to add a 10 second timeout to visit the Stackblitz url.

Async list fetching

If the list is fetched by an async Angular service (httpClient) during component construction, using this in the test

const draggable = Cypress.$('#cdk-drop-list-0 > :nth-child(1)')[0]

will not work, because the nth-child will not be present immediately, only after the fetch completes.

Instead, you can use cy.get() to provide retries up to a timeout (default 5 seconds).

cy.get('#cdk-drop-list-0 > :nth-child(1)').then(el => {
  const draggable = el[0]  // Pick up this
  cy.get('#cdk-drop-list-1 > :nth-child(4)').then(el => {
    const droppable = el[0]  // Drop over this

    const coords = droppable.getBoundingClientRect()
    draggable.dispatchEvent(new MouseEvent('mousemove'));
    draggable.dispatchEvent(new MouseEvent('mousedown'));
    draggable.dispatchEvent(new MouseEvent('mousemove', {clientX: 10, clientY: 0}));
    draggable.dispatchEvent(new MouseEvent('mousemove', {clientX: coords.x+10, clientY: coords.y+10}));
    draggable.dispatchEvent(new MouseEvent('mouseup'));

  })

  cy.get('#cdk-drop-list-1').should('contain', 'Get to work');
  cy.get('#cdk-drop-list-1 > .cdk-drag').eq(3).should('contain', 'Get to work');
})

or my preference is to use a ‘canary’ test to ensure loading is complete, something like

before(() => {
  cy.get('#cdk-drop-list-0 > :nth-child(1)') // Canary - wait 5s for data
})

it('should...', () => {
  const draggable = Cypress.$('#cdk-drop-list-0 > :nth-child(1)')[0]  // Pick up this
  const droppable = Cypress.$('#cdk-drop-list-1 > :nth-child(4)')[0]  // Drop over this
  ...
})

Typescript support

Warning – this is a quick hack to get over Typescript compiler problems, and could be improved.

const coords: ClientRect = droppable.getBoundingClientRect()
draggable.dispatchEvent(new (<any>MouseEvent)('mousemove'));
draggable.dispatchEvent(new (<any>MouseEvent)('mousedown'));
draggable.dispatchEvent(new (<any>MouseEvent)('mousemove', {clientX: 10.0, clientY: 0.0}));
draggable.dispatchEvent(new (<any>MouseEvent)('mousemove', {clientX: coords.left + 10.0, clientY: coords.top + 10.0}));
draggable.dispatchEvent(new (<any>MouseEvent)('mouseup'));

Leave a Comment