JavaScript workaround for drag and drop in Selenium WebDriver

29,091

Solution 1

Since I posted this question, I've found a few different solutions to this problem. I'm posting them here for reference so that this question is useful to others:

When drag and drop in Selenium failed to work for HTML5, a defect was entered against Selenium and some people commented on the defect with suggested Javascript work around solutions:

JQuery/HTML5 Solution

http://code.google.com/p/selenium/issues/detail?id=3604#c9

This is a ruby-based solution, which could be translated into Java, C#, or any other language supported by Selenium. It requires JQuery to be on the page, as is clear from many of the comments from other users trying to use this solution, and it seems this only works for HTML5 pages:

The workaround we put together is working for us. It's been a life saver for testing our Ember.js app.

attached is the latest version of what we are using...

this is what we have in our test_helper:

  def drag_and_drop(source,target)

    js_filepath=File.dirname(__FILE__)+"/drag_and_drop_helper.js"
    js_file= File.new(js_filepath,"r")
    java_script=""

    while (line=js_file.gets)
      java_script+=line
    end

    js_file.close

    @driver.execute_script(java_script+"$('#{source}').simulateDragDrop({ dropTarget: '#{target}'});")

    rescue Exception => e
      puts "ERROR :" + e.to_s

  end

The source code for the referenced Javascript drag and drop simulation is posted here:

https://gist.github.com/rcorreia/2362544

(function( $ ) {
    $.fn.simulateDragDrop = function(options) {
        return this.each(function() {
            new $.simulateDragDrop(this, options);
        });
    };
    $.simulateDragDrop = function(elem, options) {
        this.options = options;
        this.simulateEvent(elem, options);
    };
    $.extend($.simulateDragDrop.prototype, {
        simulateEvent: function(elem, options) {
            /*Simulating drag start*/
            var type = 'dragstart';
            var event = this.createEvent(type);
            this.dispatchEvent(elem, type, event);

            /*Simulating drop*/
            type = 'drop';
            var dropEvent = this.createEvent(type, {});
            dropEvent.dataTransfer = event.dataTransfer;
            this.dispatchEvent($(options.dropTarget)[0], type, dropEvent);

            /*Simulating drag end*/
            type = 'dragend';
            var dragEndEvent = this.createEvent(type, {});
            dragEndEvent.dataTransfer = event.dataTransfer;
            this.dispatchEvent(elem, type, dragEndEvent);
        },
        createEvent: function(type) {
            var event = document.createEvent("CustomEvent");
            event.initCustomEvent(type, true, true, null);
            event.dataTransfer = {
                data: {
                },
                setData: function(type, val){
                    this.data[type] = val;
                },
                getData: function(type){
                    return this.data[type];
                }
            };
            return event;
        },
        dispatchEvent: function(elem, type, event) {
            if(elem.dispatchEvent) {
                elem.dispatchEvent(event);
            }else if( elem.fireEvent ) {
                elem.fireEvent("on"+type, event);
            }
        }
    });
})(jQuery);

Another user posted additional code when it didn't work completely for him, along with some instructions for injecting JQuery into the page:

http://code.google.com/p/selenium/issues/detail?id=3604#c25

We had trouble with the original drag_and_drop_helper.js posted as a workaround for this issue. The workaround is 99% correct, but I needed to modify the workaround to include the dropTarget in the options propagated via the 'coord' object in simulateDrag.

i.e. I need to change:

  coord = { clientX: x, clientY: y }

to:

coord = { clientX: x, clientY: y , dropTarget: options.dropTarget || undefined }

Also, a note for those following the example usage, if the app under test does not already alias the jQuery function to $, you will need to spell-out jQuery:

i.e., after injecting the drag and drop helper onto the page, we have a method that accepts jQuery selectors to use the simulated drag and drop functions (Java):

/**
 * Drag and drop via the JQuery-based drag and drop helper -- the helper
 * must have been injected onto the page prior to calling this method.
 *
 * @param dragSourceJQuerySelector a JQuery-style selector that identifies the source element to drag;
 * <em>will be passed directly to jQuery(), perform all quoting yourself</em>
 * @param dropTargetJQuerySelector a JQuery-style selector that identifies the target element to drop the source onto;
 * <em>will be passed directly to jQuery(), perform all quoting yourself</em>
 */
protected void dragAndDropViaJQueryHelper(String dragSourceJQuerySelector, String dropTargetJQuerySelector) {
    String javascript =
        "var dropTarget = jQuery(" + dropTargetJQuerySelector + ");" +
        "\n" +
        "jQuery("+ dragSourceJQuerySelector + ").simulate('drag', { dropTarget: dropTarget });";

    getLogger().info("executing javascript:\n" + javascript);
    this.executeScript(javascript);
    getLogger().info("executed drag-n-drop action via javascript");
}

Another, non-jQuery solution

http://ynot408.wordpress.com/2011/09/22/drag-and-drop-using-selenium-webdriver/

Javascript based drag and drop which works across browsers.

Mouse move stopped working after version 2.3 while using RemoteWebDriver in selenium. The below function drags element 1 to element 2 position and releases the mouse down.

