How to implement drag and drop in cypress test?

后端 未结 6 1384
青春惊慌失措
青春惊慌失措 2020-12-14 07:47

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

相关标签:
6条回答
  • 2020-12-14 08:14

    Not Angular specific, but should be generic and simple enough to tweak if needed. I did try a lot of recipes out there and also cypress-file-upload but that wouldn't work with webp for example.

    The command below seems to work for most cases and reflects pretty closely what a user would do

    Cypress.Commands.add('dropFile', {prevSubject: true}, (subject, fileName, fileType) => {
      return cy.fixture(fileName, 'binary').then((data) => {
        return Cypress.Blob.binaryStringToBlob(data, fileType).then(blob => {
          const file = new File([blob], fileName, {type: fileType});
          const dataTransfer = new DataTransfer();
          dataTransfer.items.add(file);
          cy.wrap(subject)
            .trigger("dragenter", {force: true})
            .trigger("drop", {dataTransfer})
        })
      })
    })
    

    Ensure fixturesFolder is specified in your cypress.json config file. Then you simply use like below

    cy.get("#dropzone").dropFile("myfile1.webp", "image/webp")
    cy.get("#dropzone").dropFile("myfile2.jpg", "image/jpeg")
    
    0 讨论(0)
  • 2020-12-14 08:15

    After a lot of battling, I managed to make the drag and drop work with this:

    cy.get('.list .item')
          .contains(startpos)
          .trigger('dragstart', { dataTransfer: new DataTransfer });
    cy.get('.list .item')
          .eq(endpos)
          .trigger('drop')
          .trigger('dragend');
    

    Pretty easy to use.

    0 讨论(0)
  • 2020-12-14 08:25

    It seems cy.trigger isn't sending the proper events at the correct target elements. I expect this to be fixed in version 4.0

    ...but, I've written up a small plugin for drag and drop.

    It works by adding a dragTo command like so:

    /// <reference types="cypress"/>
    
    it('works', () => {
      cy.visit('https://angular-oxkc7l-zirwfs.stackblitz.io/')
      cy.contains('To do', { timeout: 15000 }) // ensure page is loaded -__-
    
      const item = '.example-box:not(.cdk-drag-placeholder)'
    
      cy.get('#cdk-drop-list-1').children(item).should('have.length', 5)
    
      cy.get('.example-box:contains("Get to work")').dragTo('.example-box:contains("Get up")')
      cy.get('#cdk-drop-list-1').children(item).should('have.length', 6)
    
      // interpolates 10 extra mousemove events on the way
      cy.get('#cdk-drop-list-0').dragTo('#cdk-drop-list-1', { steps: 10 })
      cy.get('#cdk-drop-list-1').children(item).should('have.length', 7)
    
      // sets steps >= 10
      cy.get('#cdk-drop-list-0').dragTo('#cdk-drop-list-1', { smooth: true })
      cy.get('#cdk-drop-list-1').children(item).should('have.length', 8)
    
      cy.get('#cdk-drop-list-0').dragTo('#cdk-drop-list-1')
      cy.get('#cdk-drop-list-1').children(item).should('have.length', 9)
    })
    
    

    To add it, try putting this in your support/index.js or pasting it at the bottom of a spec file (warning: poor code quality):

    
    const getCoords = ($el) => {
      const domRect = $el[0].getBoundingClientRect()
      const coords = { x: domRect.left + (domRect.width / 2 || 0), y: domRect.top + (domRect.height / 2 || 0) }
    
      return coords
    }
    
    const dragTo = (subject, to, opts) => {
    
      opts = Cypress._.defaults(opts, {
        // delay inbetween steps
        delay: 0,
        // interpolation between coords
        steps: 0,
        // >=10 steps
        smooth: false,
      })
    
      if (opts.smooth) {
        opts.steps = Math.max(opts.steps, 10)
      }
    
      const win = subject[0].ownerDocument.defaultView
    
      const elFromCoords = (coords) => win.document.elementFromPoint(coords.x, coords.y)
      const winMouseEvent = win.MouseEvent
    
      const send = (type, coords, el) => {
    
        el = el || elFromCoords(coords)
    
        el.dispatchEvent(
          new winMouseEvent(type, Object.assign({}, { clientX: coords.x, clientY: coords.y }, { bubbles: true, cancelable: true }))
        )
      }
    
      const toSel = to
    
      function drag (from, to, steps = 1) {
    
        const fromEl = elFromCoords(from)
    
        const _log = Cypress.log({
          $el: fromEl,
          name: 'drag to',
          message: toSel,
        })
    
        _log.snapshot('before', { next: 'after', at: 0 })
    
        _log.set({ coords: to })
    
        send('mouseover', from, fromEl)
        send('mousedown', from, fromEl)
    
        cy.then(() => {
          return Cypress.Promise.try(() => {
    
            if (steps > 0) {
    
              const dx = (to.x - from.x) / steps
              const dy = (to.y - from.y) / steps
    
              return Cypress.Promise.map(Array(steps).fill(), (v, i) => {
                i = steps - 1 - i
    
                let _to = {
                  x: from.x + dx * (i),
                  y: from.y + dy * (i),
                }
    
                send('mousemove', _to, fromEl)
    
                return Cypress.Promise.delay(opts.delay)
    
              }, { concurrency: 1 })
            }
          })
          .then(() => {
    
            send('mousemove', to, fromEl)
            send('mouseover', to)
            send('mousemove', to)
            send('mouseup', to)
            _log.snapshot('after', { at: 1 }).end()
    
          })
    
        })
    
      }
    
      const $el = subject
      const fromCoords = getCoords($el)
      const toCoords = getCoords(cy.$$(to))
    
      drag(fromCoords, toCoords, opts.steps)
    }
    
    Cypress.Commands.addAll(
      { prevSubject: 'element' },
      {
        dragTo,
      }
    )
    
    

    0 讨论(0)
  • 2020-12-14 08:26

    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'));
    
    0 讨论(0)
  • 2020-12-14 08:31

    Did you take a look at the official recipe that does exactly the same?

    It uses this combination of triggered events

    cy.get('.selector')
      .trigger('mousedown', { which: 1 })
      .trigger('mousemove', { clientX: 400, clientY: 500 })
      .trigger('mouseup', {force: true})
    

    to drag&drop the item, let me know if you need some more help when you have tried it

    0 讨论(0)
  • 2020-12-14 08:32

    Here's my cypress command for this:

    Cypress.Commands.add(
      'dragTo',
      (selector: string, position: { x: number; y: number }) => {
        const log = Cypress.log({
          message: `Drag ${selector} to (${position.x}, ${position.y})`,
          consoleProps: () => ({ selector, position })
        });
        log.snapshot('before');
        const ret = cy
          .get(selector, { log: false })
          .trigger('mouseover', { force: true, log: false })
          .trigger('mousedown', {
            button: 0,
            log: false
          })
          .trigger('mousemove', {
            pageX: 10,
            pageY: 10,
            log: false
          })
          .then(el => {
            log.snapshot('Drag start');
            return el;
          })
          .trigger('mousemove', {
            pageX: position.x,
            pageY: position.y,
            force: true,
            log: false
          })
          .then(event => {
            log.snapshot('Drag End');
            return event;
          })
          .trigger('mouseup', { force: true, log: false })
          .then(() => {
            log.snapshot('after');
          });
        log.end();
        return ret;
      }
    );
    
    0 讨论(0)
提交回复
热议问题