diff --git a/lib/capybara/selenium/extensions/html5_drag.rb b/lib/capybara/selenium/extensions/html5_drag.rb new file mode 100644 index 00000000..de4ea529 --- /dev/null +++ b/lib/capybara/selenium/extensions/html5_drag.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class Capybara::Selenium::Node + module Html5Drag + private + + def html5_drag_to(element) + driver.execute_script MOUSEDOWN_TRACKER + scroll_if_needed { browser_action.click_and_hold(native).perform } + if driver.evaluate_script('window.capybara_mousedown_prevented') + element.scroll_if_needed { browser_action.move_to(element.native).release.perform } + else + driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element + end + end + + def draggable? + # Workaround https://github.com/SeleniumHQ/selenium/issues/6396 + driver.evaluate_script('arguments[0]["draggable"]', self) == true + end + + MOUSEDOWN_TRACKER = <<~JS + if (!window.hasOwnProperty('capybara_mousedown_prevented')){ + document.addEventListener('mousedown', function(ev){ + window.capybara_mousedown_prevented = ev.defaultPrevented; + }) + } + JS + + HTML5_DRAG_DROP_SCRIPT = <<~JS + var source = arguments[0]; + var target = arguments[1]; + + var dt = new DataTransfer(); + var opts = { cancelable: true, bubbles: true, dataTransfer: dt }; + + if (source.tagName == 'A'){ + dt.setData('text/uri-list', source.href); + dt.setData('text', source.href); + } + if (source.tagName == 'IMG'){ + dt.setData('text/uri-list', source.src); + dt.setData('text', source.src); + } + var dragEvent = new DragEvent('dragstart', opts); + source.dispatchEvent(dragEvent); + target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'}); + var dragOverEvent = new DragEvent('dragover', opts); + target.dispatchEvent(dragOverEvent); + var dragLeaveEvent = new DragEvent('dragleave', opts); + target.dispatchEvent(dragLeaveEvent); + if (dragOverEvent.defaultPrevented) { + var dropEvent = new DragEvent('drop', opts); + target.dispatchEvent(dropEvent); + } + var dragEndEvent = new DragEvent('dragend', opts); + source.dispatchEvent(dragEndEvent); + JS + end +end diff --git a/lib/capybara/selenium/nodes/chrome_node.rb b/lib/capybara/selenium/nodes/chrome_node.rb index b2d5f1db..518265c1 100644 --- a/lib/capybara/selenium/nodes/chrome_node.rb +++ b/lib/capybara/selenium/nodes/chrome_node.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true +require 'capybara/selenium/extensions/html5_drag' + class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node + include Html5Drag + def set_file(value) # rubocop:disable Naming/AccessorMethodName super(value) rescue ::Selenium::WebDriver::Error::ExpectedError => err @@ -11,10 +15,8 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node end def drag_to(element) - return super unless self[:draggable] == 'true' - - scroll_if_needed { browser_action.click_and_hold(native).perform } - driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element + return super unless draggable? + html5_drag_to(element) end private @@ -22,26 +24,4 @@ private def bridge driver.browser.send(:bridge) end - - HTML5_DRAG_DROP_SCRIPT = <<~JS - var source = arguments[0]; - var target = arguments[1]; - - var dt = new DataTransfer(); - var opts = { cancelable: true, bubbles: true, dataTransfer: dt }; - - var dragEvent = new DragEvent('dragstart', opts); - source.dispatchEvent(dragEvent); - target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'}); - var dragOverEvent = new DragEvent('dragover', opts); - target.dispatchEvent(dragOverEvent); - var dragLeaveEvent = new DragEvent('dragleave', opts); - target.dispatchEvent(dragLeaveEvent); - if (dragOverEvent.defaultPrevented) { - var dropEvent = new DragEvent('drop', opts); - target.dispatchEvent(dropEvent); - } - var dragEndEvent = new DragEvent('dragend', opts); - source.dispatchEvent(dragEndEvent); - JS end diff --git a/lib/capybara/selenium/nodes/marionette_node.rb b/lib/capybara/selenium/nodes/marionette_node.rb index 52afad0a..e9c1082c 100644 --- a/lib/capybara/selenium/nodes/marionette_node.rb +++ b/lib/capybara/selenium/nodes/marionette_node.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true +require 'capybara/selenium/extensions/html5_drag' class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node + include Html5Drag + def click(keys = [], **options) super rescue ::Selenium::WebDriver::Error::ElementNotInteractableError @@ -54,10 +57,8 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node end def drag_to(element) - return super unless (browser_version >= 62.0) && (self[:draggable] == 'true') - - scroll_if_needed { browser_action.click_and_hold(native).perform } - driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element + return super unless (browser_version >= 62.0) && draggable? + html5_drag_to(element) end private @@ -116,26 +117,4 @@ private def browser_version driver.browser.capabilities[:browser_version].to_f end - - HTML5_DRAG_DROP_SCRIPT = <<~JS - var source = arguments[0]; - var target = arguments[1]; - - var dt = new DataTransfer(); - var opts = { cancelable: true, bubbles: true, dataTransfer: dt }; - - var dragEvent = new DragEvent('dragstart', opts); - source.dispatchEvent(dragEvent); - target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'}); - var dragOverEvent = new DragEvent('dragover', opts); - target.dispatchEvent(dragOverEvent); - var dragLeaveEvent = new DragEvent('dragleave', opts); - target.dispatchEvent(dragLeaveEvent); - if (dragOverEvent.defaultPrevented) { - var dropEvent = new DragEvent('drop', opts); - target.dispatchEvent(dropEvent); - } - var dragEndEvent = new DragEvent('dragend', opts); - source.dispatchEvent(dragEndEvent); - JS end diff --git a/lib/capybara/spec/public/test.js b/lib/capybara/spec/public/test.js index 6ab4933f..f4cdc1e3 100644 --- a/lib/capybara/spec/public/test.js +++ b/lib/capybara/spec/public/test.js @@ -1,7 +1,7 @@ var activeRequests = 0; $(function() { $('#change').text('I changed it'); - $('#drag, #drag_scroll').draggable(); + $('#drag, #drag_scroll, #drag_link').draggable(); $('#drop, #drop_scroll').droppable({ drop: function(event, ui) { ui.draggable.remove(); diff --git a/lib/capybara/spec/session/node_spec.rb b/lib/capybara/spec/session/node_spec.rb index 813a88eb..9046b936 100644 --- a/lib/capybara/spec/session/node_spec.rb +++ b/lib/capybara/spec/session/node_spec.rb @@ -316,6 +316,14 @@ Capybara::SpecHelper.spec 'node' do element.drag_to(target) expect(@session).to have_xpath('//div[contains(., "Dropped!")]') end + + it 'should drag a link' do + @session.visit('/with_js') + link = @session.find_link('drag_link') + target = @session.find(:id, 'drop') + link.drag_to target + expect(@session).to have_xpath('//div[contains(., "Dropped!")]') + end end describe '#hover', requires: [:hover] do diff --git a/lib/capybara/spec/views/with_js.erb b/lib/capybara/spec/views/with_js.erb index 033e02dd..58ba8e31 100644 --- a/lib/capybara/spec/views/with_js.erb +++ b/lib/capybara/spec/views/with_js.erb @@ -12,6 +12,7 @@

FooBar

This is text

+ This link is non-HTML5 draggable

This is a draggable element.

@@ -27,6 +28,7 @@

This is an HTML5 draggable element.

+ This is an HTML5 draggable link

It should be dropped here.

diff --git a/spec/shared_selenium_session.rb b/spec/shared_selenium_session.rb index 60d7e561..83dbdbd2 100644 --- a/spec/shared_selenium_session.rb +++ b/spec/shared_selenium_session.rb @@ -313,6 +313,15 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode| element.drag_to(target) expect(session).to have_xpath('//div[contains(., "HTML5 Dropped drag_html5_scroll")]') end + + it 'should drag HTML5 default draggable elements' do + pending "Firefox < 62 doesn't support a DataTransfer constuctor" if marionette_lt?(62.0, session) + session.visit('/with_js') + link = session.find_link('drag_link_html5') + target = session.find(:id, 'drop_html5') + link.drag_to target + expect(session).to have_xpath('//div[contains(., "HTML5 Dropped")]') + end end context 'Windows' do