Как реализовать перетаскивание в тесте кипариса? - PullRequest
11 голосов
/ 26 марта 2019

Я пытаюсь проверить перетаскивание с помощью Cypress и Angular Material Drag and Drop .Таким образом, цель состоит в том, чтобы переместить «Приступить к работе» из Todo в Done.Я создал следующий тест, который позволит вам легко воспроизвести:

Вы можете играть с Stackblitz здесь .

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

Вот репо для разработки решения.

Спасибо за помощь.

События сгенерированы с помощью отладчика Chrome:

Item

  • указатель
  • pointerenter
  • наведение курсора
  • mousedown
  • движение указателя
  • перемещение мыши
  • указатель
  • указатель мыши
  • указатель мыши
  • mouseleave

Drop зона

  • указатель
  • указатель
  • указатель мыши
  • pointermove
  • mousemove
  • pointerleave
  • mouseout
  • mouseleave

Solution

После удивительного ответа @Richard Matsen Iв итоге добавили его ответ в качестве пользовательской команды.Решение выглядит следующим образом:

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 and (и электронно локально).Протестировано с CircleCI 2.0.

Ответы [ 4 ]

6 голосов
/ 31 марта 2019

Отправка MouseEvents, кажется, единственный способ проверить перетаскивание Angular Material.

Вам также следует знать о следующей проблеме, которая тестирует в Protractor, но также относится и к этому тесту Cypress

CDK DragDrop Регрессия между 7.0.0-beta.2 и 7.0.0-rc.2: тесты транспортира перестали работать # 13642 ,

Похоже, что (из-за отсутствия лучшего объяснения) требуется дополнительное толчок мышью.

Шаги, заданные как обходной путь (синтаксис транспортира),

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

можно перевести в тест Cypress, самая простая форма, которую я нашел, это

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 в тесте Cypress, он также не будет выполнен.
  • Синтаксис cy.get(..).trigger(), похоже, не работает с Angular, но собственный dispatchEvent() работает.
  • Перетаскивание определенного элемента в списке целей (в отличие от простого падения в списке) дает точное позиционирование в списке целей.
  • dragstart, dragend может не подходить для Angular Material, поскольку код показывает, что полученное событие имеет тип CdkDragDrop, а не объект DataTransfer.
  • Если контент извлекается асинхронно, вам, возможно, придется переключиться с Cypress.$(...) на cy.get(...).then(el => {...}), чтобы воспользоваться преимуществами автоматического повтора команды cypress.
  • Мне пришлось добавить 10-секундный тайм-аут, чтобы посетить URL Stackblitz.

Асинхронный выбор списка

Если список выбирается асинхронной угловой службой (httpClient) во время создания компонента, используя это в тесте

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

не будет работать, потому что nth-ребенок не будет присутствовать сразу, только после завершения выборки.

Вместо этого вы можете использовать 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');
})

или я предпочитаю использовать «канарейку», чтобы убедиться, что загрузка завершена, что-то вроде

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 голос
/ 07 июня 2019

После долгих сражений мне удалось заставить работать методом перетаскивания:

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

Довольно просто в использовании.

1 голос
/ 29 марта 2019

Кажется, 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 или вставить в конец файла спецификации (предупреждение: плохое качество кода):


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

0 голосов
/ 26 марта 2019

Вы взглянули на официальный рецепт , который делает то же самое?

Используется эта комбинация вызванных событий

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

чтобы перетащить элемент, дайте мне знать, если вам понадобится дополнительная помощь, когда вы его попробовали ?

...