起因
有个小组在做一个页面构建的web站点,用来快速创建结构化的网站页面。该站点支持鼠标拖拽已定义好的各种页面插件,来拼凑成一个完整的页面。这些创建好的页面可以直接在公司的站点上使用,提高前端页面构建效率。
对此web站点,也是要做自动化进行验证,那么拖拽功能就是首先遇到的一个问题。
经过
思路1
selenium webdriver 中不就有拖拽命令,直接用不就行了。我们用的是webdriverio 来执行浏览器操作,首先就找到了这个链接 webdriverio dragAndDrop ,各种尝试都不行。
思路2
搜了一下internet看看网上是否有,发现这可能是一个selenium未解决的问题 selenium issue, chromedriver issue
网上也有很多人遇到同样的问题,最早从2015年开始一直到最近都陆续有人在selenium issue里面提问题,但是官方一直没有解决???这也是很神奇
思路3
网上搜索还是找到很多不同的解决方案,于是先找一些现成的代码试试看,阅尽千帆后发现没有一个能直接用上的,但是还是获取了一些信息,对后续的解决方案提供了帮助。同时也找到另一个有意思的端到端测试框架cypress.js,可以直接利用它来实现页面元素的拖拽测试,但是涉及到要换测试框架的问题,所以最终还是没有采用这个新框架,但该框架提供了一个用JS来解决拖拽问题的方向。
思路4
还是来分析一下测试目标的一些细节,可以从页面源码看到该站点的可拖拽元素是基于HTML drag and drop API 来做的,该元素增加了一个draggable 属性,就可以执行拖拽行为了。同时从站点源码看到其使用vuejs构建,vuejs的拖拽功能是通过vue.draggable.next来实现的,它是基于SortableJS这个开源包的支持来实现网页拖拽行为。
于是最终的想法是利用drag and drop API来模拟拖拽动作
结果
下面的代码参考了很多资料,真实有效的解决了vuejs上拖拽操作的模拟。代码中有注释做简单说明,希望对大家有帮助。
拖拽动作到底要触发哪些事件,其实是参考了cypress.js 的执行结果,发现它使用了以下事件来模拟拖拽:
pointerdown
mousedown
dragstart
dragover
mousemove
pointermove
drop
mouseup
-
pointerup
所以下面的代码总的来说就是使用JS创建以上的事件,然后按照顺序执行这一系列事件,从而达到模拟拖拽行为。
比较重要的一点,是要意识到:拖拽元素是通过drag event的dataTransfer来传输,所以需要在dragstart事件构造一个dataTransfer对象,然后把该dataTransfer对象传给后续的drag事件,以此绑定元素到拖拽动作中。var triggerDragAndDrop = function (source, target, offsetY = 0) { var DELAY_INTERVAL_MS = 100; var MAX_TRIES = 10; var dragStartEvent; function createNewDataTransfer() { var data = {}; return { clearData: function (key) { if (key === undefined) { data = {}; } else { delete data[key]; } }, getData: function (key) { return data[key]; }, setData: function (key, value) { data[key] = value; }, setDragImage: function () { }, dropEffect: 'move', files: [], items: [], types: ['text/plain'], effectAllowed: 'move' } }; var fireDragEvent = function (type, elem, clientX, clientY, dataTransfer) { let event = new DragEvent(type, { bubbles: true, cancelable: true, view: window, clientX: clientX, clientY: clientY, pageX: clientX, pageY: clientY, screenX: clientX, screenY: clientY, relatedTarget: elem }); event.dataTransfer = dataTransfer || createNewDataTransfer(); elem.dispatchEvent(event); return event; } var firePointerEvent = function (type, elem, clientX, clientY) { let event = new PointerEvent(type, { bubbles: true, cancelable: true, view: window, pageX: clientX, pageY: clientY, screenX: clientX, screenY: clientY, button: 0, which: 1 }); event.preventDefault() elem.dispatchEvent(event) } var firePointerMoveEvent = function (type, elem, clientX, clientY) { let event = new PointerEvent(type, { bubbles: true, cancelable: true, view: window, pageX: clientX, pageY: clientY, screenX: clientX, screenY: clientY, }); event.preventDefault() elem.dispatchEvent(event) } var fireMouseMoveEvent = function (type, elem, clientX, clientY) { var event = new MouseEvent(type, { bubbles: true, cancelable: true, view: window, pageX: clientX, pageY: clientY, screenX: clientX, screenY: clientY, }); event.preventDefault() elem.dispatchEvent(event) } var fireMouseEvent = function (type, elem, clientX, clientY) { var event = new MouseEvent(type, { bubbles: true, cancelable: true, button: 0, view: window, pageX: clientX, pageY: clientY, screenX: clientX, screenY: clientY, which: 1 }); event.preventDefault() elem.dispatchEvent(event) } // fetch target elements var elemDrag = document.querySelector(source) var elemDrop = document.querySelector(target) if (!elemDrag || !elemDrop) return false; // calculate positions var pos = elemDrag.getBoundingClientRect() var center1X = Math.floor((pos.left + (pos.width / 2))) var center1Y = Math.floor((pos.top + (pos.height / 2))) pos = elemDrop.getBoundingClientRect() var center2X = Math.floor((pos.left + (pos.width / 2))) var center2Y = pos.bottom var counter = 0; var startingDropRect = elemDrop.getBoundingClientRect(); function rectsEqual(r1, r2) { return r1.top === r2.top && r1.right === r2.right && r1.bottom === r2.bottom && r1.left === r2.left; } function dragover() { counter++; console.log('DRAGOVER #' + counter); var currentDropRect = elemDrop.getBoundingClientRect(); if (rectsEqual(startingDropRect, currentDropRect) && counter < MAX_TRIES) { if (counter != 1) console.log("drop target rect hasn't changed, trying again"); fireDragEvent("dragover", elemDrop, center2X, center2Y + offsetY, dragStartEvent.dataTransfer) fireMouseMoveEvent("mousemove", elemDrop, center2X, center2Y + offsetY) firePointerMoveEvent("pointermove", elemDrop, center2X, center2Y + offsetY) setTimeout(dragover, DELAY_INTERVAL_MS); } else { if (rectsEqual(startingDropRect, currentDropRect)) { console.log("wasn't able to budge drop target after " + MAX_TRIES + " tries, aborting"); fireDragEvent("drop", elemDrop, center2X, center2Y + offsetY, dragStartEvent.dataTransfer) fireMouseEvent("mouseup", elemDrop, center2X, center2Y + offsetY) firePointerEvent("pointerup", elemDrop, center2X, center2Y + offsetY) } else { setTimeout(drop, DELAY_INTERVAL_MS); } setTimeout(drop, DELAY_INTERVAL_MS); } } function drop() { console.log('DROP'); fireDragEvent("drop", elemDrop, center2X, center2Y + offsetY, dragStartEvent.dataTransfer) fireMouseEvent("mouseup", elemDrop, center2X, center2Y + offsetY) firePointerEvent("pointerup", elemDrop, center2X, center2Y + offsetY) } // start dragging process console.log('DRAGSTART'); firePointerEvent("pointerdown", elemDrag, center1X, center1Y); fireMouseEvent("mousedown", elemDrag, center1X, center1Y) dragStartEvent = fireDragEvent("dragstart", elemDrag, center1X, center1Y) setTimeout(dragover, DELAY_INTERVAL_MS); return true }