public void dragdrop(By ByFrom, By ByTo) {
    WebElement LocatorFrom = driver.findElement(ByFrom);
    WebElement LocatorTo = driver.findElement(ByTo);
    String xto=Integer.toString(LocatorTo.getLocation().x);
    String yto=Integer.toString(LocatorTo.getLocation().y);
    ((JavascriptExecutor)driver).executeScript("function simulate(f,c,d,e){var b,a=null;for(b in eventMatchers)if(eventMatchers[b].test(c)){a=b;break}if(!a)return!1;document.createEvent?(b=document.createEvent(a),a==\"HTMLEvents\"?b.initEvent(c,!0,!0):b.initMouseEvent(c,!0,!0,document.defaultView,0,d,e,d,e,!1,!1,!1,!1,0,null),f.dispatchEvent(b)):(a=document.createEventObject(),a.detail=0,a.screenX=d,a.screenY=e,a.clientX=d,a.clientY=e,a.ctrlKey=!1,a.altKey=!1,a.shiftKey=!1,a.metaKey=!1,a.button=1,f.fireEvent(\"on\"+c,a));return!0} var eventMatchers={HTMLEvents:/^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/,MouseEvents:/^(?:click|dblclick|mouse(?:down|up|over|move|out))$/}; " +
    "simulate(arguments[0],\"mousedown\",0,0); simulate(arguments[0],\"mousemove\",arguments[1],arguments[2]); simulate(arguments[0],\"mouseup\",arguments[1],arguments[2]); ",
    LocatorFrom,xto,yto);
}

Solution 2

I had exactly the same issue with Firefox which led me here. Playing around with the suggestions on Stack, finally I was able to resolve the issue using

actions.DragAndDropToOffset(element, x, y).Perform();

I know that's not the Javascript workaround you asked for but hopefully it will work for you, as it did for me. These quirky issues can be really frustrating.

Solution 3

On back of this post re Safari 14, I was testing a kendo application and tried the solutions in Selena's answer but it was a different solution that ended up working for me.

Test application.

Working drap and drop (small change to use WebElements rather than CSS strings):

var triggerDragAndDrop = function (elemDrag, elemDrop) {
  /* function for triggering mouse events */
  var fireMouseEvent = function (type, elem, centerX, centerY) {
    var evt = document.createEvent('MouseEvents');
    evt.initMouseEvent(
      type,
      true,
      true,
      window,
      1,
      1,
      1,
      centerX,
      centerY,
      false,
      false,
      false,
      false,
      0,
      elem
    );
    elem.dispatchEvent(evt);
  };

  /* calculate positions*/
  var pos = elemDrag.getBoundingClientRect();
  var center1X = Math.floor((pos.left + pos.right) / 2);
  var center1Y = Math.floor((pos.top + pos.bottom) / 2);
  pos = elemDrop.getBoundingClientRect();
  var center2X = Math.floor((pos.left + pos.right) / 2);
  var center2Y = Math.floor((pos.top + pos.bottom) / 2);

  /* mouse over dragged element and mousedown*/
  fireMouseEvent('mousemove', elemDrag, center1X, center1Y);
  fireMouseEvent('mouseenter', elemDrag, center1X, center1Y);
  fireMouseEvent('mouseover', elemDrag, center1X, center1Y);
  fireMouseEvent('mousedown', elemDrag, center1X, center1Y);

  /* start dragging process over to drop target*/
  fireMouseEvent('dragstart', elemDrag, center1X, center1Y);
  fireMouseEvent('drag', elemDrag, center1X, center1Y);
  fireMouseEvent('mousemove', elemDrag, center1X, center1Y);
  fireMouseEvent('drag', elemDrag, center2X, center2Y);
  fireMouseEvent('mousemove', elemDrop, center2X, center2Y);

  /* trigger dragging process on top of drop target*/
  fireMouseEvent('mouseenter', elemDrop, center2X, center2Y);
  fireMouseEvent('dragenter', elemDrop, center2X, center2Y);
  fireMouseEvent('mouseover', elemDrop, center2X, center2Y);
  fireMouseEvent('dragover', elemDrop, center2X, center2Y);

  /* release dragged element on top of drop target*/
  fireMouseEvent('drop', elemDrop, center2X, center2Y);
  fireMouseEvent('dragend', elemDrag, center2X, center2Y);
  fireMouseEvent('mouseup', elemDrag, center2X, center2Y);

  return true;
};

Injected javascript as per solutions in Selena's answers above (read file into variable java_script). Executed against existing WebElements (drag and drop) with:

    String js =  "var src = arguments[0];var dest = arguments[1];";
    js += "triggerDragAndDrop(src, dest);";
    JavascriptExecutor executor = (JavascriptExecutor) webdriver;
    executor.executeScript(java_script + js, drag, drop);
Share:
29,091
Selena
Author by

Selena

Updated on July 09, 2022

Comments

  • Selena
    Selena almost 2 years

    clickAndHold is not working for me in my test environment setup. I keep getting this error when trying to execute it using the Advanced User Interactions:

    "Cannot press more than one button or an already pressed button.' when calling method: [wdIMouse::down]"

    I've tested numerous versions of Firefox with selenium versions 2.31.0-2.35.0 and Firefox 21 with selenium 2.35 has the fewest issues. Other combinations have problems with click() failing silently and visible elements being treated as invisible.

    I want to use a JavaScript workaround for drag and drop of one element to another, but I can't find any decent examples anywhere after googling extensively.

  • Forever Noob
    Forever Noob over 2 years
    The non-jQuery solution has helped a lot. Thank you very much posting it.