capybara 3.29.0 → 3.37.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +229 -15
  3. data/README.md +13 -4
  4. data/lib/capybara/config.rb +24 -10
  5. data/lib/capybara/cucumber.rb +1 -1
  6. data/lib/capybara/driver/base.rb +8 -0
  7. data/lib/capybara/driver/node.rb +5 -1
  8. data/lib/capybara/dsl.rb +5 -3
  9. data/lib/capybara/helpers.rb +19 -2
  10. data/lib/capybara/minitest/spec.rb +156 -97
  11. data/lib/capybara/minitest.rb +232 -144
  12. data/lib/capybara/node/actions.rb +41 -37
  13. data/lib/capybara/node/base.rb +6 -6
  14. data/lib/capybara/node/document.rb +2 -2
  15. data/lib/capybara/node/document_matchers.rb +3 -3
  16. data/lib/capybara/node/element.rb +35 -21
  17. data/lib/capybara/node/finders.rb +33 -19
  18. data/lib/capybara/node/matchers.rb +72 -57
  19. data/lib/capybara/node/simple.rb +13 -3
  20. data/lib/capybara/queries/active_element_query.rb +18 -0
  21. data/lib/capybara/queries/ancestor_query.rb +4 -3
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +14 -4
  24. data/lib/capybara/queries/selector_query.rb +91 -30
  25. data/lib/capybara/queries/sibling_query.rb +4 -3
  26. data/lib/capybara/queries/style_query.rb +1 -1
  27. data/lib/capybara/queries/text_query.rb +7 -1
  28. data/lib/capybara/rack_test/browser.rb +68 -10
  29. data/lib/capybara/rack_test/driver.rb +6 -5
  30. data/lib/capybara/rack_test/form.rb +2 -2
  31. data/lib/capybara/rack_test/node.rb +44 -16
  32. data/lib/capybara/registration_container.rb +41 -0
  33. data/lib/capybara/registrations/drivers.rb +18 -12
  34. data/lib/capybara/registrations/patches/puma_ssl.rb +3 -1
  35. data/lib/capybara/registrations/servers.rb +3 -2
  36. data/lib/capybara/result.rb +35 -15
  37. data/lib/capybara/rspec/matcher_proxies.rb +8 -8
  38. data/lib/capybara/rspec/matchers/base.rb +12 -6
  39. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  40. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  41. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  42. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  43. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  44. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  45. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  46. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  47. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  48. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  49. data/lib/capybara/rspec/matchers.rb +33 -32
  50. data/lib/capybara/rspec.rb +2 -0
  51. data/lib/capybara/selector/builders/css_builder.rb +2 -2
  52. data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
  53. data/lib/capybara/selector/css.rb +2 -2
  54. data/lib/capybara/selector/definition/button.rb +35 -13
  55. data/lib/capybara/selector/definition/checkbox.rb +3 -3
  56. data/lib/capybara/selector/definition/css.rb +3 -1
  57. data/lib/capybara/selector/definition/datalist_input.rb +2 -2
  58. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  59. data/lib/capybara/selector/definition/element.rb +3 -2
  60. data/lib/capybara/selector/definition/field.rb +1 -1
  61. data/lib/capybara/selector/definition/file_field.rb +2 -2
  62. data/lib/capybara/selector/definition/fillable_field.rb +3 -3
  63. data/lib/capybara/selector/definition/label.rb +5 -3
  64. data/lib/capybara/selector/definition/link.rb +8 -0
  65. data/lib/capybara/selector/definition/radio_button.rb +3 -3
  66. data/lib/capybara/selector/definition/select.rb +33 -14
  67. data/lib/capybara/selector/definition/table.rb +6 -3
  68. data/lib/capybara/selector/definition/table_row.rb +2 -2
  69. data/lib/capybara/selector/definition.rb +15 -11
  70. data/lib/capybara/selector/filter_set.rb +17 -17
  71. data/lib/capybara/selector/filters/base.rb +6 -1
  72. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  73. data/lib/capybara/selector/selector.rb +17 -3
  74. data/lib/capybara/selector.rb +37 -19
  75. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  76. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  77. data/lib/capybara/selenium/atoms/src/isDisplayed.js +1 -1
  78. data/lib/capybara/selenium/driver.rb +84 -17
  79. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +11 -13
  80. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +10 -12
  81. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +4 -4
  82. data/lib/capybara/selenium/extensions/find.rb +4 -4
  83. data/lib/capybara/selenium/extensions/html5_drag.rb +30 -13
  84. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  85. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  86. data/lib/capybara/selenium/node.rb +122 -26
  87. data/lib/capybara/selenium/nodes/chrome_node.rb +34 -19
  88. data/lib/capybara/selenium/nodes/edge_node.rb +5 -3
  89. data/lib/capybara/selenium/nodes/firefox_node.rb +11 -6
  90. data/lib/capybara/selenium/nodes/safari_node.rb +3 -3
  91. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  92. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  93. data/lib/capybara/selenium/patches/logs.rb +7 -9
  94. data/lib/capybara/server/animation_disabler.rb +38 -15
  95. data/lib/capybara/server/checker.rb +1 -1
  96. data/lib/capybara/server/middleware.rb +22 -10
  97. data/lib/capybara/server.rb +15 -3
  98. data/lib/capybara/session/config.rb +10 -4
  99. data/lib/capybara/session/matchers.rb +11 -11
  100. data/lib/capybara/session.rb +62 -39
  101. data/lib/capybara/spec/public/test.js +75 -7
  102. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  103. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  104. data/lib/capybara/spec/session/all_spec.rb +63 -12
  105. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  106. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  107. data/lib/capybara/spec/session/check_spec.rb +15 -0
  108. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  109. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  110. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  111. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  112. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  113. data/lib/capybara/spec/session/find_spec.rb +37 -8
  114. data/lib/capybara/spec/session/has_any_selectors_spec.rb +4 -0
  115. data/lib/capybara/spec/session/has_button_spec.rb +75 -0
  116. data/lib/capybara/spec/session/has_css_spec.rb +14 -10
  117. data/lib/capybara/spec/session/has_current_path_spec.rb +17 -4
  118. data/lib/capybara/spec/session/has_field_spec.rb +41 -1
  119. data/lib/capybara/spec/session/has_link_spec.rb +30 -0
  120. data/lib/capybara/spec/session/has_select_spec.rb +36 -8
  121. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  122. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  123. data/lib/capybara/spec/session/has_text_spec.rb +21 -1
  124. data/lib/capybara/spec/session/html_spec.rb +1 -1
  125. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  126. data/lib/capybara/spec/session/node_spec.rb +226 -33
  127. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  128. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  129. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  130. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  131. data/lib/capybara/spec/session/scroll_spec.rb +4 -4
  132. data/lib/capybara/spec/session/selectors_spec.rb +15 -2
  133. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  134. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  135. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  136. data/lib/capybara/spec/session/window/window_spec.rb +9 -9
  137. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  138. data/lib/capybara/spec/spec_helper.rb +17 -17
  139. data/lib/capybara/spec/test_app.rb +89 -29
  140. data/lib/capybara/spec/views/animated.erb +1 -1
  141. data/lib/capybara/spec/views/form.erb +52 -6
  142. data/lib/capybara/spec/views/frame_child.erb +1 -1
  143. data/lib/capybara/spec/views/frame_one.erb +1 -1
  144. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  145. data/lib/capybara/spec/views/frame_two.erb +1 -1
  146. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  147. data/lib/capybara/spec/views/layout.erb +10 -0
  148. data/lib/capybara/spec/views/obscured.erb +1 -1
  149. data/lib/capybara/spec/views/offset.erb +2 -1
  150. data/lib/capybara/spec/views/path.erb +2 -2
  151. data/lib/capybara/spec/views/popup_one.erb +1 -1
  152. data/lib/capybara/spec/views/popup_two.erb +1 -1
  153. data/lib/capybara/spec/views/react.erb +2 -2
  154. data/lib/capybara/spec/views/scroll.erb +2 -1
  155. data/lib/capybara/spec/views/spatial.erb +1 -1
  156. data/lib/capybara/spec/views/with_animation.erb +10 -3
  157. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  158. data/lib/capybara/spec/views/with_dragula.erb +5 -3
  159. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  160. data/lib/capybara/spec/views/with_hover.erb +2 -2
  161. data/lib/capybara/spec/views/with_html.erb +3 -3
  162. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  163. data/lib/capybara/spec/views/with_js.erb +5 -3
  164. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  165. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  166. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  167. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  168. data/lib/capybara/spec/views/with_sortable_js.erb +3 -3
  169. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  170. data/lib/capybara/spec/views/with_windows.erb +1 -1
  171. data/lib/capybara/spec/views/within_frames.erb +1 -1
  172. data/lib/capybara/version.rb +1 -1
  173. data/lib/capybara/window.rb +4 -8
  174. data/lib/capybara.rb +36 -29
  175. data/spec/basic_node_spec.rb +25 -11
  176. data/spec/capybara_spec.rb +1 -1
  177. data/spec/dsl_spec.rb +18 -5
  178. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  179. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  180. data/spec/minitest_spec.rb +3 -2
  181. data/spec/minitest_spec_spec.rb +46 -46
  182. data/spec/rack_test_spec.rb +43 -11
  183. data/spec/regexp_dissassembler_spec.rb +40 -36
  184. data/spec/result_spec.rb +53 -45
  185. data/spec/rspec/features_spec.rb +7 -4
  186. data/spec/rspec/scenarios_spec.rb +5 -1
  187. data/spec/rspec/shared_spec_matchers.rb +68 -56
  188. data/spec/rspec_spec.rb +8 -4
  189. data/spec/sauce_spec_chrome.rb +3 -3
  190. data/spec/selector_spec.rb +19 -4
  191. data/spec/selenium_spec_chrome.rb +49 -26
  192. data/spec/selenium_spec_chrome_remote.rb +13 -6
  193. data/spec/selenium_spec_firefox.rb +29 -17
  194. data/spec/selenium_spec_firefox_remote.rb +2 -2
  195. data/spec/selenium_spec_ie.rb +3 -6
  196. data/spec/selenium_spec_safari.rb +31 -19
  197. data/spec/server_spec.rb +88 -35
  198. data/spec/session_spec.rb +1 -1
  199. data/spec/shared_selenium_node.rb +21 -7
  200. data/spec/shared_selenium_session.rb +123 -21
  201. data/spec/spec_helper.rb +2 -2
  202. metadata +80 -21
  203. data/lib/capybara/spec/session/source_spec.rb +0 -0
  204. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -7,27 +7,25 @@ module Capybara::Selenium::Driver::ChromeDriver
