diff --git a/lib/capybara/selenium/nodes/chrome_node.rb b/lib/capybara/selenium/nodes/chrome_node.rb index 6eb4ed56..11694a94 100644 --- a/lib/capybara/selenium/nodes/chrome_node.rb +++ b/lib/capybara/selenium/nodes/chrome_node.rb @@ -10,9 +10,38 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node raise end + def drag_to(element) + return super unless self[:draggable] == 'true' + + scroll_if_needed { driver.browser.action.click_and_hold(native).perform } + driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element + end + 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 8f821bb6..1526d711 100644 --- a/lib/capybara/selenium/nodes/marionette_node.rb +++ b/lib/capybara/selenium/nodes/marionette_node.rb @@ -14,7 +14,7 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node def disabled? # Not sure exactly what version of FF fixed the below issue, but it is definitely fixed in 61+ - return super unless driver.browser.capabilities[:browser_version].to_f < 61.0 + return super unless browser_version < 61.0 return true if super # workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements @@ -27,7 +27,7 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node def set_file(value) # rubocop:disable Naming/AccessorMethodName native.clear # By default files are appended so we have to clear here - return super if driver.browser.capabilities[:browser_version].to_f >= 62.0 + return super if browser_version >= 62.0 # Workaround lack of support for multiple upload by uploading one at a time path_names = value.to_s.empty? ? [] : value @@ -54,6 +54,13 @@ class Capybara::Selenium::MarionetteNode < Capybara::Selenium::Node actions.perform end + def drag_to(element) + return super unless (browser_version >= 62.0) && (self[:draggable] == 'true') + + scroll_if_needed { driver.browser.action.click_and_hold(native).perform } + driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element + end + private def _send_keys(keys, actions, down_keys = nil) @@ -99,4 +106,30 @@ private result = bridge.http.call(:post, "session/#{bridge.session_id}/file", file: Selenium::WebDriver::Zipper.zip_file(local_file)) result['value'] end + + 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 7038bf5a..6ab4933f 100644 --- a/lib/capybara/spec/public/test.js +++ b/lib/capybara/spec/public/test.js @@ -8,6 +8,16 @@ $(function() { $(this).html('Dropped!'); } }); + $('#drag_html5, #drag_html5_scroll').on('dragstart', function(ev){ + ev.originalEvent.dataTransfer.setData("text", ev.target.id); + }); + $('#drop_html5, #drop_html5_scroll').on('dragover', function(ev){ + if ($(this).hasClass('drop')) { ev.preventDefault(); } + }); + $('#drop_html5, #drop_html5_scroll').on('drop', function(ev){ + ev.preventDefault(); + $(this).html('HTML5 Dropped ' + ev.originalEvent.dataTransfer.getData("text")); + }); $('#clickable').click(function(e) { var link = $(this); setTimeout(function() { diff --git a/lib/capybara/spec/session/node_spec.rb b/lib/capybara/spec/session/node_spec.rb index b830e00a..59d73e0f 100644 --- a/lib/capybara/spec/session/node_spec.rb +++ b/lib/capybara/spec/session/node_spec.rb @@ -306,7 +306,7 @@ Capybara::SpecHelper.spec 'node' do element = @session.find('//div[@id="drag"]') target = @session.find('//div[@id="drop"]') element.drag_to(target) - expect(@session.find('//div[contains(., "Dropped!")]')).not_to be_nil + expect(@session).to have_xpath('//div[contains(., "Dropped!")]') end it 'should drag and drop if scrolling is needed' do @@ -314,7 +314,7 @@ Capybara::SpecHelper.spec 'node' do element = @session.find('//div[@id="drag_scroll"]') target = @session.find('//div[@id="drop_scroll"]') element.drag_to(target) - expect(@session.find('//div[contains(., "Dropped!")]')).not_to be_nil + expect(@session).to have_xpath('//div[contains(., "Dropped!")]') end end diff --git a/lib/capybara/spec/views/with_js.erb b/lib/capybara/spec/views/with_js.erb index 7ce86cfc..2ce78a7f 100644 --- a/lib/capybara/spec/views/with_js.erb +++ b/lib/capybara/spec/views/with_js.erb @@ -19,6 +19,16 @@

It should be dropped here.

+
+

It should be dropped here.

+
+
+

This is an HTML5 draggable element.

+
+
+

It should be dropped here.

+
+

Click me

Slowly

@@ -135,6 +145,9 @@

It should be dropped here.

+
+

This is an HTML5 draggable element.

+