解决Selenium元素拖拽不生效Bug

转载请注明出处❤️

作者:测试蔡坨坨

原文链接:caituotuo.top/e8aa6c6f.html


你好,我是测试蔡坨坨。

前几天在使用Selenium进行元素拖拽操作时,发现Selenium自带的元素拖拽方法(dragAndDrop())不生效,网上的回答也是五花八门,比较混乱,尝试了以下几种方法均无法解决

方案1:通过dragAndDrop()方法将元素拖放到特定区域上——无效

1
2
3
4
5
// 要拖拽的元素
WebElement draggable = driver.findElement(By.xpath(""));
// 目标元素/区域
WebElement droppable = driver.findElement(By.xpath(""));
new Actions(driver).dragAndDrop(draggable, droppable).build().perform();

方案2:通过dragAndDropBy()方法将元素进行指定像素位移,从而实现拖放到特定区域,该方法需要先找到元素的像素——无效

1
new Actions(driver).dragAndDropBy(draggable,135, 40).build().perform();

方案3:先通过clickAndHold()方法点击并按住元素,然后使用moveByOffset()方法将元素拖拽到目标区域,再使用release()方法将按住的元素释放——无效

1
new Actions(driver).clickAndHold(draggable).moveByOffset(400, 0).release().build().perform();

方案4:先通过clickAndHold()方法点击并按住元素,然后使用moveToElement()方法将元素拖拽到指定元素上,再使用release()方法将元素释放——无效

1
new Actions(driver).clickAndHold(draggable).moveToElement(droppable).release(droppable).build().perform();

方案5:借助Robot类实现拖拽——无效

1
2
3
4
5
6
7
Point coordinates1 = draggable.getLocation();
Point coordinates2 = droppable.getLocation();
Robot robot = new Robot();
robot.mouseMove(coordinates1.getX(), coordinates1.getY());
robot.mousePress(InputEvent.BUTTON1_MASK);
robot.mouseMove(coordinates2.getX(), coordinates2.getY());
robot.mouseRelease(InputEvent.BUTTON1_MASK);

……

以上方案均未生效,具体表现为运行均无任何报错,但在应用程序中未发生拖放。

经过一顿操作,最终在「Selenium Drag and Drop Bug Workaround」上找到了问题原因及解决方案。

经了解,Selenium的拖放功能在某些情况下无效的错误已经存在多年。

原因是拖放功能包含三个动作:单击并按住(click and hold)、将鼠标移动到其他元素或位置(move mouse to other element/location)、释放鼠标(release mouse),问题在于最后一步释放鼠标的操作,当Webdriver API发送释放鼠标的请求时,在某些情况下它会一直按住它,所以导致拖放功能无效。

解决方法就是通过Webdriver API将JavaScript代码发送到浏览器,利用JavaScript模拟拖放操作,而不使用Webdriver自带的拖放方法。

其工作原理是将浏览器实例和CSS选择器找到的两个Web元素作为参数,然后在浏览器端执行JavaScript代码。

如果你是使用Python+Selenium技术栈实现的Web UI自动化,可以直接下载seletools(Selenium Tools,作者:Dmitrii Bormotov)包,并将它导入到需要执行拖放的地方,然后简单地调用它的drag_and_drop()方法即可。

1
pip install seletools
1
2
3
4
5
from seletools.actions import drag_and_drop

source = driver.find_element(By.CSS_SELECTOR, "#column-a")
target = browser.find_element(By.CSS_SELECTOR, "#column-b")
drag_and_drop(driver, source, target)

如果使用的是Java+Selenium技术栈,则可以使用以下代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
// 要拖拽的元素
WebElement draggable = driver.findElement(By.xpath(""));
// 目标元素
WebElement droppable = driver.findElement(By.xpath(""));

// 拖动前先点击并按住要拖拽的元素,避免在elementui,拖放前draggable属性才会变成true,目的是让draggable变成true,如果一开始就是true也可不加这句
new Actions(driver).clickAndHold(draggable).perform();

final String java_script = "var args = arguments," + "callback = args[args.length - 1]," + "source = args[0]," + "target = args[1]," + "offsetX = (args.length > 2 && args[2]) || 0," + "offsetY = (args.length > 3 && args[3]) || 0," + "delay = (args.length > 4 && args[4]) || 1;" + "if (!source.draggable) throw new Error('Source element is not draggable.');" + "var doc = source.ownerDocument," + "win = doc.defaultView," + "rect1 = source.getBoundingClientRect()," + "rect2 = target ? target.getBoundingClientRect() : rect1," + "x = rect1.left + (rect1.width >> 1)," + "y = rect1.top + (rect1.height >> 1)," + "x2 = rect2.left + (rect2.width >> 1) + offsetX," + "y2 = rect2.top + (rect2.height >> 1) + offsetY," + "dataTransfer = Object.create(Object.prototype, {" + " _items: { value: { } }," + " effectAllowed: { value: 'all', writable: true }," + " dropEffect: { value: 'move', writable: true }," + " files: { get: function () { return undefined } }," + " types: { get: function () { return Object.keys(this._items) } }," + " setData: { value: function (format, data) { this._items[format] = data } }," + " getData: { value: function (format) { return this._items[format] } }," + " clearData: { value: function (format) { delete this._items[format] } }," + " setDragImage: { value: function () { } }" + "});" + "target = doc.elementFromPoint(x2, y2);" + "if(!target) throw new Error('The target element is not interactable and need to be scrolled into the view.');" + "rect2 = target.getBoundingClientRect();" + "emit(source, 'dragstart', delay, function () {" + "var rect3 = target.getBoundingClientRect();" + "x = rect3.left + x2 - rect2.left;" + "y = rect3.top + y2 - rect2.top;" + "emit(target, 'dragenter', 1, function () {" + " emit(target, 'dragover', delay, function () {" + "\ttarget = doc.elementFromPoint(x, y);" + "\temit(target, 'drop', 1, function () {" + "\t emit(source, 'dragend', 1, callback);" + "});});});});" + "function emit(element, type, delay, callback) {" + "var event = doc.createEvent('DragEvent');" + "event.initMouseEvent(type, true, true, win, 0, 0, 0, x, y, false, false, false, false, 0, null);" + "Object.defineProperty(event, 'dataTransfer', { get: function () { return dataTransfer } });" + "element.dispatchEvent(event);" + "win.setTimeout(callback, delay);" + "}";

// 默认拖拽到中心点位置,第3个参数是X坐标偏移量(左负右正),第4个参数为Y坐标偏移量(上负下正),第5个参数是延迟时间(单位为毫秒,表示当鼠标点下后,延迟指定时间后才开始激活拖拽动作,用来防止误点击)
((JavascriptExecutor) driver).executeScript(java_script, draggable, droppable, -200, -300, 500);

以上就是在Python和Java中的解决方案,至于为什么不在Selenium中直接修改程序,而是创建单独的包来处理,以下是Dmitrii Bormotov的说法:

The drag and drop bug is a webdriver issue, so all you can do on the Selenium side is to simply perform the same workaround that I did. I spoke with David Burnes (core Selenium committer) about pushing that workaround into Selenium, but he said that it is not a good idea to have any workarounds in Selenium itself. That is why I had to create a separate package to help the test automation community with this problem.

大概的意思就是拖放错误是一个webdriver网络驱动问题,David Burnes(核心 Selenium 提交者)认为在Selenium中提供任何暂时避开网络的方法并不是一个好主意。