7
7
  def self.extended(base)
8
8
  bridge = base.send(:bridge)
9
9
  bridge.extend Capybara::Selenium::ChromeLogs unless bridge.respond_to?(:log)
10
- bridge.extend Capybara::Selenium::IsDisplayed unless bridge.commands(:is_element_displayed)
10
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
11
11
  base.options[:native_displayed] = false if base.options[:native_displayed].nil?
12
12
  end
13
13
 
14
14
  def fullscreen_window(handle)
15
15
  within_given_window(handle) do
16
- begin
17
- super
18
- rescue NoMethodError => e
19
- raise unless e.message.match?(/full_screen_window/)
20
-
21
- result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
22
- result['value']
23
- end
16
+ super
17
+ rescue NoMethodError => e
18
+ raise unless e.message.include?('full_screen_window')
19
+
20
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
21
+ result['value']
24
22
  end
25
23
  end
26
24
 
27
25
  def resize_window_to(handle, width, height)
28
26
  super
29
27
  rescue Selenium::WebDriver::Error::UnknownError => e
30
- raise unless e.message.match?(/failed to change window state/)
28
+ raise unless e.message.include?('failed to change window state')
31
29
 
32
30
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
33
31
  # and raises unnecessary error. Wait a bit and try again.
@@ -40,7 +38,7 @@ module Capybara::Selenium::Driver::ChromeDriver
40
38
  return unless @browser
41
39
 
42
40
  switch_to_window(window_handles.first)
43
- window_handles.slice(1..-1).each { |win| close_window(win) }
41
+ window_handles.slice(1..).each { |win| close_window(win) }
44
42
  return super if chromedriver_version < 73
45
43
 
46
44
  timer = Capybara::Helpers.timer(expire_in: 10)
@@ -65,7 +63,7 @@ private
65
63
  end
66
64
 
67
65
  def clear_all_storage?
68
- storage_clears.none? { |s| s == false }
66
+ storage_clears.none? false
69
67
  end
70
68
 
71
69
  def uniform_storage_clear?
@@ -96,7 +94,7 @@ private
96
94
 
97
95
  def execute_cdp(cmd, params = {})
98
96
  if browser.respond_to? :execute_cdp
99
- browser.execute_cdp(cmd, params)
97
+ browser.execute_cdp(cmd, **params)
100
98
  else
101
99
  args = { cmd: cmd, params: params }
102
100
  result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
@@ -5,7 +5,7 @@ require 'capybara/selenium/nodes/edge_node'
5
5
  module Capybara::Selenium::Driver::EdgeDriver
6
6
  def self.extended(base)
7
7
  bridge = base.send(:bridge)
8
- bridge.extend Capybara::Selenium::IsDisplayed unless bridge.commands(:is_element_displayed)
8
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
9
9
  base.options[:native_displayed] = false if base.options[:native_displayed].nil?
10
10
  end
11
11
 
@@ -13,21 +13,19 @@ module Capybara::Selenium::Driver::EdgeDriver
13
13
  return super if edgedriver_version < 75
14
14
 
15
15
  within_given_window(handle) do
16
- begin
17
- super
18
- rescue NoMethodError => e
19
- raise unless e.message.match?(/full_screen_window/)
20
-
21
- result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
22
- result['value']
23
- end
16
+ super
17
+ rescue NoMethodError => e
18
+ raise unless e.message.include?('full_screen_window')
19
+
20
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
21
+ result['value']
24
22
  end
25
23
  end
26
24
 
27
25
  def resize_window_to(handle, width, height)
28
26
  super
29
27
  rescue Selenium::WebDriver::Error::UnknownError => e
30
- raise unless e.message.match?(/failed to change window state/)
28
+ raise unless e.message.include?('failed to change window state')
31
29
 
32
30
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
33
31
  # and raises unnecessary error. Wait a bit and try again.
@@ -41,7 +39,7 @@ module Capybara::Selenium::Driver::EdgeDriver
41
39
  return unless @browser
42
40
 
43
41
  switch_to_window(window_handles.first)
44
- window_handles.slice(1..-1).each { |win| close_window(win) }
42
+ window_handles.slice(1..).each { |win| close_window(win) }
45
43
 
46
44
  timer = Capybara::Helpers.timer(expire_in: 10)
47
45
  begin
@@ -74,7 +72,7 @@ private
74
72
  end
75
73
 
76
74
  def clear_all_storage?
77
- storage_clears.none? { |s| s == false }
75
+ storage_clears.none? false
78
76
  end
79
77
 
80
78
  def uniform_storage_clear?
@@ -6,7 +6,7 @@ module Capybara::Selenium::Driver::FirefoxDriver
6
6
  def self.extended(driver)
7
7
  driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver if w3c?(driver)
8
8
  bridge = driver.send(:bridge)
9
- bridge.extend Capybara::Selenium::IsDisplayed unless bridge.commands(:is_element_displayed)
9
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
10
10
  end
11
11
 
12
12
  def self.w3c?(driver)
@@ -46,13 +46,13 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
46
46
  begin
47
47
  # Firefox 68 hangs if we try to switch windows while a modal is visible
48
48
  browser.switch_to.alert&.dismiss
49
- rescue Selenium::WebDriver::Error::NoSuchAlertError # rubocop:disable Lint/HandleExceptions
49
+ rescue Selenium::WebDriver::Error::NoSuchAlertError
50
50
  # Swallow
51
51
  end
52
52
  end
53
53
 
54
54
  switch_to_window(window_handles.first)
55
- window_handles.slice(1..-1).each { |win| close_window(win) }
55
+ window_handles.slice(1..).each { |win| close_window(win) }
56
56
  super
57
57
  end
58
58
 
@@ -61,7 +61,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
61
61
  accept_modal :confirm, wait: 0.1 do
62
62
  super
63
63
  end
64
- rescue Capybara::ModalNotFound # rubocop:disable Lint/HandleExceptions
64
+ rescue Capybara::ModalNotFound
65
65
  # No modal was opened - page has refreshed - ignore
66
66
  end
67
67
 
@@ -28,7 +28,7 @@ module Capybara
28
28
  hints_js, functions = build_hints_js(uses_visibility, styles, position)
29
29
  return [] unless functions.any?
30
30
 
31
- es_context.execute_script(hints_js, elements).map! do |results|
31
+ (es_context.execute_script(hints_js, elements) || []).map! do |results|
32
32
  hint = {}
33
33
  hint[:style] = results.pop if functions.include?(:style_func)
34
34
  hint[:position] = results.pop if functions.include?(:position_func)
@@ -100,9 +100,9 @@ module Capybara
100
100
  def is_displayed_atom # rubocop:disable Naming/PredicateName
101
101
  @@is_displayed_atom ||= begin # rubocop:disable Style/ClassVars
102
102
  browser.send(:bridge).send(:read_atom, 'isDisplayed')
103
- rescue StandardError
104
- # If the atom doesn't exist or other error
105
- ''
103
+ rescue StandardError
104
+ # If the atom doesn't exist or other error
105
+ ''
106
106
  end
107
107
  end
108
108
  end
@@ -4,25 +4,32 @@ class Capybara::Selenium::Node
4
4
  module Html5Drag
5
5
  # Implement methods to emulate HTML5 drag and drop
6
6
 
7
- def drag_to(element, html5: nil, delay: 0.05)
7
+ def drag_to(element, html5: nil, delay: 0.05, drop_modifiers: [])
8
+ drop_modifiers = Array(drop_modifiers)
9
+
8
10
  driver.execute_script MOUSEDOWN_TRACKER
9
11
  scroll_if_needed { browser_action.click_and_hold(native).perform }
10
12
  html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
11
13
  if html5
12
- perform_html5_drag(element, delay)
14
+ perform_html5_drag(element, delay, drop_modifiers)
13
15
  else
14
- perform_legacy_drag(element)
16
+ perform_legacy_drag(element, drop_modifiers)
15
17
  end
16
18
  end
17
19
 
18
20
  private
19
21
 
20
- def perform_legacy_drag(element)
21
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
22
+ def perform_legacy_drag(element, drop_modifiers)
23
+ element.scroll_if_needed do
24
+ # browser_action.move_to(element.native).release.perform
25
+ keys_down = modifiers_down(browser_action, drop_modifiers)
26
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
27
+ keys_up.perform
28
+ end
22
29
  end
23
30
 
24
- def perform_html5_drag(element, delay)
25
- driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000
31
+ def perform_html5_drag(element, delay, drop_modifiers)
32
+ driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000, normalize_keys(drop_modifiers)
26
33
  browser_action.release.perform
27
34
  end
28
35
 
@@ -153,6 +160,14 @@ class Capybara::Selenium::Node
153
160
  var targetRect = target.getBoundingClientRect();
154
161
  var sourceCenter = rectCenter(source.getBoundingClientRect());
155
162
 
163
+ for (var i = 0; i < drop_modifier_keys.length; i++) {
164
+ key = drop_modifier_keys[i];
165
+ if (key == "control"){
166
+ key = "ctrl"
167
+ }
168
+ opts[key + 'Key'] = true;
169
+ }
170
+
156
171
  // fire 2 dragover events to simulate dragging with a direction
157
172
  var entryPoint = pointOnRect(sourceCenter, targetRect)
158
173
  var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
@@ -166,17 +181,18 @@ class Capybara::Selenium::Node
166
181
  var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
167
182
  var dragOverEvent = new DragEvent('dragover', dragOverOpts);
168
183
  target.dispatchEvent(dragOverEvent);
169
- window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented);
184
+ window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
170
185
  }
171
186
 
