Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement drag and drop in cypress test?

I am struggling to test drag and drop with Cypress and Angular Material Drag and Drop. So the goal is to move "Get to work" from Todo to Done. I have created the following test, that should make it easy for you to reproduce:

You can play with the Stackblitz here.

describe('Trying to implement drag-n-drop', () => {      before(() => {         Cypress.config('baseUrl', null);          cy.viewport(1000, 600);         cy.visit('https://angular-oxkc7l-zirwfs.stackblitz.io')         .url().should('contain', 'angular')         .get('h2').should('contain', 'To do');     });      it('Should work, based on this https://stackoverflow.com/a/54119137/3694288', () => {          const dataTransfer = new DataTransfer;          cy.get('#cdk-drop-list-0 > :nth-child(1)')             .trigger('dragstart', { dataTransfer });          cy.get('#cdk-drop-list-1')             .trigger('drop', { dataTransfer });          cy.get('#cdk-drop-list-0 > :nth-child(1)')             .trigger('dragend');          cy.get('#cdk-drop-list-1').should('contain', 'Get to work');     });       it('Should work, with this library https://github.com/4teamwork/cypress-drag-drop', () => {         cy.get('#cdk-drop-list-0 > :nth-child(1)')             .drag('#cdk-drop-list-1');          cy.get('#cdk-drop-list-1').should('contain', 'Get to work');     });  }); 

The result from running the above test, looks like this:

enter image description here

Here is a repo to develop a solution.

Thanks for the help.

Events fired, found using the chrome debugger:

Item

  • pointerover
  • pointerenter
  • mouseover
  • mousedown
  • pointermove
  • mousemove
  • pointerout
  • pointerleave
  • mouseout
  • mouseleave

Drop zone

  • pointerover
  • pointerenter
  • mouseover
  • pointermove
  • mousemove
  • pointerleave
  • mouseout
  • mouseleave

Solution

After @Richard Matsen's awesome answer I ended up with adding his answer as a custom command. The solution looks like this

support/drag-support.ts

    export function drag(dragSelector: string, dropSelector: string) {         // Based on this answer: https://stackoverflow.com/a/55436989/3694288         cy.get(dragSelector).should('exist')           .get(dropSelector).should('exist');                const draggable = Cypress.$(dragSelector)[0]; // Pick up this           const droppable = Cypress.$(dropSelector)[0]; // Drop over this                const coords = droppable.getBoundingClientRect();           draggable.dispatchEvent(<any>new MouseEvent('mousedown'));           draggable.dispatchEvent(<any>new MouseEvent('mousemove', {clientX: 10, clientY: 0}));           draggable.dispatchEvent(<any>new MouseEvent('mousemove', {               // I had to add (as any here --> maybe this can help solve the issue??)               clientX: coords.left + 10,               clientY: coords.top + 10  // A few extra pixels to get the ordering right           }));           draggable.dispatchEvent(new MouseEvent('mouseup'));           return cy.get(dropSelector);     } 

support/commands.ts

    // Add typings for the custom command     declare global {         namespace Cypress {             interface Chainable {                 drag: (dragSelector: string, dropSelector: string) => Chainable;             }         }     }     // Finally add the custom command     Cypress.Commands.add('drag', drag); 

in the spec file

    it('🔥 Thx to Stackoverflow, drag and drop support now works 🔥', () => {        cy.drag('#cdk-drop-list-0 > :nth-child(1)', '#cdk-drop-list-1')        .should('contain', 'Get to work');     }); 

A small giph, because I'm just so happy it finally works 😲

enter image description here

CI

Now it also works in CI 🔥 (and electron locally). Tested with CircleCI 2.0.

like image 480
DauleDK Avatar asked Mar 26 '19 16:03

DauleDK


People also ask

How do you write a test case in Cypress?

The common part of the setup can be added in before or beforeEach. Exercise - The actual test part e.g. type text and click on submit button. Assertion - Verifying the exercise part e.g. asserting the success message. Teardown - This is done after performing assertion.

Is Cypress good for UI testing?

Conclusion. Cypress is a great tool with a growing feature-set. It makes setting up, writing, running, and debugging tests easy for QA automation engineers. It also has a quicker learning cycle with a good, baked-in execution environment.

Can Cypress be used for end to end testing?

Cypress is a great tool for those who want to create useful end to end tests with very little effort. It also makes it very easy to debug issues with its live preview, snapshots, videos and screenshots. It does fall down in certain areas, and may not be the best solution for certain projects.


1 Answers

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.

  • Cast MouseEvent to <any> as per MouseEvent in TypeScript error not matching signature

  • Type return of getBoundingClientRect() to ClientRect, and use properties left and top instead of x and y.

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')); 
like image 115
Richard Matsen Avatar answered Sep 19 '22 00:09

Richard Matsen