cy.trigger()
is a very low-level utility, so it doesn't do much for you by default and relies on knowing and triggering the exact events with the right arguments that the drag-n-drop implementation relies upon.
I'm guessing you're using the HTML5 backend for React DnD, so that generally means needing to trigger dragstart
and drop
. It might require certain arguments to be passed as well, but the only way to know is to dig into the source code.
I too use React DnD with Cypress and the code below works fine for me:
const dataTransfer = new DndSimulatorDataTransfer()
cy.get('.draggable')
.trigger('mousedown', { which: 1 })
.trigger('dragstart', { dataTransfer })
.trigger('drag', {})
cy.get('.droppable')
.trigger('dragover', { dataTransfer })
.trigger('drop', { dataTransfer })
.trigger('dragend', { dataTransfer })
.trigger('mouseup', { which: 1 })
function DndSimulatorDataTransfer() {
this.data = {}
}
DndSimulatorDataTransfer.prototype.dropEffect = "move"
DndSimulatorDataTransfer.prototype.effectAllowed = "all"
DndSimulatorDataTransfer.prototype.files = []
DndSimulatorDataTransfer.prototype.items = []
DndSimulatorDataTransfer.prototype.types = []
DndSimulatorDataTransfer.prototype.clearData = function(format) {
if(format) {
delete this.data[format]
const index = this.types.indexOf(format)
delete this.types[index]
delete this.data[index]
} else {
this.data = {}
}
}
DndSimulatorDataTransfer.prototype.setData = function(format, data) {
this.data[format] = data
this.items.push(data)
this.types.push(format)
}
DndSimulatorDataTransfer.prototype.getData = function(format) {
if(format in this.data) {
return this.data[format]
}
return ""
}
DndSimulatorDataTransfer.prototype.setDragImage = function(img, xOffset, yOffset) {
// since simulation doesn"t replicate the visual
// effects, there is no point in implementing this
}
I'm not sure does it cover all DnD cases, but you can start from here.
@anatoliyarkhipov that is sweet! We can turn that into a global child command by putting it into commands.js
along with:
Cypress.Commands.add('drag', {
prevSubject: 'element',
}, (sourceSelector, targetSelector) => {
const dataTransfer = new DndSimulatorDataTransfer()
cy.wrap(sourceSelector.get(0))
.trigger('mousedown', { which: 1 })
.trigger('dragstart', { dataTransfer })
.trigger('drag', {})
cy.get(targetSelector)
.trigger('dragover', { dataTransfer })
.trigger('drop', { dataTransfer })
.trigger('dragend', { dataTransfer })
.trigger('mouseup', { which: 1 })
});
example:
cy.get('#dragHandle').drag('#dropTarget')
look at the cypress documentation here
you may have to provide some specific properties to the dnd event. see https://github.com/react-dnd/react-dnd/blob/master/packages/react-dnd-html5-backend/src/HTML5Backend.ts#L359
DataTransfer reference https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer
@anatoliyarkhipov maybe right
@anatoliyarkhipov can you share how your commands.js file now looks with the child command combined. Currently trying to test DnD
@anatoliyarkhipov can you share how your commands.js file now looks with the child command combined. Currently trying to test DnD
I use it this way.
class DndSimulatorDataTransfer {
data = {};
dropEffect = 'move';
effectAllowed = 'all';
files = [];
items = [];
types = [];
clearData(format) {
if (format) {
delete this.data[format]
const index = this.types.indexOf(format)
delete this.types[index]
delete this.data[index]
} else {
this.data = {}
}
}
setData(format, data) {
this.data[format] = data
this.items.push(data)
this.types.push(format)
}
getData(format) {
if (format in this.data) {
return this.data[format]
}
return ''
}
setDragImage(img, xOffset, yOffset) {}
}
Cypress.Commands.add('reactDnd', {
prevSubject: 'element',
}, (sourceSelector, targetSelector, options) => {
const dataTransfer = new DndSimulatorDataTransfer();
const opts = {
offsetX: 100,
offsetY: 100,
...(options || {})
};
cy.wrap(sourceSelector.get(0))
.trigger('dragstart', {
dataTransfer,
})
.trigger('drag', {});
cy.get(targetSelector).then($el => {
const {
x,
y,
} = $el.get(0).getBoundingClientRect();
cy.wrap($el.get(0))
.trigger('dragover', {
dataTransfer,
})
.trigger('drop', {
dataTransfer,
clientX: x + opts.offsetX,
clientY: y + opts.offsetY,
})
.trigger('dragend', {
dataTransfer,
});
})
});
it works very well
Closing as there seems to be a provided workaround that works well today. Improving the actions such as drag and drop is an ongoing process for our team.
Please comment if necessary to reopen.
Thanks for posting your solution @anatoliyarkhipov .
I can't get this to work though, because react-dnd 7.0.2
says: Cannot call hover while not dragging.
.
Any ideas how I might fix that?
Testing with react-dnd 8.0.0
is a bit flakey without delays. Here's where I put them:
cy.wait(500); // Give a moment for react-dnd's drag event listeners to setup
cy.wrap(sourceSelector.get(0)).trigger('dragstart');
cy.get(targetSelector).trigger('drop');
cy.wait(500); // Let react-dnd update its internal state (fixes Cannot call hover while not dragging.)
cy.get(targetSelector).trigger('dragend');
@zquancai I tried your example but it didn't work.
Do i need to pass something in this.data in the class constructor itself?
@zquancai I tried your example but it didn't work.
Do i need to pass something in this.data in the class constructor itself?
no need, are you add the command correctly?
I tried it too but it didn't work by the way I'm using react-beautiful-dnd.
Is the workaround different for that?
I tried it too but it didn't work by the way I'm using react-beautiful-dnd.
Is the workaround different for that?
oh~I think it is different for react-beautiful-dnd
lib. can you show the error log?
Most helpful comment
I too use React DnD with Cypress and the code below works fine for me:
I'm not sure does it cover all DnD cases, but you can start from here.