解決Selenium元素拖拽不生效Bug

測試蔡坨坨發表於2023-03-12

轉載請註明出處❤️

作者:測試蔡坨坨

原文連結:caituotuo.top/e8aa6c6f.html


你好,我是測試蔡坨坨。

前幾天在使用Selenium進行元素拖拽操作時,發現Selenium自帶的元素拖拽方法(dragAndDrop())不生效,網上的回答也是五花八門,比較混亂,嘗試了以下幾種方法均無法解決

方案1:透過dragAndDrop()方法將元素拖放到特定區域上——無效

// 要拖拽的元素
WebElement draggable = driver.findElement(By.xpath(""));
// 目標元素/區域
WebElement droppable = driver.findElement(By.xpath(""));
new Actions(driver).dragAndDrop(draggable, droppable).build().perform();

方案2:透過dragAndDropBy()方法將元素進行指定畫素位移,從而實現拖放到特定區域,該方法需要先找到元素的畫素——無效

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

方案3:先透過clickAndHold()方法點選並按住元素,然後使用moveByOffset()方法將元素拖拽到目標區域,再使用release()方法將按住的元素釋放——無效

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

方案4:先透過clickAndHold()方法點選並按住元素,然後使用moveToElement()方法將元素拖拽到指定元素上,再使用release()方法將元素釋放——無效

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

方案5:藉助Robot類實現拖拽——無效

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()方法即可。

pip install seletools
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技術棧,則可以使用以下程式碼實現:

// 要拖拽的元素
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中提供任何暫時避開網路的方法並不是一個好主意。

相關文章