Drag and Drop für HTML 5 Elemente mit Selenium

Normalerweise ist Drag and Drop mit Selenium kein Problem. In der Standard-Bibliothek von Selenium existiert die Möglichkeit eine DragDrop Action zu erstellen:

Actions actions = new Actions(driver);
actions.DragAndDrop(element1, element2);
actions.Build();
actions.Perform();

Dieses Vorgehen funktioniert auch absolut problemlos mit der HTML4 JS Lösungen. Seit der HTML5 Version klappt es mit Selenium leider überhaupt nicht mehr. Auch die Kombination aus ClickAndHold Aktion, MoveToElement und Release bringt einen bei dem neuen HTML Standard kein Stück weiter.
Dieser Fehler ist bekannt und ist bereits seit 2012 offen: Issue 3604: HTML5 Drag and Drop with Selenium Webdriver

Es gibt in der oben genannten Google Code Diskussion einen Beispiel mit Ausführung von JavaScript, die sehr gut funktioniert. Allerdings verwendet dieses Beispiel JQuery um die Elemente auf der Webseite zu identifizieren. Auf Webseiten die einen JQuery Verweis besitzen, klappt dieses Vorgehen wie gewünscht, heißt aber leider, dass der Code auf Webseiten die diesen Verweis nicht besitzen, eine Exception „jQuery is not defined“ ausgibt. Jetzt könnte man natürlich an der Stelle hingehen und den Header der Webseite manipulieren um einen JQuery Verweis einzufügen, es gibt aber eine viel elegantere Lösung.

Diese hat der Poster des Beitrages #58 in Java hinzugefügt: Issue 3604: HTML5 Drag and Drop with Selenium Webdriver – Post 58.
Die dort beschriebene Lösung verwendet nur natives JavaScript, dadurch ist die JQuery Referenz nicht mehr notwendig. Die Java Programmierer unter euch können sich den Code dort direkt 1 zu 1 übernehmen (bis auf die kleine Bemerkung in dem Kommentar 59, dieser ist für Chrome User relevant).
Für die C# User habe ich den oben geschriebenen Code in eine statische Helper Klasse übertragen:

