如何在赛普拉斯测试中实现拖放?
How to implement drag and drop in cypress test?
我正在努力测试 Cypress and Angular Material Drag and Drop 的拖放功能。因此,目标是将“开始工作”从待办事项移至完成。
我创建了以下测试,应该可以让您轻松重现:
您可以玩 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 () => {
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');
});
});
运行 上述测试的结果如下所示:
这里a repo制定解决方案。
感谢您的帮助。
触发的事件,使用 chrome 调试器发现:
项目
- 指针悬停
- 指针输入
- 鼠标悬停
- 鼠标按下
- 指针移动
- 鼠标移动
- 指针输出
- 指针离开
- 鼠标悬停
- 鼠标离开
掉落区
- 指针悬停
- 指针输入
- 鼠标悬停
- 指针移动
- 鼠标移动
- 指针离开
- 鼠标悬停
- 鼠标离开
解决方案
在@Richard Matsen 的出色回答之后,我最终添加了 作为自定义命令。解决方案如下所示
support/drag-support.ts
export function drag(dragSelector: string, dropSelector: string) {
// Based on this answer:
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 Whosebug, drag and drop support now works ', () => {
cy.drag('#cdk-drop-list-0 > :nth-child(1)', '#cdk-drop-list-1')
.should('contain', 'Get to work');
});
一个小 giph,因为我很高兴它终于起作用了
CI
现在它也适用于 CI(和本地电子)。使用 CircleCI 2.0.
测试
你看过完全一样的official recipe了吗?
它使用这种触发事件的组合
cy.get('.selector')
.trigger('mousedown', { which: 1 })
.trigger('mousemove', { clientX: 400, clientY: 500 })
.trigger('mouseup', {force: true})
拖放项目,如果您在尝试后需要更多帮助,请告诉我
我写了一个关于如何实现拖放的小例子。
它通过添加一个 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,
}
)
调度 MouseEvents 似乎是测试 Angular Material 拖放的唯一方法。
您还应该注意以下问题,该问题在 Protractor 中进行测试,但也适用于此 Cypress 测试
CDK DragDrop Regression between 7.0.0-beta.2 and 7.0.0-rc.2: Protractor tests stopped working #13642,
似乎(为了得到更好的解释)mousemove 需要额外的推动。
作为 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();
}
可以翻译成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');
});
注释
- 引用的issue中的问题不限于Protractor。如果您删除 Cypress 测试中的第一个
mousemove
,它也会失败。
cy.get(..).trigger()
语法似乎不适用于 Angular,但本机 dispatchEvent()
可以。
- 拖动目标列表中的特定元素(而不是仅仅放在列表上)可以在目标列表中进行精确定位。
dragstart, dragend
可能不适合 Angular Material,因为代码显示接收到的事件是类型 CdkDragDrop
而不是 DataTransfer object.
- 如果内容是异步获取的,您可能需要从
Cypress.$(...)
切换到 cy.get(...).then(el => {...})
,以利用 cypress 在命令中的自动重试。
- 我必须添加 10 秒的超时才能访问 Stackblitz url。
异步列表获取
如果在组件构造期间通过异步 Angular 服务 (httpClient) 获取列表,请在测试中使用它
const draggable = Cypress.$('#cdk-drop-list-0 > :nth-child(1)')[0]
将不起作用,因为 nth-child 不会立即出现,只有在提取完成后才会出现。
相反,您可以使用 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 编译器问题的快速技巧,可以改进。
根据
将MouseEvent
转换为<any>
将 getBoundingClientRect()
的 return 键入 ClientRect
,并使用属性 left 和 top 代替 x 和 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'));
经过大量的斗争,我设法使拖放工作与此有关:
cy.get('.list .item')
.contains(startpos)
.trigger('dragstart', { dataTransfer: new DataTransfer });
cy.get('.list .item')
.eq(endpos)
.trigger('drop')
.trigger('dragend');
相当容易使用。
不是 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")
这是我的 cypress 命令:
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;
}
);
试试这个:
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 });
});
我一直遇到 dropevent.isPointerOverContainer 的问题,这里的其他解决方案总是错误的,所以最后我不得不使用 click() 而不是 mouseup。这是使指针位置和拖动位置处于正确位置以在我的组件中触发 drop() 事件的唯一方法。
export function drag(dragSelector: string, dropSelector: string) {
// Based on this answer:
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);
对于那些在拖放和“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');
});
});
});
如果有人还在为未触发 cdkDropListEntered 而苦苦挣扎,您可能需要检查是否有任何滚动正在进行。
由于滚动是由 CDK 处理的(例如 https://github.com/angular/components/blob/master/src/cdk/scrolling/viewport-ruler.ts#L131),我不得不将滚动位置添加到任何鼠标事件中。滚动位置是这样计算的(对应上面link):
const win = subject[0].ownerDocument.defaultView;
const window = win;
const document = window.document;
const documentElement = document.documentElement;
const documentRect = documentElement.getBoundingClientRect();
const top =
-documentRect.top ||
document.body.scrollTop ||
window.scrollY ||
documentElement.scrollTop ||
0;
const left =
-documentRect.left ||
document.body.scrollLeft ||
window.scrollX ||
documentElement.scrollLeft ||
0;
其中主题是产生的结果,例如通过 cy.get 命令。
你只需要将class或id从你想拖放的地方替换掉
const dataTransfer = new DataTransfer();
cy.get('ID or class which you want to drag').trigger('dragstart',{
dataTransfer
});
cy.get('ID or class where you want to drop').trigger('drop',{
dataTransfer
});
如果拖放之间不涉及任何步骤
然后使用以下步骤:
为 运行 个具有拖放功能的脚本下载此文件
npm install --save-dev @4tw/cypress-drag-drop
转到cypress.json
"compilerOptions": {
"types": ["cypress", "@4tw/cypress-drag-drop"]
}
将此添加到 cypress.json
继续command.js
require('@4tw/cypress-drag-drop')
使用
cy.get('Class or id which you want to drag ').drag('Class or id where you want to drop')
对我来说@bkucera 的代码有效,谢谢。但我不得不碰碰运气 3 件事才能按预期为我工作:
1.
let _to = {
x: from.x + dx * (i),
y: from.y - dy * (i),
}
i = i+1
必须使用一些延迟、步骤和 smooth:true
delay: 30,
steps: 30,
smooth: true,
const dataTransfer = new DataTransfer();
cy.get('#your-item-id').trigger('dragstart', { dataTransfer });
cy.get('#your-dropable-div').trigger('drop', { dataTransfer });
我正在努力测试 Cypress and Angular Material Drag and Drop 的拖放功能。因此,目标是将“开始工作”从待办事项移至完成。 我创建了以下测试,应该可以让您轻松重现:
您可以玩 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 () => {
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');
});
});
运行 上述测试的结果如下所示:
这里a repo制定解决方案。
感谢您的帮助。
触发的事件,使用 chrome 调试器发现:
项目
- 指针悬停
- 指针输入
- 鼠标悬停
- 鼠标按下
- 指针移动
- 鼠标移动
- 指针输出
- 指针离开
- 鼠标悬停
- 鼠标离开
掉落区
- 指针悬停
- 指针输入
- 鼠标悬停
- 指针移动
- 鼠标移动
- 指针离开
- 鼠标悬停
- 鼠标离开
解决方案
在@Richard Matsen 的出色回答之后,我最终添加了
support/drag-support.ts
export function drag(dragSelector: string, dropSelector: string) {
// Based on this answer:
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 Whosebug, drag and drop support now works ', () => {
cy.drag('#cdk-drop-list-0 > :nth-child(1)', '#cdk-drop-list-1')
.should('contain', 'Get to work');
});
一个小 giph,因为我很高兴它终于起作用了
CI
现在它也适用于 CI(和本地电子)。使用 CircleCI 2.0.
测试你看过完全一样的official recipe了吗?
它使用这种触发事件的组合
cy.get('.selector')
.trigger('mousedown', { which: 1 })
.trigger('mousemove', { clientX: 400, clientY: 500 })
.trigger('mouseup', {force: true})
拖放项目,如果您在尝试后需要更多帮助,请告诉我
我写了一个关于如何实现拖放的小例子。
它通过添加一个 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,
}
)
调度 MouseEvents 似乎是测试 Angular Material 拖放的唯一方法。
您还应该注意以下问题,该问题在 Protractor 中进行测试,但也适用于此 Cypress 测试
CDK DragDrop Regression between 7.0.0-beta.2 and 7.0.0-rc.2: Protractor tests stopped working #13642,
似乎(为了得到更好的解释)mousemove 需要额外的推动。
作为 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();
}
可以翻译成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');
});
注释
- 引用的issue中的问题不限于Protractor。如果您删除 Cypress 测试中的第一个
mousemove
,它也会失败。 cy.get(..).trigger()
语法似乎不适用于 Angular,但本机dispatchEvent()
可以。- 拖动目标列表中的特定元素(而不是仅仅放在列表上)可以在目标列表中进行精确定位。
dragstart, dragend
可能不适合 Angular Material,因为代码显示接收到的事件是类型CdkDragDrop
而不是 DataTransfer object.- 如果内容是异步获取的,您可能需要从
Cypress.$(...)
切换到cy.get(...).then(el => {...})
,以利用 cypress 在命令中的自动重试。 - 我必须添加 10 秒的超时才能访问 Stackblitz url。
异步列表获取
如果在组件构造期间通过异步 Angular 服务 (httpClient) 获取列表,请在测试中使用它
const draggable = Cypress.$('#cdk-drop-list-0 > :nth-child(1)')[0]
将不起作用,因为 nth-child 不会立即出现,只有在提取完成后才会出现。
相反,您可以使用 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 编译器问题的快速技巧,可以改进。
根据
将将
getBoundingClientRect()
的 return 键入ClientRect
,并使用属性 left 和 top 代替 x 和 y。
MouseEvent
转换为<any>
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'));
经过大量的斗争,我设法使拖放工作与此有关:
cy.get('.list .item')
.contains(startpos)
.trigger('dragstart', { dataTransfer: new DataTransfer });
cy.get('.list .item')
.eq(endpos)
.trigger('drop')
.trigger('dragend');
相当容易使用。
不是 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")
这是我的 cypress 命令:
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;
}
);
试试这个:
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 });
});
我一直遇到 dropevent.isPointerOverContainer 的问题,这里的其他解决方案总是错误的,所以最后我不得不使用 click() 而不是 mouseup。这是使指针位置和拖动位置处于正确位置以在我的组件中触发 drop() 事件的唯一方法。
export function drag(dragSelector: string, dropSelector: string) {
// Based on this answer:
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);
对于那些在拖放和“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');
});
});
});
如果有人还在为未触发 cdkDropListEntered 而苦苦挣扎,您可能需要检查是否有任何滚动正在进行。
由于滚动是由 CDK 处理的(例如 https://github.com/angular/components/blob/master/src/cdk/scrolling/viewport-ruler.ts#L131),我不得不将滚动位置添加到任何鼠标事件中。滚动位置是这样计算的(对应上面link):
const win = subject[0].ownerDocument.defaultView;
const window = win;
const document = window.document;
const documentElement = document.documentElement;
const documentRect = documentElement.getBoundingClientRect();
const top =
-documentRect.top ||
document.body.scrollTop ||
window.scrollY ||
documentElement.scrollTop ||
0;
const left =
-documentRect.left ||
document.body.scrollLeft ||
window.scrollX ||
documentElement.scrollLeft ||
0;
其中主题是产生的结果,例如通过 cy.get 命令。
你只需要将class或id从你想拖放的地方替换掉
const dataTransfer = new DataTransfer();
cy.get('ID or class which you want to drag').trigger('dragstart',{
dataTransfer
});
cy.get('ID or class where you want to drop').trigger('drop',{
dataTransfer
});
如果拖放之间不涉及任何步骤 然后使用以下步骤:
为 运行 个具有拖放功能的脚本下载此文件
npm install --save-dev @4tw/cypress-drag-drop
转到cypress.json
"compilerOptions": {
"types": ["cypress", "@4tw/cypress-drag-drop"]
}
将此添加到 cypress.json
继续command.js
require('@4tw/cypress-drag-drop')
使用
cy.get('Class or id which you want to drag ').drag('Class or id where you want to drop')
对我来说@bkucera 的代码有效,谢谢。但我不得不碰碰运气 3 件事才能按预期为我工作:
1.
let _to = {
x: from.x + dx * (i),
y: from.y - dy * (i),
}
i = i+1
必须使用一些延迟、步骤和 smooth:true
delay: 30,
steps: 30,
smooth: true,
const dataTransfer = new DataTransfer();
cy.get('#your-item-id').trigger('dragstart', { dataTransfer });
cy.get('#your-dropable-div').trigger('drop', { dataTransfer });