From 94f7f8881a4a50f7fbb24ee0a9eab2e5df218d3c Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Wed, 15 May 2019 14:25:50 -0700 Subject: [PATCH] Simulate file/external data drop in chrome and firefox and move HTML5 drag/drop tests into shared tests --- History.md | 7 ++ lib/capybara/driver/node.rb | 4 + lib/capybara/node/element.rb | 24 ++++++ .../selenium/extensions/html5_drag.rb | 63 +++++++++++++++ lib/capybara/selenium/node.rb | 4 + lib/capybara/selenium/nodes/chrome_node.rb | 4 + lib/capybara/selenium/nodes/firefox_node.rb | 4 + lib/capybara/spec/public/test.js | 28 ++++++- lib/capybara/spec/session/attach_file_spec.rb | 6 -- lib/capybara/spec/session/node_spec.rb | 81 +++++++++++++++++++ lib/capybara/spec/spec_helper.rb | 4 + spec/selenium_spec_firefox.rb | 2 + spec/selenium_spec_ie.rb | 2 + spec/selenium_spec_safari.rb | 2 + spec/shared_selenium_session.rb | 51 ------------ 15 files changed, 228 insertions(+), 58 deletions(-) diff --git a/History.md b/History.md index f3843dab..93dbe1c3 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,10 @@ +# Version 3.21.0 +Release date: unreleased + +### Added + +* Chrome and Firefox via the selenium driver support dropping files/data on elements + # Version 3.20.1 Release date: 2019-05-17 diff --git a/lib/capybara/driver/node.rb b/lib/capybara/driver/node.rb index fd98cb5d..2edd2baa 100644 --- a/lib/capybara/driver/node.rb +++ b/lib/capybara/driver/node.rb @@ -69,6 +69,10 @@ module Capybara raise NotImplementedError end + def drop(*args) + raise NotImplementedError + end + def scroll_by(x, y) raise NotImplementedError end diff --git a/lib/capybara/node/element.rb b/lib/capybara/node/element.rb index 2fb3ca1d..f32389e8 100644 --- a/lib/capybara/node/element.rb +++ b/lib/capybara/node/element.rb @@ -392,6 +392,30 @@ module Capybara self end + ## + # + # Drop items on the current element. + # + # target = page.find('#foo') + # target.drop('/some/path/file.csv') + # + # @overload drop(path, ...) + # @param [String, #to_path] path Location of the file to drop on the element + # + # @overload drop(strings, ...) + # @param [Hash] strings A hash of type to data to be dropped - { "text/url" => "https://www.google.com" } + # + # @return [Capybara::Node::Element] The element + def drop(*args) + options = args.map do |arg| + return arg.to_path if arg.respond_to?(:to_path) + + arg + end + synchronize { base.drop(*options) } + self + end + ## # # Scroll the page or element diff --git a/lib/capybara/selenium/extensions/html5_drag.rb b/lib/capybara/selenium/extensions/html5_drag.rb index d0a0adef..f5207e73 100644 --- a/lib/capybara/selenium/extensions/html5_drag.rb +++ b/lib/capybara/selenium/extensions/html5_drag.rb @@ -22,6 +22,69 @@ class Capybara::Selenium::Node native.property('draggable') end + def html5_drop(*args) + if args[0].is_a? String + input = driver.evaluate_script ATTACH_FILE + input.set_file(args) + driver.execute_script DROP_FILE, self, input + else + items = args.each_with_object([]) do |arg, arr| + arg.each_with_object(arr) do |(type, data), arr_| + arr_ << { type: type, data: data } + end + end + driver.execute_script DROP_STRING, items, self + end + end + + DROP_STRING = <<~JS + var strings = arguments[0], + el = arguments[1], + dt = new DataTransfer(), + opts = { cancelable: true, bubbles: true, dataTransfer: dt }; + for (var i=0; i < strings.length; i++){ + if (dt.items) { + dt.items.add(strings[i]['data'], strings[i]['type']); + } else { + dt.setData(strings[i]['type'], strings[i]['data']); + } + } + var dropEvent = new DragEvent('drop', opts); + el.dispatchEvent(dropEvent); + JS + + DROP_FILE = <<~JS + var el = arguments[0], + input = arguments[1], + files = input.files, + dt = new DataTransfer(), + opts = { cancelable: true, bubbles: true, dataTransfer: dt }; + input.parentElement.removeChild(input); + if (dt.items){ + for (var i=0; i { window.capybara_mousedown_prevented = ev.defaultPrevented; diff --git a/lib/capybara/selenium/node.rb b/lib/capybara/selenium/node.rb index a832b9bd..1874f103 100644 --- a/lib/capybara/selenium/node.rb +++ b/lib/capybara/selenium/node.rb @@ -137,6 +137,10 @@ class Capybara::Selenium::Node < Capybara::Driver::Node element.scroll_if_needed { browser_action.move_to(element.native).release.perform } end + def drop(*_) + raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser' + end + def tag_name @tag_name ||= native.tag_name.downcase end diff --git a/lib/capybara/selenium/nodes/chrome_node.rb b/lib/capybara/selenium/nodes/chrome_node.rb index c0c865d5..7ae18e08 100644 --- a/lib/capybara/selenium/nodes/chrome_node.rb +++ b/lib/capybara/selenium/nodes/chrome_node.rb @@ -34,6 +34,10 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node html5_drag_to(element) end + def drop(*args) + html5_drop(*args) + end + def click(*) super rescue ::Selenium::WebDriver::Error::WebDriverError => e diff --git a/lib/capybara/selenium/nodes/firefox_node.rb b/lib/capybara/selenium/nodes/firefox_node.rb index cfd11bbb..145118c9 100644 --- a/lib/capybara/selenium/nodes/firefox_node.rb +++ b/lib/capybara/selenium/nodes/firefox_node.rb @@ -52,6 +52,10 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node html5_drag_to(element) end + def drop(*args) + html5_drop(*args) + end + def hover return super unless browser_version >= 65.0 diff --git a/lib/capybara/spec/public/test.js b/lib/capybara/spec/public/test.js index bf78bd93..8662bd19 100644 --- a/lib/capybara/spec/public/test.js +++ b/lib/capybara/spec/public/test.js @@ -18,7 +18,33 @@ $(function() { }); $('#drop_html5, #drop_html5_scroll').on('drop', function(ev){ ev.preventDefault(); - $(this).html('HTML5 Dropped ' + ev.originalEvent.dataTransfer.getData("text")); + var oev = ev.originalEvent; + if (oev.dataTransfer.items) { + for (var i = 0; i < oev.dataTransfer.items.length; i++){ + var item = oev.dataTransfer.items[i]; + if (item.kind === 'file'){ + var file = item.getAsFile(); + $(this).append('HTML5 Dropped file: ' + file.name); + } else { + var _this = this; + var callback = (function(type){ + return function(s){ + $(_this).append('HTML5 Dropped string: ' + type + ' ' + s) + } + })(item.type); + item.getAsString(callback); + } + } + } else { + $(this).html('HTML5 Dropped ' + oev.dataTransfer.getData("text")); + for (var i = 0; i < oev.dataTransfer.files.length; i++) { + $(this).append('HTML5 Dropped file: ' + oev.dataTransfer.files[i].name); + } + for (var i = 0; i < oev.dataTransfer.types.length; i++) { + var type = oev.dataTransfer.types[i]; + $(this).append('HTML5 Dropped string: ' + type + ' ' + oev.dataTransfer.getData(type)); + } + } }); $('#clickable').click(function(e) { var link = $(this); diff --git a/lib/capybara/spec/session/attach_file_spec.rb b/lib/capybara/spec/session/attach_file_spec.rb index 9fbe6d07..fd3dbce4 100644 --- a/lib/capybara/spec/session/attach_file_spec.rb +++ b/lib/capybara/spec/session/attach_file_spec.rb @@ -199,10 +199,4 @@ Capybara::SpecHelper.spec '#attach_file' do expect(extract_results(@session)['hidden_image']).to end_with(File.basename(__FILE__)) end end - -private - - def with_os_path_separators(path) - Gem.win_platform? ? path.to_s.tr('/', '\\') : path.to_s - end end diff --git a/lib/capybara/spec/session/node_spec.rb b/lib/capybara/spec/session/node_spec.rb index 024a2262..10a06408 100644 --- a/lib/capybara/spec/session/node_spec.rb +++ b/lib/capybara/spec/session/node_spec.rb @@ -414,6 +414,87 @@ Capybara::SpecHelper.spec 'node' do link.drag_to target expect(@session).to have_xpath('//div[contains(., "Dropped!")]') end + + context 'HTML5', requires: %i[js html5_drag] do + it 'should HTML5 drag and drop an object' do + @session.visit('/with_js') + element = @session.find('//div[@id="drag_html5"]') + target = @session.find('//div[@id="drop_html5"]') + element.drag_to(target) + expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5")]') + end + + it 'should set clientX/Y in dragover events' do + @session.visit('/with_js') + element = @session.find('//div[@id="drag_html5"]') + target = @session.find('//div[@id="drop_html5"]') + element.drag_to(target) + expect(@session).to have_css('div.log', text: /DragOver with client position: [1-9]\d*,[1-9]\d*/, count: 2) + end + + it 'should not HTML5 drag and drop on a non HTML5 drop element' do + @session.visit('/with_js') + element = @session.find('//div[@id="drag_html5"]') + target = @session.find('//div[@id="drop_html5"]') + target.execute_script("$(this).removeClass('drop');") + element.drag_to(target) + sleep 1 + expect(@session).not_to have_xpath('//div[contains(., "HTML5 Dropped")]') + end + + it 'should HTML5 drag and drop when scrolling needed' do + @session.visit('/with_js') + element = @session.find('//div[@id="drag_html5_scroll"]') + target = @session.find('//div[@id="drop_html5_scroll"]') + element.drag_to(target) + expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5_scroll")]') + end + + it 'should drag HTML5 default draggable elements' do + @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 + end + + describe 'Element#drop', requires: %i[js html5_drag] do + it 'can drop a file' do + @session.visit('/with_js') + target = @session.find('//div[@id="drop_html5"]') + target.drop( + with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__))) + ) + expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]') + end + + it 'can drop multiple files' do + @session.visit('/with_js') + target = @session.find('//div[@id="drop_html5"]') + target.drop( + with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__))), + with_os_path_separators(File.expand_path('../fixtures/test_file.txt', File.dirname(__FILE__))) + ) + expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]') + expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: test_file.txt")]') + end + + it 'can drop strings' do + @session.visit('/with_js') + target = @session.find('//div[@id="drop_html5"]') + target.drop('text/plain' => 'Some dropped text') + expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain Some dropped text")]') + end + + it 'can drop multiple strings' do + @session.visit('/with_js') + target = @session.find('//div[@id="drop_html5"]') + target.drop('text/plain' => 'Some dropped text', 'text/url' => 'http://www.google.com') + expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain Some dropped text")]') + expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/url http://www.google.com")]') + end end describe '#hover', requires: [:hover] do diff --git a/lib/capybara/spec/spec_helper.rb b/lib/capybara/spec/spec_helper.rb index 479143de..61adf0be 100644 --- a/lib/capybara/spec/spec_helper.rb +++ b/lib/capybara/spec/spec_helper.rb @@ -124,6 +124,10 @@ module Capybara def be_an_invalid_element_error(session) satisfy { |error| session.driver.invalid_element_errors.any? { |e| error.is_a? e } } end + + def with_os_path_separators(path) + Gem.win_platform? ? path.to_s.tr('/', '\\') : path.to_s + end end end diff --git a/spec/selenium_spec_firefox.rb b/spec/selenium_spec_firefox.rb index 174905cd..68b7d703 100644 --- a/spec/selenium_spec_firefox.rb +++ b/spec/selenium_spec_firefox.rb @@ -64,6 +64,8 @@ Capybara::SpecHelper.run_specs TestSessions::SeleniumFirefox, 'selenium', capyba pending "Geckodriver doesn't provide a way to remove cookies outside the current domain" when 'Capybara::Session selenium #attach_file with a block can upload by clicking the file input' pending "Geckodriver doesn't allow clicking on file inputs" + when /drag_to.*HTML5/ + pending "Firefox < 62 doesn't support a DataTransfer constuctor" if firefox_lt?(62.0, @session) end end diff --git a/spec/selenium_spec_ie.rb b/spec/selenium_spec_ie.rb index 4ba82de9..6ded6db5 100644 --- a/spec/selenium_spec_ie.rb +++ b/spec/selenium_spec_ie.rb @@ -110,6 +110,8 @@ Capybara::SpecHelper.run_specs TestSessions::SeleniumIE, 'selenium', capybara_sk pending 'IE treats blank href as a parent request (against HTML spec)' when /#attach_file with a block/ skip 'Hangs IE testing for unknown reason' + when /drag_to.*HTML5/ + pending "IE doesn't support a DataTransfer constuctor" end end diff --git a/spec/selenium_spec_safari.rb b/spec/selenium_spec_safari.rb index c797299f..1273a04b 100644 --- a/spec/selenium_spec_safari.rb +++ b/spec/selenium_spec_safari.rb @@ -73,6 +73,8 @@ Capybara::SpecHelper.run_specs TestSessions::Safari, SAFARI_DRIVER.to_s, capybar when 'Capybara::Session selenium_safari #go_back should fetch a response from the driver from the previous page', 'Capybara::Session selenium_safari #go_forward should fetch a response from the driver from the previous page' skip 'safaridriver loses the ability to find elements in the document after `go_back`' + when /drag_to.*HTML5/ + pending "Safari doesn't support" end end diff --git a/spec/shared_selenium_session.rb b/spec/shared_selenium_session.rb index 87e4cf4d..d2a7ed0a 100644 --- a/spec/shared_selenium_session.rb +++ b/spec/shared_selenium_session.rb @@ -303,57 +303,6 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode| end end - describe 'Element#drag_to' do - before do - skip "Firefox < 62 doesn't support a DataTransfer constuctor" if firefox_lt?(62.0, session) - skip "IE doesn't support a DataTransfer constuctor" if ie?(session) - skip "Safari doesn't support" if safari?(session) - end - - it 'should HTML5 drag and drop an object' do - session.visit('/with_js') - element = session.find('//div[@id="drag_html5"]') - target = session.find('//div[@id="drop_html5"]') - element.drag_to(target) - expect(session).to have_xpath('//div[contains(., "HTML5 Dropped drag_html5")]') - end - - it 'should set clientX/Y in dragover events' do - session.visit('/with_js') - element = session.find('//div[@id="drag_html5"]') - target = session.find('//div[@id="drop_html5"]') - element.drag_to(target) - session.all(:css, 'div.log').each { |el| puts el.text } - expect(session).to have_css('div.log', text: /DragOver with client position: [1-9]\d*,[1-9]\d*/, count: 2) - end - - it 'should not HTML5 drag and drop on a non HTML5 drop element' do - session.visit('/with_js') - element = session.find('//div[@id="drag_html5"]') - target = session.find('//div[@id="drop_html5"]') - target.execute_script("$(this).removeClass('drop');") - element.drag_to(target) - sleep 1 - expect(session).not_to have_xpath('//div[contains(., "HTML5 Dropped drag_html5")]') - end - - it 'should HTML5 drag and drop when scrolling needed' do - session.visit('/with_js') - element = session.find('//div[@id="drag_html5_scroll"]') - target = session.find('//div[@id="drop_html5_scroll"]') - 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 - 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 - describe 'Capybara#Node#attach_file' do it 'can attach a directory' do pending "Geckodriver doesn't support uploading a directory" if firefox?(session)