using OpenQA.Selenium;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SeleniumFramework.Helper
{
    public static class DragDropHelper
    {
        public enum Position
        {
            Top_Left,
            Top,
            Top_Right,
            Left,
            Center,
            Right,
            Bottom_Left,
            Bottom,
            Bottom_Right
        }

        public static int getX(Position pos, int width)
        {
            if (Position.Top_Left.Equals(pos) || Position.Left.Equals(pos) || Position.Bottom_Left.Equals(pos))
            {
                return 1;
            }
            else if (Position.Top.Equals(pos) || Position.Center.Equals(pos) || Position.Bottom.Equals(pos))
            {
                return width / 2;
            }
            else if (Position.Top_Right.Equals(pos) || Position.Right.Equals(pos) || Position.Bottom_Right.Equals(pos))
            {
                return width - 1;
            }
            else
            {
                return 0;
            }
        }

        static int getY(Position pos, int height)
        {
            if (Position.Top_Left.Equals(pos) || Position.Top.Equals(pos) || Position.Top_Right.Equals(pos))
            {
                return 1;
            }
            else if (Position.Left.Equals(pos) || Position.Center.Equals(pos) || Position.Right.Equals(pos))
            {
                return height / 2;
            }
            else if (Position.Bottom_Left.Equals(pos) || Position.Bottom.Equals(pos) || Position.Bottom_Right.Equals(pos))
            {
                return height - 1;
            }
            else
            {
                return 0;
            }
        }

        private static string javaScriptEventSimulator = "" +
            /* Creates a drag event */
            "function createDragEvent(eventName, options)\r\n" +
            "{\r\n" +
            "var event = document.createEvent('HTMLEvents');\r\n" +
            "event.initEvent('DragEvent', true, true);\r\n" +
            //"	var event = document.createEvent(\"DragEvent\");\r\n" +
            "	var screenX = window.screenX + options.clientX;\r\n" +
            "	var screenY = window.screenY + options.clientY;\r\n" +
            "	var clientX = options.clientX;\r\n" +
            "	var clientY = options.clientY;\r\n" +
            "	var dataTransfer = {\r\n" +
            "		data: options.dragData == null ? {} : options.dragData,\r\n" +
            "		setData: function(eventName, val){\r\n" +
            "			if (typeof val === 'string') {\r\n" +
            "				this.data[eventName] = val;\r\n" +
            "			}\r\n" +
            "		},\r\n" +
            "		getData: function(eventName){\r\n" +
            "			return this.data[eventName];\r\n" +
            "		},\r\n" +
            "		clearData: function(){\r\n" +
            "			return this.data = {};\r\n" +
            "		},\r\n" +
            "		setDragImage: function(dragElement, x, y) {}\r\n" +
            "	};\r\n" +
            "	var eventInitialized=false;\r\n" +
            "	if (event != null && event.initDragEvent) {\r\n" +
            "		try {\r\n" +
            "			event.initDragEvent(eventName, true, true, window, 0, screenX, screenY, clientX, clientY, false, false, false, false, 0, null, dataTransfer);\r\n" +
            "			event.initialized=true;\r\n" +
            "		} catch(err) {\r\n" +
            "			// no-op\r\n" +
            "		}\r\n" +
            "	}\r\n" +
            "	if (!eventInitialized) {\r\n" +
            "		event = document.createEvent(\"CustomEvent\");\r\n" +
            "		event.initCustomEvent(eventName, true, true, null);\r\n" +
            "		event.view = window;\r\n" +
            "		event.detail = 0;\r\n" +
            "		event.screenX = screenX;\r\n" +
            "		event.screenY = screenY;\r\n" +
            "		event.clientX = clientX;\r\n" +
            "		event.clientY = clientY;\r\n" +
            "		event.ctrlKey = false;\r\n" +
            "		event.altKey = false;\r\n" +
            "		event.shiftKey = false;\r\n" +
            "		event.metaKey = false;\r\n" +
            "		event.button = 0;\r\n" +
            "		event.relatedTarget = null;\r\n" +
            "		event.dataTransfer = dataTransfer;\r\n" +
            "	}\r\n" +
            "	return event;\r\n" +
            "}\r\n" +

            /* Creates a mouse event */
            "function createMouseEvent(eventName, options)\r\n" +
            "{\r\n" +
            "	var event = document.createEvent(\"MouseEvent\");\r\n" +
            "	var screenX = window.screenX + options.clientX;\r\n" +
            "	var screenY = window.screenY + options.clientY;\r\n" +
            "	var clientX = options.clientX;\r\n" +
            "	var clientY = options.clientY;\r\n" +
            "	if (event != null && event.initMouseEvent) {\r\n" +
            "		event.initMouseEvent(eventName, true, true, window, 0, screenX, screenY, clientX, clientY, false, false, false, false, 0, null);\r\n" +
            "	} else {\r\n" +
            "		event = document.createEvent(\"CustomEvent\");\r\n" +
            "		event.initCustomEvent(eventName, true, true, null);\r\n" +
            "		event.view = window;\r\n" +
            "		event.detail = 0;\r\n" +
            "		event.screenX = screenX;\r\n" +
            "		event.screenY = screenY;\r\n" +
            "		event.clientX = clientX;\r\n" +
            "		event.clientY = clientY;\r\n" +
            "		event.ctrlKey = false;\r\n" +
            "		event.altKey = false;\r\n" +
            "		event.shiftKey = false;\r\n" +
            "		event.metaKey = false;\r\n" +
            "		event.button = 0;\r\n" +
            "		event.relatedTarget = null;\r\n" +
            "	}\r\n" +
            "	return event;\r\n" +
            "}\r\n" +

            /* Runs the events */
            "function dispatchEvent(webElement, eventName, event)\r\n" +
            "{\r\n" +
            "	if (webElement.dispatchEvent) {\r\n" +
            "		webElement.dispatchEvent(event);\r\n" +
            "	} else if (webElement.fireEvent) {\r\n" +
            "		webElement.fireEvent(\"on\"+eventName, event);\r\n" +
            "	}\r\n" +
            "}\r\n" +

            /* Simulates an individual event */
            "function simulateEventCall(element, eventName, dragStartEvent, options) {\r\n" +
            "	var event = null;\r\n" +
            "	if (eventName.indexOf(\"mouse\") > -1) {\r\n" +
            "		event = createMouseEvent(eventName, options);\r\n" +
            "	} else {\r\n" +
            "		event = createDragEvent(eventName, options);\r\n" +
            "	}\r\n" +
            "	if (dragStartEvent != null) {\r\n" +
            "		event.dataTransfer = dragStartEvent.dataTransfer;\r\n" +
            "	}\r\n" +
            "	dispatchEvent(element, eventName, event);\r\n" +
            "	return event;\r\n" +
            "}\r\n";

        /**
         * Simulates an individual events
         */
        private static string simulateEvent = javaScriptEventSimulator +
                "function simulateEvent(element, eventName, clientX, clientY, dragData) {\r\n" +
                "	return simulateEventCall(element, eventName, null, {clientX: clientX, clientY: clientY, dragData: dragData});\r\n" +
                "}\r\n" +

                "var event = simulateEvent(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);\r\n" +
                "if (event.dataTransfer != null) {\r\n" +
                "	return event.dataTransfer.data;\r\n" +
                "}\r\n";

        /**
         * Simulates drag and drop
         */
        private static string simulateHTML5DragAndDrop = javaScriptEventSimulator +
                "function simulateHTML5DragAndDrop(dragFrom, dragTo, dragFromX, dragFromY, dragToX, dragToY) {\r\n" +
                "	var mouseDownEvent = simulateEventCall(dragFrom, \"mousedown\", null, {clientX: dragFromX, clientY: dragFromY});\r\n" +
                "	var dragStartEvent = simulateEventCall(dragFrom, \"dragstart\", null, {clientX: dragFromX, clientY: dragFromY});\r\n" +
                "	var dragEnterEvent = simulateEventCall(dragTo,   \"dragenter\", dragStartEvent, {clientX: dragToX, clientY: dragToY});\r\n" +
                "	var dragOverEvent  = simulateEventCall(dragTo,   \"dragover\",  dragStartEvent, {clientX: dragToX, clientY: dragToY});\r\n" +
                "	var dropEvent      = simulateEventCall(dragTo,   \"drop\",      dragStartEvent, {clientX: dragToX, clientY: dragToY});\r\n" +
                "	var dragEndEvent   = simulateEventCall(dragFrom, \"dragend\",   dragStartEvent, {clientX: dragToX, clientY: dragToY});\r\n" +
                "}\r\n" +
                "simulateHTML5DragAndDrop(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);\r\n";

        /**
         * Calls a drag event
         *
         * @param driver
         *            The WebDriver to execute on
         * @param dragFrom
         *            The WebElement to simulate on
         * @param eventName
         *            The event name to call
         * @param clientX
         *            The mouse click X position on the screen
         * @param clientY
         *            The mouse click Y position on the screen
         * @param data
         *            The data transfer data
         * @return The updated data transfer data
         */
        public static Object html5_simulateEvent(IWebDriver driver, IWebElement dragFrom, String eventName, int clientX, int clientY, Object data)
        {
            return ((IJavaScriptExecutor)driver).ExecuteScript(simulateEvent, dragFrom, eventName, clientX, clientY, data);
        }

        /**
         * Calls a drag event
         *
         * @param driver
         *            The WebDriver to execute on
         * @param dragFrom
         *            The WebElement to simulate on
         * @param eventName
         *            The event name to call
         * @param mousePosition
         *            The mouse click area in the element
         * @param data
         *            The data transfer data
         * @return The updated data transfer data
         */
        public static Object html5_simulateEvent(IWebDriver driver, IWebElement dragFrom, String eventName, Position mousePosition, Object data)
        {
            Point fromLocation = dragFrom.Location;
            Size fromSize = dragFrom.Size;

            // Get Client X and Client Y locations
            int clientX = fromLocation.X + (fromSize == null ? 0 : getX(mousePosition, fromSize.Width));
            int clientY = fromLocation.Y + (fromSize == null ? 0 : getY(mousePosition, fromSize.Height));

            return html5_simulateEvent(driver, dragFrom, eventName, clientX, clientY, data);
        }

        /**
         * Drags and drops a web element from source to target
         *
         * @param driver
         *            The WebDriver to execute on
         * @param dragFrom
         *            The WebElement to drag from
         * @param dragTo
         *            The WebElement to drag to
         * @param dragFromX
         *            The position to click relative to the top-left-corner of the
         *            client
         * @param dragFromY
         *            The position to click relative to the top-left-corner of the
         *            client
         * @param dragToX
         *            The position to release relative to the top-left-corner of the
         *            client
         * @param dragToY
         *            The position to release relative to the top-left-corner of the
         *            client
         */
        public static void html5_DragAndDrop(IWebDriver driver, IWebElement dragFrom, IWebElement dragTo, int dragFromX, int dragFromY, int dragToX, int dragToY)
        {
            ((IJavaScriptExecutor)driver).ExecuteScript(simulateHTML5DragAndDrop, dragFrom, dragTo, dragFromX, dragFromY, dragToX, dragToY);
        }

        /**
         * Drags and drops a web element from source to target
         *
         * @param driver
         *            The WebDriver to execute on
         * @param dragFrom
         *            The WebElement to drag from
         * @param dragTo
         *            The WebElement to drag to
         * @param dragFromPosition
         *            The place to click on the dragFrom
         * @param dragToPosition
         *            The place to release on the dragTo
         */
        public static void html5_DragAndDrop(IWebDriver driver, IWebElement dragFrom, IWebElement dragTo, Position dragFromPosition, Position dragToPosition)
        {
            Point fromLocation = dragFrom.Location;
            Point toLocation = dragTo.Location;
            Size fromSize = dragFrom.Size;
            Size toSize = dragTo.Size;

            // Get Client X and Client Y locations
            int dragFromX = fromLocation.X + (fromSize == null ? 0 : getX(dragFromPosition, fromSize.Width));
            int dragFromY = fromLocation.Y + (fromSize == null ? 0 : getY(dragFromPosition, fromSize.Height));
            int dragToX = toLocation.X + (toSize == null ? 0 : getX(dragToPosition, toSize.Width));
            int dragToY = toLocation.Y + (toSize == null ? 0 : getY(dragToPosition, toSize.Height));

            html5_DragAndDrop(driver, dragFrom, dragTo, dragFromX, dragFromY, dragToX, dragToY);
        }

        //-------------
        // Cross-Window Drag And Drop Example
        //-------------
        public static void dragToWindow(IWebDriver dragFromDriver, IWebElement dragFromElement, IWebDriver dragToDriver)
        {
            // Drag start
            html5_simulateEvent(dragFromDriver, dragFromElement, "mousedown", Position.Center, null);
            Object dragData = html5_simulateEvent(dragFromDriver, dragFromElement, "dragstart", Position.Center, null);
            dragData = html5_simulateEvent(dragFromDriver, dragFromElement, "dragenter", Position.Center, dragData);
            dragData = html5_simulateEvent(dragFromDriver, dragFromElement, "dragleave", Position.Left, dragData);
            dragData = html5_simulateEvent(dragFromDriver, dragFromDriver.FindElement(By.TagName("body")), "dragleave", Position.Left, dragData);

            // Drag to other window
            html5_simulateEvent(dragToDriver, dragToDriver.FindElement(By.TagName("body")), "dragenter", Position.Right, null);
            IWebElement dropOverlay = dragToDriver.FindElement(By.ClassName("DropOverlay"));
            html5_simulateEvent(dragToDriver, dropOverlay, "dragenter", Position.Right, null);
            html5_simulateEvent(dragToDriver, dropOverlay, "dragover", Position.Center, null);
            dragData = html5_simulateEvent(dragToDriver, dropOverlay, "drop", Position.Center, dragData);
            html5_simulateEvent(dragFromDriver, dragFromElement, "dragend", Position.Center, dragData);
        }

    }
}

Wie zu sehen ist, sind auch einige andere sinnvolle Funktionalitäten wie Drag to Window implementiert.
Um den Code auszuführen, muss jetzt lediglich eine der öffentlichen Funktionen aufgerufen werden, um z.B. Drag and Drop zwischen zwei Objekten zu implementieren, wird die Funktion html5_DragAndDrop verwendet. Diese bekommt den zuständigen Webdriver, die beiden IWebElement Objekte und die Elementpositionen, mehr ist an der Stelle auch nicht mehr zu tun.

Ich habe den Code mit Firefox 34, IE11 und Chrome 40 getestet. Bitte beachtet, dass Firefox 35 ein Problem bei der JS Ausführung hat, siehe den dazugehörigen Issue: Issue 8390: Firefox 35: Passing arguments to executeScript isn’t working

1 Kommentar

Hinterlasse einen Kommentar

An der Diskussion beteiligen?
Hinterlasse uns deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert