赛普拉斯测试中如何实现拖放?

时间:2019-03-26 16:03:20

标签: angular-material2 cypress

我正在努力用CypressAngular Material Drag and Drop测试拖放。因此,目标是将“开始工作”从Todo转移到Done。 我创建了以下测试,该测试应该可以使您轻松重现:

您可以玩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');
    });

});

运行上述测试的结果如下:

enter image description here

这里是a repo,用于开发解决方案。

感谢您的帮助。

触发事件,使用chrome调试器发现

项目

  • 指针悬停
  • pointerenter
  • 鼠标悬停
  • 鼠标按下
  • 指针移动
  • mousemove
  • pointerout
  • 指针离开
  • 鼠标超时
  • mouseleave

下降区域

  • 指针悬停
  • pointerenter
  • 鼠标悬停
  • 指针移动
  • mousemove
  • 指针离开
  • 鼠标超时
  • mouseleave

解决方案

在@Richard Matsen的出色回答之后,我最终添加了his answer作为自定义命令。解决方案看起来像这样

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);

在规格文件中

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');
});

一个小字,因为我很高兴它终于可以工作

enter image description here

CI

现在,它们也可以在CI(和本地电子)中使用。经过CircleCI 2.0的测试。

9 个答案:

答案 0 :(得分:5)

调度MouseEvents似乎是测试Angular Material拖放的唯一方法。

您还应该注意以下问题,该问题在量角器中进行测试,但也适用于此赛普拉斯测试

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

(为了更好地说明),似乎需要在鼠标移动时再轻推一下。

workaround(量角器语法)给出的步骤,

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();
}

可以翻译成赛普拉斯测试,我发现的最简单的形式是

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');

});

注释

  • 所引用问题中的问题不仅限于量角器。如果删除赛普拉斯测试中的第一个mousemove,它也将失败。
  • cy.get(..).trigger()语法似乎不适用于Angular,但本机dispatchEvent()可以。
  • 通过拖放到目标列表中的特定元素(而不是仅拖放到列表中),可以在目标列表中进行精确定位。
  • dragstart, dragend可能不适用于Angular Material,因为代码显示接收到的事件为CdkDragDrop类型,而不是DataTransfer对象。
  • 如果异步获取内容,则可能必须从Cypress.$(...)切换到cy.get(...).then(el => {...}),才能利用cypress的命令自动重试功能。
  • 我必须添加10秒超时才能访问Stackblitz网址。

异步列表获取

如果列表是在组件构建过程中由异步Angular服务(httpClient)提取的,请在测试中使用

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

不起作用,因为只有在提取完成后,第n个孩子才会立即出现。

相反,您可以使用cy.get()来提供重试,直到超时(默认为5秒)。

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');
})

或者我更喜欢使用“ canary”测试来确保加载完成,例如

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编译器问题的快速技巧,可以改进。

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'));

答案 1 :(得分:1)

似乎cy.trigger没有在正确的目标元素上发送正确的事件。我希望此问题将在版本4.0

中修复

...但是,我编写了一个用于拖放的小插件。

通过添加dragTo命令,如下所示:

/// <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)
})

要添加它,请尝试将其放入support/index.js或将其粘贴到spec文件的底部(警告:代码质量较差):


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,
  }
)

enter image description here

答案 2 :(得分:1)

经过大量的战斗,我设法通过以下方式进行了拖放:

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

非常易于使用。

答案 3 :(得分:0)

您是否看过功能完全相同的official recipe

它使用触发事件的这种组合

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

要拖放该项目,请尝试后告诉我

答案 4 :(得分:0)

不是特定于Angular,但应通用且足够简单,以便在需要时进行调整。 我确实尝试了很多食谱,而且cypress-file-upload也尝试过,但这不适用于webp。

以下命令似乎适用于大多数情况,并且非常接近地反映了用户将要执行的操作

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})
    })
  })
})

确保在您的cypress.json配置文件中指定了fixturesFolder。然后,您只需像下面这样使用

cy.get("#dropzone").dropFile("myfile1.webp", "image/webp")
cy.get("#dropzone").dropFile("myfile2.jpg", "image/jpeg")

答案 5 :(得分:0)

这是我的柏树命令:

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;
  }
);

答案 6 :(得分:0)

试试这个:

  it('should drag and drop the element', () => {
    const myItem = cy.get('my-item').first();
    myItem.trigger('mousedown', 100, 100, { force: true }).trigger('mousemove', 300, 300, { force: true });
    myItem.click().trigger('mouseup', { force: true });
});

答案 7 :(得分:0)

我一直遇到 dropevent.isPointerOverContainer 的问题,这里的其他解决方案总是为 false,所以最后我不得不使用 click(),而不是使用 mouseup。这是使指针位置和拖动位置处于正确位置以在我的组件中触发 drop() 事件的唯一方法。

export function drag(dragSelector: string, dropSelector: string) {
  // Based on this answer: https://stackoverflow.com/questions/55361499/how-to-implement-drag-and-drop-in-cypress-test
  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', {clientX: coords.left + 40, clientY: coords.top + 10}));
  cy.get(dropSelector).click();
  //   draggable.dispatchEvent(new MouseEvent('mouseup'));

  return cy.get(dropSelector);
}

// 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);

答案 8 :(得分:0)

对于那些在拖放和“react-beautiful-dnd”库中苦苦挣扎的人,这里有一段代码对我有帮助(其他什么都没做)。它是从 this post

中提取的
Cypress.Commands.add('dragAndDrop', (subject, target) => {
Cypress.log({
    name: 'DRAGNDROP',
    message: `Dragging element ${subject} to ${target}`,
    consoleProps: () => {
        return {
            subject: subject,
            target: target
        };
    }
});
const BUTTON_INDEX = 0;
const SLOPPY_CLICK_THRESHOLD = 10;
cy.get(target)
    .first()
    .then($target => {
        let coordsDrop = $target[0].getBoundingClientRect();
        cy.get(subject)
            .first()
            .then(subject => {
                const coordsDrag = subject[0].getBoundingClientRect();
                cy.wrap(subject)
                    .trigger('mousedown', {
                        button: BUTTON_INDEX,
                        clientX: coordsDrag.x,
                        clientY: coordsDrag.y,
                        force: true
                    })
                    .trigger('mousemove', {
                        button: BUTTON_INDEX,
                        clientX: coordsDrag.x + SLOPPY_CLICK_THRESHOLD,
                        clientY: coordsDrag.y,
                        force: true
                    });
                cy.get('body')
                    .trigger('mousemove', {
                        button: BUTTON_INDEX,
                        clientX: coordsDrop.x,
                        clientY: coordsDrop.y,
                        force: true            
                    })
                    .trigger('mouseup');
            });
    });
});
相关问题