Как реализовать перетаскивание в кипарисовом тесте?
Я пытаюсь проверить перетаскивание с помощью 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');
});
});
Результат выполнения вышеуказанного теста выглядит следующим образом:
Вот репо для разработки решения.
Спасибо за помощь.
События сгенерированы с помощью отладчика Chrome:
Вещь
- pointerover
- pointerenter
- Mouseover
- MouseDown
- pointermove
- MouseMove
- pointerout
- pointerleave
- MouseOut
- MouseLeave
Зона сброса
- pointerover
- pointerenter
- Mouseover
- pointermove
- MouseMove
- pointerleave
- MouseOut
- MouseLeave
Решение
После потрясающего ответа @Richard Matsen я добавил его ответ в виде пользовательской команды. Решение выглядит так
поддержка/сопротивление-support.ts
export function drag(dragSelector: string, dropSelector: string) {
// Based on this answer: /info/17099915/how-to-implement-drag-and-drop-in-cypress-test/26673934#26673934
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);
}
поддержка /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');
});
Небольшой отрывок, потому что я так счастлив, что он наконец работает 😲
CI
Теперь это также работает в CI 🔥 (и электрон локально). Протестировано с CircleCI 2.0.
Ответы
Ответ 1
Похоже, отправка 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. - Если содержимое
cy.get(...).then(el => {...})
асинхронно, вам может потребоваться переключиться с Cypress.$(...)
на cy.get(...).then(el => {...})
, чтобы воспользоваться преимуществами автоматической повторной попытки кипариса. в командах. - Мне пришлось добавить 10-секундный тайм-аут, чтобы посетить URL Stackblitz.
Выборка асинхронного списка
Если список выбирается асинхронной угловой службой (httpClient) во время создания компонента, используя это в тесте
const draggable = Cypress.$('#cdk-drop-list-0 > :nth-child(1)')[0]
не будет работать, потому что nth-ребенок не будет присутствовать сразу, только после завершения выборки.
Вместо этого вы можете использовать cy.get()
для предоставления повторов до 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'));
Ответ 2
Кажется, 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
или support/index.js
его в 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,
}
)
Ответ 3
После долгих сражений мне удалось заставить работать методом перетаскивания:
cy.get('.list .item')
.contains(startpos)
.trigger('dragstart', { dataTransfer: new DataTransfer });
cy.get('.list .item')
.eq(endpos)
.trigger('drop')
.trigger('dragend');
Довольно прост в использовании.
Ответ 4
Вы взглянули на официальный рецепт, который делает то же самое?
Он использует эту комбинацию вызванных событий
cy.get('.selector')
.trigger('mousedown', { which: 1 })
.trigger('mousemove', { clientX: 400, clientY: 500 })
.trigger('mouseup', {force: true})
чтобы перетащить элемент, дайте мне знать, если вам понадобится дополнительная помощь, когда вы попробуете его 😉