3

i've got an issue with drag and drop in my protractor tests (like some other people), but not in general - only within an angular app using HTML5.

For demonstration i wrote a little protractor test-suite using two webpages demonstrating drag-and-drop functionality. The fist page (used in first test-case) demonstrates an implementation of jQueryUI drag and drop functionality in AngualarJS. This one is working just fine. The second page (used in second test-case) uses Angular drag & drop with HTML5. This one does not work within my test.

My next try was to use a helper-function this posting brought me too: https://gist.github.com/druska/624501b7209a74040175 Unfortunaly this didn't not work for me neighter as my 3rd test shows

Can anyone tell me why drag & drop with HTML 5 does not work and what i've got to do to get this running?

Many thanks in advance

Akki

My system:

  • protractor 4.0.0
  • selenium-server-standalone 2.53.1
  • chromedriver 2.22
  • iedriver 2.53.1
  • geckodriver 0.9.0

My testsuite:

describe('Protractor drag-and-drop test', function() {

    afterEach(function(){
        browser.sleep(5000);
    });

  it('1st test - jQueryUI drag and drop for AngularJS', function() {
      //found here: http://stackoverflow.com/questions/24315571/drag-drop-with-protractor-by-repeater
      browser.get('http://codef0rmer.github.io/angular-dragdrop/#!/');
      var elem = element(by.css('.ui-draggable'));
      var target = element(by.css('.thumbnail'));
      browser.sleep(3000);

      elem.click();
      browser.actions().dragAndDrop(elem, target).perform();
  });

   it('2nd test - Angular drag & drop with HTML5', function() {
       browser.get('http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/simple');

       var elem =element.all(by.xpath("/html/body/div[2]/div[2]/div[2]/div[1]/div[1]/div[2]/div/div[2]/ul/li[1]")).first();
       var target = $('ul[dnd-list=list]');

       expect(elem.getText()).toEqual("Item B1"); //Item that should be dragged and dropped
       expect(target.getText()).toContain("Item A1"); //element sorrounding "ItemA1", "Item A2", "ItemA3"

       elem.click();      
       browser.actions().dragAndDrop(elem, target).perform();
    });

    it('3rd test - Angular drag & drop with HTML5 with native_js_drag_and_drop_helper', function() {
       browser.get('http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/simple');
       var dragAndDropFn = require('./native_js_drag_and_drop_helper.js');

       var elem =element.all(by.xpath("/html/body/div[2]/div[2]/div[2]/div[1]/div[1]/div[2]/div/div[2]/ul/li[1]")).first();
        var target = $('ul[dnd-list=list]');

       expect(elem.getText()).toEqual("Item B1"); //Item that should be dragged and dropped
       expect(target.getText()).toContain("Item A1"); //element sorrounding "ItemA1", "Item A2", "ItemA3"

       elem.click();      
       browser.executeScript(dragAndDropFn, target.getWebElement(), elem.getWebElement());
    });

    xit(' 4th test - Test of native drag and drop helper ', function() {
        // test found on https://gist.github.com/druska/624501b7209a74040175 failing with "Angular could not be found on the page http://html5demos.com/drag : retries looking for angular exceeded"
        var dragAndDropFn = require('./native_js_drag_and_drop_helper.js');
        browser.get("http://html5demos.com/drag");
        var field = element.all(by.className('drag-handle')).get(0);
        var src = element.all(by.className('box-list-compact-hover')).get(0);
        browser.executeScript(dragAndDropFn, field.getWebElement(), src.getWebElement());
    }, 120000);
});

My configuration file:

exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['spec.js'],
capabilities: {
  //browserName: 'internet explorer'
  browserName: 'chrome'
  //browserName: 'firefox'
  },
};

The native_js_drag_and_drop_helper:

module.exports = function simulateDragDrop(sourceNode, destinationNode) {
var EVENT_TYPES = {
    DRAG_END: 'dragend',
    DRAG_START: 'dragstart',
    DROP: 'drop'
}

function createCustomEvent(type) {
    var event = new CustomEvent("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
}

function dispatchEvent(node, type, event) {
    if (node.dispatchEvent) {
        return node.dispatchEvent(event)
    }
    if (node.fireEvent) {
        return node.fireEvent("on" + type, event)
    }
}

var event = createCustomEvent(EVENT_TYPES.DRAG_START)
dispatchEvent(sourceNode, EVENT_TYPES.DRAG_START, event)

var dropEvent = createCustomEvent(EVENT_TYPES.DROP)
dropEvent.dataTransfer = event.dataTransfer
dispatchEvent(destinationNode, EVENT_TYPES.DROP, dropEvent)

var dragEndEvent = createCustomEvent(EVENT_TYPES.DRAG_END)
dragEndEvent.dataTransfer = event.dataTransfer
dispatchEvent(sourceNode, EVENT_TYPES.DRAG_END, dragEndEvent)
}

3 Answers3

1

I had the same issue with various dnd simulate libaries not seeming to work with angular-drag-and-drop-lists.

In the end I forked html-dnd simpling just adding a dragover event, something angular-drag-and-drop-lists requires as it calculates the index of the element being dropped upon via the dragover event. It also sticks in a temp li element which the code uses as the actual drop point before removing it. This is what the user sees as the shifting list points on the screen.

The fork is at forked html-dnd. I include it via a git pull in the dependancies in my package.json file

"html-dnd": "git://github.com/PloughingAByteField/html-dnd.git"

For usage in protractor

// at the top of the spec
var dragAndDrop = require('html-dnd').code;

<snip>

it('should dragover and drop', function() {
  var draggable = browser.findElement(by.id('id1'));
  var droppable = browser.findElement(by.id('id2'));
  browser.driver.executeScript(dragAndDrop, draggable, droppable);
);

Update: The html-dnd project has merged in the dragover event so you can use that instead of my not maintained fork.

0

Note sure if it's any different but I've used an ActionSequence call instead of just Action to drag and drop items which seemed to work just fine for me in our Angular app. It wasn't dealing with HTML5 though not sure if that makes a difference.

Also this was a while ago so again this may not be valid anymore but you could try something like:

//1. Obtain the list of all the measurement method draggables on the screen baItems.listOfMeasurementMethods.then(function (elements) {

            //Only perform drag and drops if there is more than one measurement method
            if (elements.length > 1) {
                var val1 = baItems.getLists();      //get the current text order

                console.log(elements.length);
                baItems.dragAndDrop(elements[0], elements[elements.length - 1]); //drag element 0 to last element and drop it
                helpers.sleepX(15000);

                var val2 = baItems.getLists();  //get the new text order

                expect(val2).not.toBe(val1);    //expect that they are not the same (i.e. they have been dragged and dropped)
            }
            else {
                console.log('Only one measurement method - cant change order');
            }

and then the drag and drop method for me looks like

this.dragAndDrop = function (dragFromElement, dropToElement) {
    //console.log('inside dragAndDrop()');
    new protractor.ActionSequence(browser).
            click(dragFromElement).
            dragAndDrop(dragFromElement.getLocation(), dropToElement.getLocation()).
            perform();
}
d.rodriguez
  • 319
  • 6
  • 17
0

I was also going through this face few days before, native_js_drag_and_drop_helper.js didn't worked for me as well... Then I got to know about simulate.js api I have used its code and that fortunately worked for me...

code for dragdrop.js

module.exports = function (dragEleSelector, dropEleSelector) {
    (function ($, undefined) {
        var rkeyEvent = /^key/,
            rmouseEvent = /^(?:mouse|contextmenu)|click/;

        $.fn.simulate = function (type, options) {
            return this.each(function () {
                new $.simulate(this, type, options);
            });
        };

        $.simulate = function (elem, type, options) {
            var method = $.camelCase("simulate-" + type);

            this.target = elem;
            this.options = options;

            if (this[ method ]) {
                this[ method ]();
            } else {
                this.simulateEvent(elem, type, options);
            }
        };

        $.extend($.simulate, {
            keyCode: {
                BACKSPACE: 8,
                COMMA: 188,
                DELETE: 46,
                DOWN: 40,
                END: 35,
                ENTER: 13,
                ESCAPE: 27,
                HOME: 36,
                LEFT: 37,
                NUMPAD_ADD: 107,
                NUMPAD_DECIMAL: 110,
                NUMPAD_DIVIDE: 111,
                NUMPAD_ENTER: 108,
                NUMPAD_MULTIPLY: 106,
                NUMPAD_SUBTRACT: 109,
                PAGE_DOWN: 34,
                PAGE_UP: 33,
                PERIOD: 190,
                RIGHT: 39,
                SPACE: 32,
                TAB: 9,
                UP: 38
            },

            buttonCode: {
                LEFT: 0,
                MIDDLE: 1,
                RIGHT: 2
            }
        });

        $.extend($.simulate.prototype, {

            simulateEvent: function (elem, type, options) {
                var event = this.createEvent(type, options);
                this.dispatchEvent(elem, type, event, options);
            },

            createEvent: function (type, options) {
                if (rkeyEvent.test(type)) {
                    return this.keyEvent(type, options);
                }

                if (rmouseEvent.test(type)) {
                    return this.mouseEvent(type, options);
                }
            },

            mouseEvent: function (type, options) {
                var event, eventDoc, doc, body;
                options = $.extend({
                    bubbles: true,
                    cancelable: (type !== "mousemove"),
                    view: window,
                    detail: 0,
                    screenX: 0,
                    screenY: 0,
                    clientX: 1,
                    clientY: 1,
                    ctrlKey: false,
                    altKey: false,
                    shiftKey: false,
                    metaKey: false,
                    button: 0,
                    relatedTarget: undefined
                }, options);

                if (document.createEvent) {
                    event = document.createEvent("MouseEvents");
                    event.initMouseEvent(type, options.bubbles, options.cancelable,
                        options.view, options.detail,
                        options.screenX, options.screenY, options.clientX, options.clientY,
                        options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
                        options.button, options.relatedTarget || document.body.parentNode);

                    // IE 9+ creates events with pageX and pageY set to 0.
                    // Trying to modify the properties throws an error,
                    // so we define getters to return the correct values.
                    if (event.pageX === 0 && event.pageY === 0 && Object.defineProperty) {
                        eventDoc = event.relatedTarget.ownerDocument || document;
                        doc = eventDoc.documentElement;
                        body = eventDoc.body;

                        Object.defineProperty(event, "pageX", {
                            get: function () {
                                return options.clientX +
                                    ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
                                    ( doc && doc.clientLeft || body && body.clientLeft || 0 );
                            }
                        });
                        Object.defineProperty(event, "pageY", {
                            get: function () {
                                return options.clientY +
                                    ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
                                    ( doc && doc.clientTop || body && body.clientTop || 0 );
                            }
                        });
                    }
                } else if (document.createEventObject) {
                    event = document.createEventObject();
                    $.extend(event, options);
                    // standards event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ff974877(v=vs.85).aspx
                    // old IE event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ms533544(v=vs.85).aspx
                    // so we actually need to map the standard back to oldIE
                    event.button = {
                        0: 1,
                        1: 4,
                        2: 2
                    }[ event.button ] || ( event.button === -1 ? 0 : event.button );
                }

                return event;
            },

            keyEvent: function (type, options) {
                var event;
                options = $.extend({
                    bubbles: true,
                    cancelable: true,
                    view: window,
                    ctrlKey: false,
                    altKey: false,
                    shiftKey: false,
                    metaKey: false,
                    keyCode: 0,
                    charCode: undefined
                }, options);

                if (document.createEvent) {
                    try {
                        event = document.createEvent("KeyEvents");
                        event.initKeyEvent(type, options.bubbles, options.cancelable, options.view,
                            options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
                            options.keyCode, options.charCode);
                        // initKeyEvent throws an exception in WebKit
                        // see: http://stackoverflow.com/questions/6406784/initkeyevent-keypress-only-works-in-firefox-need-a-cross-browser-solution
                        // and also https://bugs.webkit.org/show_bug.cgi?id=13368
                        // fall back to a generic event until we decide to implement initKeyboardEvent
                    } catch (err) {
                        event = document.createEvent("Events");
                        event.initEvent(type, options.bubbles, options.cancelable);
                        $.extend(event, {
                            view: options.view,
                            ctrlKey: options.ctrlKey,
                            altKey: options.altKey,
                            shiftKey: options.shiftKey,
                            metaKey: options.metaKey,
                            keyCode: options.keyCode,
                            charCode: options.charCode
                        });
                    }
                } else if (document.createEventObject) {
                    event = document.createEventObject();
                    $.extend(event, options);
                }

                if (!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()) || (({}).toString.call(window.opera) === "[object Opera]")) {
                    event.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode;
                    event.charCode = undefined;
                }

                return event;
            },

            dispatchEvent: function (elem, type, event) {
                if (elem[ type ]) {
                    elem[ type ]();
                } else if (elem.dispatchEvent) {
                    elem.dispatchEvent(event);
                } else if (elem.fireEvent) {
                    elem.fireEvent("on" + type, event);
                }
            },

            simulateFocus: function () {
                var focusinEvent,
                    triggered = false,
                    element = $(this.target);

                function trigger() {
                    triggered = true;
                }

                element.bind("focus", trigger);
                element[ 0 ].focus();

                if (!triggered) {
                    focusinEvent = $.Event("focusin");
                    focusinEvent.preventDefault();
                    element.trigger(focusinEvent);
                    element.triggerHandler("focus");
                }
                element.unbind("focus", trigger);
            },

            simulateBlur: function () {
                var focusoutEvent,
                    triggered = false,
                    element = $(this.target);

                function trigger() {
                    triggered = true;
                }

                element.bind("blur", trigger);
                element[ 0 ].blur();

                // blur events are async in IE
                setTimeout(function () {
                    // IE won't let the blur occur if the window is inactive
                    if (element[ 0 ].ownerDocument.activeElement === element[ 0 ]) {
                        element[ 0 ].ownerDocument.body.focus();
                    }

                    // Firefox won't trigger events if the window is inactive
                    // IE doesn't trigger events if we had to manually focus the body
                    if (!triggered) {
                        focusoutEvent = $.Event("focusout");
                        focusoutEvent.preventDefault();
                        element.trigger(focusoutEvent);
                        element.triggerHandler("blur");
                    }
                    element.unbind("blur", trigger);
                }, 1);
            }
        });

        /** complex events **/
        function findCenter(elem) {
            var offset,
                document = $(elem.ownerDocument);
            elem = $(elem);
            offset = elem.offset();

            return {
                x: offset.left + elem.outerWidth() / 2 - document.scrollLeft(),
                y: offset.top + elem.outerHeight() / 2 - document.scrollTop()
            };
        }

        function findCorner(elem) {
            var offset,
                document = $(elem.ownerDocument);
            elem = $(elem);
            offset = elem.offset();

            return {
                x: offset.left - document.scrollLeft(),
                y: offset.top - document.scrollTop()
            };
        }

        $.extend($.simulate.prototype, {
            simulateDrag: function () {
                var i = 0,
                    target = this.target,
                    options = this.options,
                    center = options.handle === "corner" ? findCorner(target) : findCenter(target),
                    x = Math.floor(center.x),
                    y = Math.floor(center.y),
                    coord = { clientX: x, clientY: y },
                    dx = options.dx || ( options.x !== undefined ? options.x - x : 0 ),
                    dy = options.dy || ( options.y !== undefined ? options.y - y : 0 ),
                    moves = options.moves || 3;

                this.simulateEvent(target, "mousedown", coord);

                for (; i < moves; i++) {
                    x += dx / moves;
                    y += dy / moves;

                    coord = {
                        clientX: Math.round(x),
                        clientY: Math.round(y)
                    };

                    this.simulateEvent(target.ownerDocument, "mousemove", coord);
                }

                if ($.contains(document, target)) {
                    this.simulateEvent(target, "mouseup", coord);
                    this.simulateEvent(target, "click", coord);
                } else {
                    this.simulateEvent(document, "mouseup", coord);
                }
            }
        });

    })($);

    try {
        var dragEle = $(dragEleSelector);
        var dropEle = $(dropEleSelector);
        if (dragEle.length == 0 || dropEle.length == 0) {
            console.error("Unable to perform drag n drop operation: Selectors are incorrect.");
            return false;
        }
        var droppableOffset = dropEle.offset(),
            draggableOffset = dragEle.offset(),
            dx = droppableOffset.left - draggableOffset.left,
            dy = droppableOffset.top - draggableOffset.top;

        dragEle.simulate("drag", {
            dx: dx,
            dy: dy
        });
        return true;
    } catch (err) {
        console.error("Unable to perform drag n drop operation.");
        return false;
    }
};

You will have to execute this script against the DOM elements specified by the CSS selectors.

var dragDrop = require('../../common/javascript/dragDrop.js');

//keep this in mind selectors are CSS selectors.
function dragDropElement(dragEleSelector, dropEleSelector) {
    var deferred = protractor.promise.defer();
    browser.executeScript(dragDrop, dragEleSelector, dropEleSelector).then(function (dropSuccessful) {
        expect(dropSuccessful).toBe(true);
        if (dropSuccessful) {
            console.log("Element dropped successfully on target.");
            deferred.fulfill(dropSuccessful);
        } else {
            console.log("Fail: Not able to drop element on target.");
            deferred.reject(dropSuccessful);
        }
    });
    return deferred.promise;
};

Though you will not be able to see the drag drop animation but it will work.