Testing D3.js drag events with Cypress.js - javascript

I have an SVG object which uses d3-zoom for zoom and pan functionality. It works flawlessly but the problem showed up when I started to work on integration tests using Cypress.js.
I tried using standard mouse events on the svg element, to simulate drag behavior:
cy.get('svg')
.trigger('mousedown', { which: 1, force: true })
.trigger('mousemove', { position: 'left' })
.trigger('mouseup', { position: 'left', force: true });
The example above is taken from the Cypress drag and drop recipe, and it produces the following error in the nodrag.js file:
cannot read property document of undefined
Below you can see where the error occurs (view is undefined):
__webpack_exports__["default"] = (function(view) {
var root = view.document.documentElement,
...
I spent a lot of hours trying to trigger the event in another way, but without a success - like trying the snippet above with the svg container.
Please keep in mind that I cannot access any d3.js package from the Cypress test because it's imported as an NPM package in a React application.
Thank you in advance for you help!

I could only arrive at a partial answer before I had to move on, but perhaps this can help you, or someone else, find the ultimate solution.
To remedy the error, a view property must be provided for mousedown. Providing window, like this, allowed the D3 methods to fire properly:
cy.get('svg')
.trigger('mousedown', { which: 1, force: true, view: window }) // <-- here
.trigger('mousemove', { position: 'left', view: window }) // <-- here
.trigger('mouseup', { position: 'left', force: true });
However, no dragging or movement occurred during the test run, and other questions emerged from there. Starting with... Is this the right context to send along with the event? It seemed so, since window seems to be the only context that has the property chain that D3 anticipates:
view.document.documentElement
Or is that an anti-pattern... a code smell?
Running down those subsequent questions led to a few observations that seemed to have significance.
The first concerns how D3 handles mouse and drag events. D3 has numerous event listeners and callbacks that override standard events and their respective handlers.
The second, is that iframes are in play with the Cypress test runner.
Could it be that Cypress's programmatically triggered events are firing properly in the Cypress iframe, but due to D3's aggressive event handling, the translation of those events into the application iframe are getting lost? Especially considering that manually dragging a circle in the testing viewport worked fine.
Which, again, leads back to:
Are the programmatically triggered events not being called in the correct context?
Are those events somehow being swallowed by or colliding with D3's event handlers?
I selected the Zoomable Force Directed Graph as my D3 subject, inside of a simple Ember application, for researching this question. It perfectly reproduced the error mentioned, so it definitely seems to be a D3 + Cypress challenge, and unrelated to the front-end framework.
I hope this effort is helpful.
Continued...
After some further reading – Cypress's Trade-offs, and particularly, their open pull request Support for Native Browser Events – it seems likely that the event handling overrides within D3 are not yet fully reconcilable within Cypress. Simpler implementations, like those detailed in the drag and drop example, do not present the event handling challenges introduced by a 3rd party library like D3. However, this support does appear to be under development within the Cypress team.

Try this:
cy.window().then(win => {
cy.get('svg')
.trigger('mousedown', {
which: 1,
force: true,
view: win,
})
.trigger('mousemove', {
clientX: 300,
clientY: 500,
force: true,
})
.trigger('mouseup', {
force: true,
view: win,
});
});
Referencing Jennifer Shehane's answer in this GitHub issue, the answer to the cannot read property document of undefined part is to plug the window object into view in the trigger options. The issue mentioned in jacefarm's answer, where no movement occurred, seems to be resolved by specifying clientX/clientY rather than using positions relative to the selected element.

I used a little bit different from your code so you can try
cy.get('svg')
.trigger('mousedown', { which: 1 })
.trigger('dragstart', {})
.trigger('drag', {});

FWIW I had this same issue and using this library allowed me to interact with the D3 elements: https://github.com/dmtrKovalenko/cypress-real-events

Related

Trigger tool event on Paper.js

My app is using Paper.js as a framework for drawing elements, and I am currently coding some tests.
I need to trigger tool events manually, but I'm getting a 'emit is not a function' error.
I'm doing it like this:
tool.emit('mousedown', {
point: new Point(5, 5)
});
What's wrong with my code? According to Paper.js documentation:
emit(type, event) Emit an event on the tool.
Parameters: type:
String('mousedown'|'mouseup'|'mousedrag'|'mousemove'|'keydown'|'keyup')
— the event type event: Object — an object literal containing
properties describing the event Returns: Boolean — true if the event
had listeners, false otherwise
If I debug my code, tool is a Tool object, but emit doesn't exist.
It seems like emit is not the correct function to trigger event on Tool objects. Paper.js doesn't document fire function (at least in newer versions).
My code needs to be like this to work:
tool.fire('mousedown', {
point: new Point(5, 5)
});

Issue using Velocity.js UI animations without jquery

So I have a project in which I can't use jquery. I must use native js. Having used Velocity.js lately I wanted to use it again for this project. However in the doc and in this post in particular I couldn't find any advices in order to make Velocity UI animations (like transition.slideLeftIn for instance) work.
In the doc I did find an exemple but it's not about UI already made animations.
Velocity(document.getElementById("dummy"), { opacity: 0.5 }, { duration: 1000 });
After that I tried :
Velocity(myElement, { transition.slideLeftIn }, { duration: 1000 });
And
Velocity(myElement, transition.slideLeftIn, { duration: 1000 });
And
myElement.Velocity("transition.bounceLeftIn");
However none of these solutions are working.
Any ideas about how I could fix this ?
Thanks in advance :)
Everything you tried is either not valid JS or not following Velocity's API.
The first line you tried will raise a syntax error.
The second will probably raise a reference/value error. More specifically, transition.slideLeftIn should be a string, as in 'transition.slideLeftIn'.
The third will obviously raise another reference error since Velocity is set on the window object and does not extend Element.
So the right syntax will be:
Velocity(myElement, 'transition.slideLeftIn', { duration: 1000 });
Just thought I'd share another Answer for anyone who stumbles here through Google.
I also was having issues using Velocity JS without jQuery. However, my problem was related to not using the correct source code, even though I downloaded it from here
You should be able to find the correct code on the github page:
Latest Here
Here's a simple example using the API also:
Velocity(myElem, {boxShadowSpread: "5em"}, {easing: "easeIn", duration: 500});

simultaneously firing and events and location.path

We have a situation where we are utilizing a combination of events and routing inside of angular to handle navigation and communication between modules. Unfortunately the technique doesn't seem to work for us:
Neither this
Events.identifierSearch.fireOnChange(symbol);
$location.path('/explore');
nor this
Events.identifierSearch.fireOnChange(symbol);
$location.path('/explore');
Is working. The event handlers don't seem to fire. However a less optimal solution that we tried to demonstrate our event handlers are properly connected seems to work:
$location.path('/explore');
$timeout(function(){
var symbol = $event.target.innerText;
Events.identifierSearch.fireOnChange(symbol);
}, 500);
Now that is not what we want to have in our production code. In reality the "symbol" will be a large group of nested objects, so passing them on the URL of a route isn't ideal. And rewriting the routing seems like too much of a pain, it handles segregating partial pages nicely.
An idea i had would be to break this into two parts. First fire the event AND handle the event in the controller that fired it. This would require me adding a source to the event - so no sweat:
var controllerName = "thisBlahController" ;
Events.identifierSearch.fireOnChange(controllerName, symbol);
then in the same controller, handle the event and fire the location change:
$scope.$on(Events.identifierSearch.onChange, function() {
var source = Events.identifierSearch.source
if(source === controllerName) {
$location.path('/explore');
}
}
Again - it is not ideal, because it uses the assumption that the event has been handled by all sources prior to the route changing.
First, it would be great to know what it is in the angular routing that is causing the defusing of the events?
Second, I would love an official solution to this problem, one that has been battle tested.
Third, failing a systematic solution, can would like to hear opinions on the two solutions I have proposed up thread.
Quick modification
If i rewrite the order using the timeout approach:
Events.identifierSearch.fireOnChange(symbol);
$timeout(function(){
$location.path('/explore');
}, 1000);
The functionality doesn't seem to work either. Leading to question four Does a controller for a partial page, that is not the current ng-view get their event plumbing hooked up?

How to force DOM element creation with qx.ui.embed.Html?

I am trying to embed a RaphaelJS paper into qooxdoo widget. RaphaelJS is a SVG drawing library for JavaScript, and it needs to bind to a HTML <div> prior to any drawing.
For that, I call new qx.ui.embed.Html("<div id='raphael'></div>") and add it to my widget. After that, I should initialize Raphael by passing the div ID to it.
Problem is that <div id='raphael'> is not committed to the DOM model (i.e., no real DOM element is created) right after qx.ui.embed.Html() constructor call. The DOM element creation is indeed deferred until the widget is painted to the screen. I've managed to catch an appear event for the widget, and, after that, element's existence is guaranteed, and I can initialize Raphael library and do some drawing.
This approach assumes that I have to run all my application logic from within that appear event handler, which is probably not what I want. Is there any other way to get a widget in its ready-for-drawing state in the main application flow?
What you could do is create your own widget RaphaelWidget.js:
qx.Class.define("myApp.RaphaelWidget",
{
extend : qx.ui.core.Widget,
construct : function()
{
this.base(arguments);
this.addListener("appear", this._onAppear, this);
},
members :
{
/**
* Overwritten from qx.ui.core.Widget.
*/
_createContentElement : function()
{
return new qx.html.Element("div", {
overflowX: "hidden",
overflowY: "hidden",
border: "1px solid #aaa" // just for debugging
}, {"id": "canvas-raphael"});
},
_onAppear : function()
{
var paper = new Raphael(document.getElementById('canvas-raphael'), 250, 250);
var circle = paper.circle(100, 100, 80);
}
}
});
And then do for example in your Application.js:
var raphael = new myApp.RaphaelWidget();
raphael.setWidth(250);
raphael.setHeight(250);
this.getRoot().add(raphael);
Now you can develop your Raphael specific code in this new widget class.
Forcing rendering/DOM manipulation actions is called "flushing" in qooxdoo. E.g. the qx.html.Element from Richard's solution has a .flush() method. You might want to try this, or search the API documentation for the term 'flush'.
That being said flushing is a last resort, and shouldn't be used excessively as this would severely degrade performance. You shouldn't shy away from asynchronous programming when you are doing JavaScript. Even your "main" method is a callback, called from the qooxdoo runtime at some point in time.
There are several qooxdoo contributions that integrate third-party libraries the likes of Rafael. For a more idiomatic solution of doing this see e.g. QxDyGraphs (part. the __addCanvas method), a contrib that integrates the Dygraphs JS library.

Event handling in Dojo

Taking Jeff Atwood's advice, I decided to use a JavaScript library for the very basic to-do list application I'm writing. I picked the Dojo toolkit, version 1.1.1. At first, all was fine: the drag-and-drop code I wrote worked first time, you can drag tasks on-screen to change their order of precedence, and each drag-and-drop operation calls an event handler that sends an AJAX call to the server to let it know that order has been changed.
Then I went to add in the email tracking functionality. Standard stuff: new incoming emails have a unique ID number attached to their subject line, all subsequent emails about that problem can be tracked by simply leaving that ID number in the subject when you reply. So, we have a list of open tasks, each with their own ID number, and each of those tasks has a time-ordered list of associated emails. I wanted the text of those emails to be available to the user as they were looking at their list of tasks, so I made each task box a Dijit "Tree" control - top level contains the task description, branches contain email dates, and a single "leaf" off of each of those branches contains the email text.
First problem: I wanted the tree view to be fully-collapsed by default. After searching Google quite extensively, I found a number of solutions, all of which seemed to be valid for previous versions of Dojo but not the one I was using. I eventually figured out that the best solution would seem to be to have a event handler called when the Tree control had loaded that simply collapsed each branch/leaf. Unfortunately, even though the Tree control had been instantiated and its "startup" event handler called, the branches and leaves still hadn't loaded (the data was still being loaded via an AJAX call). So, I modified the system so that all email text and Tree structure is added server-side. This means the whole fully-populated Tree control is available when its startup event handler is called.
So, the startup event handler fully collapses the tree. Next, I couldn't find a "proper" way to have nice formatted text for the email leaves. I can put the email text in the leaf just fine, but any HTML gets escaped out and shows up in the web page. Cue more rummaging around Dojo's documentation (tends to be out of date, with code and examples for pre-1.0 versions) and Google. I eventually came up with the solution of getting JavaScript to go and read the SPAN element that's inside each leaf node and un-escape the escaped HTML code in it's innerHTML. I figured I'd put code to do this in with the fully-collapse-the-tree code, in the Tree control's startup event handler.
However... it turns out that the SPAN element isn't actually created until the user clicks on the expando (the little "+" symbol in a tree view you click to expand a node). Okay, fair enough - I'll add the re-formatting code to the onExpand() event handler, or whatever it's called. Which doesn't seem to exist. I've searched to documentation, I've searched Google... I'm quite possibly mis-understanding Dojo's "publish/subscribe" event handling system, but I think that mainly because there doesn't seem to be any comprehensive documentation for it anywhere (like, where do I find out what events I can subscribe to?).
So, in the end, the best solution I can come up with is to add an onClick event handler (not a "Dojo" event, but a plain JavaScript event that Dojo knows nothing about) to the expando node of each Tree branch that re-formats the HTML inside the SPAN element of each leaf. Except... when that is called, the SPAN element still doesn't exist (sometimes - other times it's been cached, just to further confuse you). Therefore, I have the event handler set up a timer that periodically calls a function that checks to see if the relevant SPAN element has turned up yet before then re-formatting it.
// An event handler called whenever a "email title" tree node is expanded.
function formatTreeNode(nodeID) {
if (dijit.byId(nodeID).getChildren().length != 0) {
clearInterval(nodeUpdateIntervalID);
messageBody = dijit.byId(nodeID).getChildren()[0].labelNode.innerHTML
if (messageBody.indexOf("<b>Message text:</b>") == -1) {
messageBody = messageBody.replace(/>/g, ">");
messageBody = messageBody.replace(/</g, "<");
messageBody = messageBody.replace(/&/g, "&");
dijit.byId(nodeID).getChildren()[0].labelNode.innerHTML = "<b>Message text:</b><div style=\"font-family:courier\">"+messageBody+"</div>";
}
}
}
// An event handler called when a tree node has been set up - we changed the default fully-expanded to fully-collapsed.
function setupTree(theTree) {
dijit.byId("tree-"+theTree).rootNode.collapse();
messageNode = dijit.byId("tree-"+theTree).rootNode.getChildren();
for (pl = 0; pl < messageNode.length; pl++) {
messageNode[pl].collapse();
messageNode[pl].expandoNode.onclick = eval("nodeUpdateIntervalID = setInterval(\"formatTreeNode('"+messageNode[pl].id+"')\",200); formatTreeNode('"+messageNode[pl].id+"');");
}
}
The above has the feel of a truly horrible hack, and I feel sure I must have taken a wrong turn somewhere early on in my thought process. Can someone please tell me:
The correct way to go about putting nicely-formatted text inside a Dojo/Dijit Tree control.
The correct way to handle Dojo events, like where I can figure out what events are available for me to subscribe to.
A better JavaScript library to use (can I do what I want to with JQuery and avoid the all-around-the-houses approach seen above?).
PS: If you're naming a software project, give thought to its name's uniqueness in Google - I'm sure searching for "Dojo" documentation in Google would be easier without all the martial arts results getting in the way.
PPS: Firefox spellchecker knows how to spell "Atwood", correcting me when I put two 'T's instead of one. Is Jeff just that famous now?
I assume that you followed the dijit.Tree and dojo.data in Dojo 1.1 tutorial which directed you to pass the data to the tree control using a data store. That had me banging my head of a brick wall for a while.
Its not really a great approach and the alternative is not really well documented. You need to create a use model instead. I have included an example below of a tree model that I created for displaying the structure of an LDAP directory.
You will find the default implementation of the model in your dojo distribution at ./dijit/_tree/model.js. The comments should help you understand the functions supported by the model.
The IDirectoryService class the code below are stubs for server-side Java POJOs generated by Direct Web Remoting (DWR). I highly recommend DWR if you going to be doing a lot of client-server interaction.
dojo.declare("LDAPDirectoryTreeModel", [ dijit.tree.model ], {
getRoot : function(onItem) {
IDirectoryService.getRoots( function(roots) {
onItem(roots[0])
});
},
mayHaveChildren : function(item) {
return true;
},
getChildren : function(parentItem, onComplete) {
IDirectoryService.getChildrenImpl(parentItem, onComplete);
},
getIdentity : function(item) {
return item.dn;
},
getLabel : function(item) {
return item.rdn;
}
});
And here is an extract from the my JSP page where I created the model and used it to populate the tree control.
<div
dojoType="LDAPDirectoryTreeModel"
jsid="treeModel"
id="treeModel">
</div>
<div
jsid="tree"
id="tree"
dojoType="dijit.Tree" model="treeModel"
labelAttr="name"
label="${directory.host}:${directory.port}">
</div>

Categories

Resources