172
- function dragLeave(drop) {
173
- var dragLeaveEvent = new DragEvent('dragleave', opts);
187
+ function dragLeave(drop, dragOverOpts) {
188
+ var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
189
+ var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
174
190
  target.dispatchEvent(dragLeaveEvent);
175
191
  if (drop) {
176
- var dropEvent = new DragEvent('drop', opts);
192
+ var dropEvent = new DragEvent('drop', dragLeaveOptions);
177
193
  target.dispatchEvent(dropEvent);
178
194
  }
179
- var dragEndEvent = new DragEvent('dragend', opts);
195
+ var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
180
196
  source.dispatchEvent(dragEndEvent);
181
197
  callback.call(true);
182
198
  }
@@ -184,7 +200,8 @@ class Capybara::Selenium::Node
184
200
  var source = arguments[0],
185
201
  target = arguments[1],
186
202
  step_delay = arguments[2],
187
- callback = arguments[3];
203
+ drop_modifier_keys = arguments[3],
204
+ callback = arguments[4];
188
205
 
189
206
  var dt = new DataTransfer();
190
207
  var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
@@ -45,20 +45,18 @@ module Capybara
45
45
  JS
46
46
  end
47
47
 
48
+ SCROLL_POSITIONS = {
49
+ top: '0',
50
+ bottom: 'arguments[0].scrollHeight',
51
+ center: '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
52
+ }.freeze
53
+
48
54
  def scroll_to_location(location)
49
- scroll_y = case location
50
- when :top
51
- '0'
52
- when :bottom
53
- 'arguments[0].scrollHeight'
54
- when :center
55
- '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
56
- end
57
55
  driver.execute_script <<~JS, self
58
56
  if (arguments[0].scrollTo){
59
- arguments[0].scrollTo(0, #{scroll_y});
57
+ arguments[0].scrollTo(0, #{SCROLL_POSITIONS[location]});
60
58
  } else {
61
- arguments[0].scrollTop = #{scroll_y};
59
+ arguments[0].scrollTop = #{SCROLL_POSITIONS[location]};
62
60
  }
63
61
  JS
64
62
  end
@@ -8,8 +8,14 @@ module Capybara
8
8
  super
9
9
  end
10
10
 
11
- def deprecate(*)
12
- super unless @suppress_for_capybara
11
+ def deprecate(*args, **opts, &block)
12
+ return if @suppress_for_capybara
13
+
14
+ if opts.empty?
15
+ super(*args, &block) # support Selenium 3
16
+ else
17
+ super
18
+ end
13
19
  end
14
20
 
15
21
  def suppress_deprecations
@@ -14,7 +14,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
14
14
  end
15
15
 
16
16
  def all_text
17
- text = driver.evaluate_script('arguments[0].textContent', self)
17
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
18
18
  text.gsub(/[\u200b\u200e\u200f]/, '')
19
19
  .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
20
20
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
@@ -53,8 +53,15 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
53
53
  # :none => append the new value to the existing value <br/>
54
54
  # :backspace => send backspace keystrokes to clear the field <br/>
55
55
  # Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
56
+ # @option options [Boolean] :rapid (nil) Whether setting text inputs should use a faster &quot;rapid&quot; mode<br/>
57
+ # nil => Text inputs with length greater than 30 characters will be set using a faster driver script mode<br/>
58
+ # true => Rapid mode will be used regardless of input length<br/>
59
+ # false => Sends keys via conventional mode. This may be required to avoid losing key-presses if you have certain
60
+ # Javascript interactions on form inputs<br/>
56
61
  def set(value, **options)
57
- raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}" if value.is_a?(Array) && !multiple?
62
+ if value.is_a?(Array) && !multiple?
63
+ raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
64
+ end
58
65
 
59
66
  tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
60
67
  @tag_name ||= tag_name
@@ -76,11 +83,13 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
76
83
  set_datetime_local(value)
77
84
  when 'color'
78
85
  set_color(value)
86
+ when 'range'
87
+ set_range(value)
79
88
  else
80
- set_text(value, options)
89
+ set_text(value, **options)
81
90
  end
82
91
  when 'textarea'
83
- set_text(value, options)
92
+ set_text(value, **options)
84
93
  else
85
94
  set_content_editable(value)
86
95
  end
@@ -100,10 +109,23 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
100
109
  click_options = ClickOptions.new(keys, options)
101
110
  return native.click if click_options.empty?
102
111
 
103
- click_with_options(click_options)
112
+ perform_with_options(click_options) do |action|
113
+ target = click_options.coords? ? nil : native
114
+ if click_options.delay.zero?
115
+ action.click(target)
116
+ else
117
+ action.click_and_hold(target)
118
+ if w3c?
119
+ action.pause(action.pointer_inputs.first, click_options.delay)
120
+ else
121
+ action.pause(click_options.delay)
122
+ end
123
+ action.release
124
+ end
125
+ end
104
126
  rescue StandardError => e
105
127
  if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
106
- e.message.match?(/Other element would receive the click/)
128
+ e.message.include?('Other element would receive the click')
107
129
  scroll_to_center
108
130
  end
109
131
 
@@ -112,14 +134,26 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
112
134
 
113
135
  def right_click(keys = [], **options)
114
136
  click_options = ClickOptions.new(keys, options)
115
- click_with_options(click_options) do |action|
116
- click_options.coords? ? action.context_click : action.context_click(native)
137
+ perform_with_options(click_options) do |action|
138
+ target = click_options.coords? ? nil : native
139
+ if click_options.delay.zero?
140
+ action.context_click(target)
141
+ elsif w3c?
142
+ action.move_to(target) if target
143
+ action.pointer_down(:right)
144
+ .pause(action.pointer_inputs.first, click_options.delay)
145
+ .pointer_up(:right)
146
+ else
147
+ raise ArgumentError, 'Delay is not supported when right clicking with legacy (non-w3c) selenium driver'
148
+ end
117
149
  end
118
150
  end
119
151
 
120
152
  def double_click(keys = [], **options)
121
153
  click_options = ClickOptions.new(keys, options)
122
- click_with_options(click_options) do |action|
154
+ raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?
155
+
156
+ perform_with_options(click_options) do |action|
123
157
  click_options.coords? ? action.double_click : action.double_click(native)
124
158
  end
125
159
  end
@@ -132,11 +166,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
132
166
  scroll_if_needed { browser_action.move_to(native).perform }
133
167
  end
134
168
 
135
- def drag_to(element, **)
169
+ def drag_to(element, drop_modifiers: [], **)
170
+ drop_modifiers = Array(drop_modifiers)
136
171
  # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
137
172
  # which means Seleniums `drag_and_drop` is now broken - do it manually
138
173
  scroll_if_needed { browser_action.click_and_hold(native).perform }
139
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
174
+ # element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
175
+ element.scroll_if_needed do
176
+ keys_down = modifiers_down(browser_action, drop_modifiers)
177
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
178
+ keys_up.perform
179
+ end
140
180
  end
141
181
 
142
182
  def drop(*_)
@@ -164,10 +204,6 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
164
204
  native.attribute('isContentEditable') == 'true'
165
205
  end
166
206
 
167
- def ==(other)
168
- native == other.native
169
- end
170
-
171
207
  def path
172
208
  driver.evaluate_script GET_XPATH_SCRIPT, self
173
209
  end
@@ -183,6 +219,13 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
183
219
  native.rect
184
220
  end
185
221
 
222
+ def shadow_root
223
+ raise_error 'You must be using Selenium 4.1+ for shadow_root support' unless native.respond_to? :shadow_root
224
+
225
+ root = native.shadow_root
226
+ root && build_node(native.shadow_root)
227
+ end
228
+
186
229
  protected
187
230
 
188
231
  def scroll_if_needed
@@ -202,7 +245,7 @@ protected
202
245
  JS
203
246
  begin
204
247
  driver.execute_script(script, self)
205
- rescue StandardError # rubocop:disable Lint/HandleExceptions
248
+ rescue StandardError
206
249
  # Swallow error if scrollIntoView with options isn't supported
207
250
  end
208
251
  end
@@ -232,26 +275,38 @@ private
232
275
  find_xpath(XPath.ancestor(:select)[1]).first
233
276
  end
234
277
 
235
- def set_text(value, clear: nil, **_unused)
278
+ def set_text(value, clear: nil, rapid: nil, **_unused)
236
279
  value = value.to_s
237
280
  if value.empty? && clear.nil?
238
281
  native.clear
239
282
  elsif clear == :backspace
240
283
  # Clear field by sending the correct number of backspace keys.
241
284
  backspaces = [:backspace] * self.value.to_s.length
242
- send_keys(*([:end] + backspaces + [value]))
285
+ send_keys(:end, *backspaces, value)
243
286
  elsif clear.is_a? Array
244
287
  send_keys(*clear, value)
245
288
  else
246
289
  driver.execute_script 'arguments[0].select()', self unless clear == :none
247
- send_keys(value)
290
+ if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
291
+ send_keys(value[0..3])
292
+ driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
293
+ send_keys(value[-3..])
294
+ else
295
+ send_keys(value)
296
+ end
248
297
  end
249
298
  end
250
299
 
251
- def click_with_options(click_options)
300
+ def auto_rapid_set_length
301
+ 30
302
+ end
303
+
304
+ def perform_with_options(click_options, &block)
305
+ raise ArgumentError, 'A block must be provided' unless block
306
+
252
307
  scroll_if_needed do
253
308
  action_with_modifiers(click_options) do |action|
254
- if block_given?
309
+ if block
255
310
  yield action
256
311
  else
257
312
  click_options.coords? ? action.click : action.click(native)
@@ -288,6 +343,10 @@ private
288
343
  update_value_js(value)
289
344
  end
290
345
 
346
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
347
+ update_value_js(value)
348
+ end
349
+
291
350
  def update_value_js(value)
292
351
  driver.execute_script(<<-JS, self, value)
293
352
  if (arguments[0].readOnly) { return };
@@ -373,10 +432,12 @@ private
373
432
 
374
433
  def modifiers_down(actions, keys)
375
434
  each_key(keys) { |key| actions.key_down(key) }
435
+ actions
376
436
  end
377
437
 
378
438
  def modifiers_up(actions, keys)
379
439
  each_key(keys) { |key| actions.key_up(key) }
440
+ actions
380
441
  end
381
442
 
382
443
  def browser
@@ -391,18 +452,30 @@ private
391
452
  browser.action
392
453
  end
393
454
 
394
- def each_key(keys)
395
- keys.each do |key|
396
- key = case key
455
+ def capabilities
456
+ browser.capabilities
457
+ end
458
+
459
+ def w3c?
460
+ (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
461
+ capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
462
+ end
463
+
464
+ def normalize_keys(keys)
465
+ keys.map do |key|
466
+ case key
397
467
  when :ctrl then :control
398
468
  when :command, :cmd then :meta
399
469
  else
400
470
  key
401
471
  end
402
- yield key
403
472
  end
404
473
  end
405
474
 
475
+ def each_key(keys, &block)
476
+ normalize_keys(keys).each(&block)
477
+ end
478
+
406
479
  def find_context
407
480
  native
408
481
  end
@@ -423,11 +496,20 @@ private
423
496
  JS
424
497
  end
425
498
 
499
+ def native_id
500
+ # Selenium 3 -> 4 changed the return of ref
501
+ type_or_id, id = native.ref
502
+ id || type_or_id
503
+ end
504
+
426
505
  GET_XPATH_SCRIPT = <<~'JS'
427
506
  (function(el, xml){
428
507
  var xpath = '';
429
508
  var pos, tempitem2;
430
509
 
510
+ if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
511
+ return "(: Shadow DOM element - no XPath :)";
512
+ };
431
513
  while(el !== xml.documentElement) {
432
514
  pos = 0;
433
515
  tempitem2 = el;
@@ -469,6 +551,16 @@ private
469
551
  })(arguments[0], arguments[1], arguments[2])
470
552
  JS
471
553
 
554
+ RAPID_APPEND_TEXT = <<~'JS'
555
+ (function(el, value) {
556
+ value = el.value + value;
557
+ if (el.maxLength && el.maxLength != -1){
558
+ value = value.slice(0, el.maxLength);
559
+ }
560
+ el.value = value;
561
+ })(arguments[0], arguments[1])
562
+ JS
563
+
472
564
  # SettableValue encapsulates time/date field formatting
473
565
  class SettableValue
474
566
  attr_reader :value
@@ -525,7 +617,11 @@ private
525
617
  end
526
618
 
527
619
  def empty?
528
- keys.empty? && !coords?
620
+ keys.empty? && !coords? && delay.zero?
621
+ end
622
+
623
+ def delay
624
+ options[:delay] || 0
529
625
  end
530
626
  end
531
627
  private_constant :ClickOptions