From 1a899c2a73c33f456e5ccbd0cebad6bbf91ba0bf Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Fri, 29 Dec 2017 12:37:08 -0800 Subject: [PATCH] Support keys and x,y offset with mouse clicks --- lib/capybara/driver/node.rb | 6 +-- lib/capybara/node/element.rb | 28 +++++++--- lib/capybara/selenium/node.rb | 72 +++++++++++++++++++++++--- lib/capybara/spec/public/test.js | 37 +++++++++++-- lib/capybara/spec/session/node_spec.rb | 64 ++++++++++++++++++++++- 5 files changed, 186 insertions(+), 21 deletions(-) diff --git a/lib/capybara/driver/node.rb b/lib/capybara/driver/node.rb index 6879e09b..530e376f 100644 --- a/lib/capybara/driver/node.rb +++ b/lib/capybara/driver/node.rb @@ -39,15 +39,15 @@ module Capybara raise NotImplementedError end - def click + def click(*options) raise NotImplementedError end - def right_click + def right_click(*options) raise NotImplementedError end - def double_click + def double_click(*options) raise NotImplementedError end diff --git a/lib/capybara/node/element.rb b/lib/capybara/node/element.rb index 22537d82..8fe90ba7 100644 --- a/lib/capybara/node/element.rb +++ b/lib/capybara/node/element.rb @@ -138,9 +138,14 @@ module Capybara # # Click the Element # + # @!macro click_modifiers + # @overload $0(*key_modifiers=[], offset={x: nil, y: nil}) + # @param [Array<:alt, :control, :meta, :shift>] *key_modifiers Keys to be held down when clicking + # @param [Hash] offset x and y coordinates to offset the click location from the top left corner of the element. If not specified will click the middle of the element. # @return [Capybara::Node::Element] The element - def click - synchronize { base.click } + def click(*options) + verify_click_options_support(__method__) if !options.empty? + synchronize { base.click(*options) } return self end @@ -148,9 +153,11 @@ module Capybara # # Right Click the Element # + # @macro click_modifiers # @return [Capybara::Node::Element] The element - def right_click - synchronize { base.right_click } + def right_click(*options) + verify_click_options_support(__method__) if !options.empty? + synchronize { base.right_click(*options) } return self end @@ -158,9 +165,11 @@ module Capybara # # Double Click the Element # + # @macro click_modifiers # @return [Capybara::Node::Element] The element - def double_click - synchronize { base.double_click } + def double_click(*options) + verify_click_options_support(__method__) if !options.empty? + synchronize { base.double_click(*options) } return self end @@ -381,6 +390,13 @@ module Capybara raise end end + private + + def verify_click_options_support(method) + if base.method(method).arity == 0 + raise ArgumentError, "The current driver does not support #{method} options" + end + end end end end diff --git a/lib/capybara/selenium/node.rb b/lib/capybara/selenium/node.rb index 7e0d053b..1079c37d 100644 --- a/lib/capybara/selenium/node.rb +++ b/lib/capybara/selenium/node.rb @@ -74,8 +74,20 @@ class Capybara::Selenium::Node < Capybara::Driver::Node native.click if selected? end - def click - native.click + def click(*keys, **options) + if keys.empty? && options.empty? && !(options[:x] && options[:y]) + native.click + else + scroll_if_needed do + action_with_modifiers(*keys, **options) do |a| + if options[:x] && options[:y] + a.click + else + a.click(native) + end + end + end + end rescue => e if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) || e.message =~ /Other element would receive the click/ @@ -87,15 +99,27 @@ class Capybara::Selenium::Node < Capybara::Driver::Node raise e end - def right_click + def right_click(*keys, **options) scroll_if_needed do - driver.browser.action.context_click(native).perform + action_with_modifiers(*keys, **options) do |a| + if options[:x] && options[:y] + a.context_click + else + a.context_click(native) + end + end end end - def double_click + def double_click(*keys, **options) scroll_if_needed do - driver.browser.action.double_click(native).perform + action_with_modifiers(*keys, **options) do |a| + if options[:x] && options[:y] + a.double_click + else + a.double_click(native) + end + end end end @@ -267,4 +291,40 @@ private native.send_keys(value.to_s) end end + + def action_with_modifiers(*keys, x: nil, y: nil) + actions = driver.browser.action + actions.move_to(native, x, y) + modifiers_down(actions, keys) + yield actions + modifiers_up(actions, keys) + actions.perform + ensure + a = driver.browser.action + a.release_actions if a.respond_to?(:release_actions) + end + + def modifiers_down(actions, keys) + keys.each do |key| + key = case key + when :ctrl then :control + when :command, :cmd then :meta + else + key + end + actions.key_down(key) + end + end + + def modifiers_up(actions, keys) + keys.each do |key| + key = case key + when :ctrl then :control + when :command, :cmd then :meta + else + key + end + actions.key_up(key) + end + end end diff --git a/lib/capybara/spec/public/test.js b/lib/capybara/spec/public/test.js index 39fbd8b7..77629a5c 100644 --- a/lib/capybara/spec/public/test.js +++ b/lib/capybara/spec/public/test.js @@ -8,7 +8,8 @@ $(function() { $(this).html('Dropped!'); } }); - $('#clickable').click(function() { + $('#clickable').click(function(e) { + debugger; var link = $(this); setTimeout(function() { $(link).after('Has been clicked'); @@ -64,12 +65,40 @@ $(function() { }, 400) }); $('#click-test').on({ - dblclick: function() { - $(this).after('Has been double clicked'); + click: function(e) { + var desc = ""; + if (e.altKey|| e.ctrlKey || e.metaKey || e.shiftKey) { + if (e.altKey) desc += 'alt '; + if (e.ctrlKey) desc += 'control '; + if (e.metaKey) desc += 'meta '; + if (e.shiftKey) desc += 'shift '; + } + var pos = this.getBoundingClientRect(); + $(this).after('Has been ' + desc + 'clicked at ' + (e.clientX - pos.left) + ',' + (e.clientY - pos.top) + ''); + }, + dblclick: function(e) { + var desc = ""; + if (e.altKey|| e.ctrlKey || e.metaKey || e.shiftKey) { + if (e.altKey) desc += 'alt '; + if (e.ctrlKey) desc += 'control '; + if (e.metaKey) desc += 'meta '; + if (e.shiftKey) desc += 'shift '; + $(this).after('Has been ' + desc + 'double clicked'); + } + var pos = this.getBoundingClientRect(); + $(this).after('Has been ' + desc + 'double clicked at ' + (e.clientX - pos.left) + ',' + (e.clientY - pos.top) + ''); }, contextmenu: function(e) { e.preventDefault(); - $(this).after('Has been right clicked'); + var desc = ""; + if (e.altKey|| e.ctrlKey || e.metaKey || e.shiftKey) { + if (e.altKey) desc += 'alt '; + if (e.ctrlKey) desc += 'control '; + if (e.metaKey) desc += 'meta '; + if (e.shiftKey) desc += 'shift '; + } + var pos = this.getBoundingClientRect(); + $(this).after('Has been ' + desc + 'right clicked at ' + (e.clientX - pos.left) + ',' + (e.clientY - pos.top) + ''); } }); $('#open-alert').click(function() { diff --git a/lib/capybara/spec/session/node_spec.rb b/lib/capybara/spec/session/node_spec.rb index 0f237755..5135c37b 100644 --- a/lib/capybara/spec/session/node_spec.rb +++ b/lib/capybara/spec/session/node_spec.rb @@ -338,15 +338,58 @@ Capybara::SpecHelper.spec "node" do radio.click expect(radio).to be_checked end + + it "should allow modifiers", requires: [:js] do + @session.visit('/with_js') + @session.find(:css, '#click-test').click(:control) + expect(@session).to have_link('Has been control clicked') + end + + it "should allow multiple modifiers", requires: [:js] do + @session.visit('with_js') + @session.find(:css, '#click-test').click(:control, :alt, :meta, :shift) + expect(@session).to have_link('Has been alt control meta shift clicked') + end + + it "should allow to adjust the click offset", requires: [:js] do + @session.visit('with_js') + @session.find(:css, '#click-test').click(x:0, y:0) + link = @session.find(:link, 'has-been-clicked') + locations = link.text.match /^Has been clicked at (?[\d\.-]+),(?[\d\.-]+)$/ + # Resulting click location should be very close to 0, 0 relative to top left corner of the element, but may not be exact due to + # integer/float conversions and rounding. + expect(locations[:x].to_f).to be_within(1).of(0) + expect(locations[:y].to_f).to be_within(1).of(0) + end end - describe '#double_click', requires: [:js] do - it "should double click an element" do + describe '#double_click', requires: [:js], focus_: true do + before do pending "selenium-webdriver/geckodriver doesn't generate double click event" if marionette?(@session) + end + + it "should double click an element" do @session.visit('/with_js') @session.find(:css, '#click-test').double_click expect(@session.find(:css, '#has-been-double-clicked')).to be end + + it "should allow modifiers", requires: [:js] do + @session.visit('/with_js') + @session.find(:css, '#click-test').double_click(:alt) + expect(@session).to have_link('Has been alt double clicked') + end + + it "should allow to adjust the offset", requires: [:js] do + @session.visit('with_js') + @session.find(:css, '#click-test').double_click(x:10, y:5) + link = @session.find(:link, 'has-been-double-clicked') + locations = link.text.match /^Has been double clicked at (?[\d\.-]+),(?[\d\.-]+)$/ + # Resulting click location should be very close to 10, 5 relative to top left corner of the element, but may not be exact due + # to integer/float conversions and rounding. + expect(locations[:x].to_f).to be_within(1).of(10) + expect(locations[:y].to_f).to be_within(1).of(5) + end end describe '#right_click', requires: [:js] do @@ -355,6 +398,23 @@ Capybara::SpecHelper.spec "node" do @session.find(:css, '#click-test').right_click expect(@session.find(:css, '#has-been-right-clicked')).to be end + + it "should allow modifiers", requires: [:js] do + @session.visit('/with_js') + @session.find(:css, '#click-test').right_click(:meta) + expect(@session).to have_link('Has been meta right clicked') + end + + it "should allow to adjust the offset", requires: [:js] do + @session.visit('with_js') + @session.find(:css, '#click-test').right_click(x:10, y:10) + link = @session.find(:link, 'has-been-right-clicked') + locations = link.text.match /^Has been right clicked at (?[\d\.-]+),(?[\d\.-]+)$/ + # Resulting click location should be very close to 10, 10 relative to top left corner of the element, but may not be exact due + # to integer/float conversions and rounding + expect(locations[:x].to_f).to be_within(1).of(10) + expect(locations[:y].to_f).to be_within(1).of(10) + end end describe '#send_keys', requires: [:send_keys] do