capybara 3.13.2 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +587 -16
  4. data/README.md +240 -90
  5. data/lib/capybara/config.rb +24 -11
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +8 -0
  8. data/lib/capybara/driver/node.rb +20 -4
  9. data/lib/capybara/dsl.rb +5 -3
  10. data/lib/capybara/helpers.rb +25 -4
  11. data/lib/capybara/minitest/spec.rb +174 -90
  12. data/lib/capybara/minitest.rb +256 -142
  13. data/lib/capybara/node/actions.rb +123 -77
  14. data/lib/capybara/node/base.rb +20 -12
  15. data/lib/capybara/node/document.rb +2 -2
  16. data/lib/capybara/node/document_matchers.rb +3 -3
  17. data/lib/capybara/node/element.rb +223 -117
  18. data/lib/capybara/node/finders.rb +81 -71
  19. data/lib/capybara/node/matchers.rb +271 -134
  20. data/lib/capybara/node/simple.rb +18 -5
  21. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  22. data/lib/capybara/queries/active_element_query.rb +18 -0
  23. data/lib/capybara/queries/ancestor_query.rb +8 -9
  24. data/lib/capybara/queries/base_query.rb +3 -2
  25. data/lib/capybara/queries/current_path_query.rb +15 -5
  26. data/lib/capybara/queries/selector_query.rb +364 -54
  27. data/lib/capybara/queries/sibling_query.rb +8 -6
  28. data/lib/capybara/queries/style_query.rb +2 -2
  29. data/lib/capybara/queries/text_query.rb +13 -1
  30. data/lib/capybara/queries/title_query.rb +1 -1
  31. data/lib/capybara/rack_test/browser.rb +76 -11
  32. data/lib/capybara/rack_test/driver.rb +10 -5
  33. data/lib/capybara/rack_test/errors.rb +6 -0
  34. data/lib/capybara/rack_test/form.rb +31 -9
  35. data/lib/capybara/rack_test/node.rb +74 -23
  36. data/lib/capybara/registration_container.rb +41 -0
  37. data/lib/capybara/registrations/drivers.rb +42 -0
  38. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  39. data/lib/capybara/registrations/servers.rb +66 -0
  40. data/lib/capybara/result.rb +44 -20
  41. data/lib/capybara/rspec/matcher_proxies.rb +13 -11
  42. data/lib/capybara/rspec/matchers/base.rb +31 -16
  43. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  44. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  45. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  46. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  47. data/lib/capybara/rspec/matchers/have_selector.rb +21 -21
  48. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  49. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  50. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  51. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  52. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  53. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  54. data/lib/capybara/rspec/matchers.rb +111 -68
  55. data/lib/capybara/rspec.rb +2 -0
  56. data/lib/capybara/selector/builders/css_builder.rb +11 -7
  57. data/lib/capybara/selector/builders/xpath_builder.rb +5 -3
  58. data/lib/capybara/selector/css.rb +11 -9
  59. data/lib/capybara/selector/definition/button.rb +68 -0
  60. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  61. data/lib/capybara/selector/definition/css.rb +10 -0
  62. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  63. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  64. data/lib/capybara/selector/definition/element.rb +28 -0
  65. data/lib/capybara/selector/definition/field.rb +40 -0
  66. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  67. data/lib/capybara/selector/definition/file_field.rb +13 -0
  68. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  69. data/lib/capybara/selector/definition/frame.rb +17 -0
  70. data/lib/capybara/selector/definition/id.rb +6 -0
  71. data/lib/capybara/selector/definition/label.rb +62 -0
  72. data/lib/capybara/selector/definition/link.rb +55 -0
  73. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  74. data/lib/capybara/selector/definition/option.rb +27 -0
  75. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  76. data/lib/capybara/selector/definition/select.rb +81 -0
  77. data/lib/capybara/selector/definition/table.rb +109 -0
  78. data/lib/capybara/selector/definition/table_row.rb +21 -0
  79. data/lib/capybara/selector/definition/xpath.rb +5 -0
  80. data/lib/capybara/selector/definition.rb +280 -0
  81. data/lib/capybara/selector/filter_set.rb +19 -18
  82. data/lib/capybara/selector/filters/base.rb +11 -2
  83. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  84. data/lib/capybara/selector/regexp_disassembler.rb +11 -7
  85. data/lib/capybara/selector/selector.rb +50 -440
  86. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  87. data/lib/capybara/selector.rb +473 -482
  88. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  89. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  90. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  91. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  92. data/lib/capybara/selenium/driver.rb +174 -62
  93. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +74 -18
  94. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  95. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +37 -3
  96. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  97. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  98. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  99. data/lib/capybara/selenium/extensions/find.rb +68 -45
  100. data/lib/capybara/selenium/extensions/html5_drag.rb +192 -22
  101. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  102. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  103. data/lib/capybara/selenium/node.rb +268 -72
  104. data/lib/capybara/selenium/nodes/chrome_node.rb +105 -9
  105. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  106. data/lib/capybara/selenium/nodes/firefox_node.rb +51 -61
  107. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  108. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  109. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  110. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  111. data/lib/capybara/selenium/patches/logs.rb +45 -0
  112. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  113. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  114. data/lib/capybara/server/animation_disabler.rb +43 -21
  115. data/lib/capybara/server/checker.rb +6 -2
  116. data/lib/capybara/server/middleware.rb +25 -13
  117. data/lib/capybara/server.rb +20 -4
  118. data/lib/capybara/session/config.rb +15 -11
  119. data/lib/capybara/session/matchers.rb +11 -11
  120. data/lib/capybara/session.rb +162 -131
  121. data/lib/capybara/spec/public/offset.js +6 -0
  122. data/lib/capybara/spec/public/test.js +105 -6
  123. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  124. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  125. data/lib/capybara/spec/session/all_spec.rb +89 -15
  126. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  127. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  128. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  129. data/lib/capybara/spec/session/attach_file_spec.rb +64 -31
  130. data/lib/capybara/spec/session/check_spec.rb +26 -4
  131. data/lib/capybara/spec/session/choose_spec.rb +14 -2
  132. data/lib/capybara/spec/session/click_button_spec.rb +109 -61
  133. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  134. data/lib/capybara/spec/session/click_link_spec.rb +23 -1
  135. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  136. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  137. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  138. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  139. data/lib/capybara/spec/session/fill_in_spec.rb +46 -5
  140. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  141. data/lib/capybara/spec/session/find_spec.rb +80 -7
  142. data/lib/capybara/spec/session/first_spec.rb +2 -2
  143. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  144. data/lib/capybara/spec/session/frame/within_frame_spec.rb +14 -1
  145. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  146. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  147. data/lib/capybara/spec/session/has_any_selectors_spec.rb +6 -2
  148. data/lib/capybara/spec/session/has_button_spec.rb +81 -0
  149. data/lib/capybara/spec/session/has_css_spec.rb +45 -8
  150. data/lib/capybara/spec/session/has_current_path_spec.rb +22 -7
  151. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  152. data/lib/capybara/spec/session/has_field_spec.rb +59 -1
  153. data/lib/capybara/spec/session/has_link_spec.rb +40 -0
  154. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  155. data/lib/capybara/spec/session/has_select_spec.rb +42 -8
  156. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  157. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  158. data/lib/capybara/spec/session/has_table_spec.rb +177 -0
  159. data/lib/capybara/spec/session/has_text_spec.rb +31 -3
  160. data/lib/capybara/spec/session/html_spec.rb +1 -1
  161. data/lib/capybara/spec/session/matches_style_spec.rb +6 -4
  162. data/lib/capybara/spec/session/node_spec.rb +697 -23
  163. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  164. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  165. data/lib/capybara/spec/session/reset_session_spec.rb +21 -7
  166. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  167. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  168. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  169. data/lib/capybara/spec/session/scroll_spec.rb +9 -7
  170. data/lib/capybara/spec/session/select_spec.rb +5 -10
  171. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  172. data/lib/capybara/spec/session/uncheck_spec.rb +3 -3
  173. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  174. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  175. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  176. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  177. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  178. data/lib/capybara/spec/session/window/window_spec.rb +54 -57
  179. data/lib/capybara/spec/session/window/windows_spec.rb +2 -2
  180. data/lib/capybara/spec/session/within_spec.rb +36 -0
  181. data/lib/capybara/spec/spec_helper.rb +30 -19
  182. data/lib/capybara/spec/test_app.rb +122 -34
  183. data/lib/capybara/spec/views/animated.erb +49 -0
  184. data/lib/capybara/spec/views/form.erb +86 -8
  185. data/lib/capybara/spec/views/frame_child.erb +3 -2
  186. data/lib/capybara/spec/views/frame_one.erb +2 -1
  187. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  188. data/lib/capybara/spec/views/frame_two.erb +1 -1
  189. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  190. data/lib/capybara/spec/views/layout.erb +10 -0
  191. data/lib/capybara/spec/views/obscured.erb +10 -10
  192. data/lib/capybara/spec/views/offset.erb +33 -0
  193. data/lib/capybara/spec/views/path.erb +2 -2
  194. data/lib/capybara/spec/views/popup_one.erb +1 -1
  195. data/lib/capybara/spec/views/popup_two.erb +1 -1
  196. data/lib/capybara/spec/views/react.erb +45 -0
  197. data/lib/capybara/spec/views/scroll.erb +2 -1
  198. data/lib/capybara/spec/views/spatial.erb +31 -0
  199. data/lib/capybara/spec/views/tables.erb +67 -0
  200. data/lib/capybara/spec/views/with_animation.erb +39 -4
  201. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  202. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  203. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  204. data/lib/capybara/spec/views/with_hover.erb +3 -2
  205. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  206. data/lib/capybara/spec/views/with_html.erb +34 -6
  207. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  208. data/lib/capybara/spec/views/with_js.erb +7 -4
  209. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  210. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  211. data/lib/capybara/spec/views/with_scope.erb +2 -2
  212. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  213. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  214. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  215. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  216. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  217. data/lib/capybara/spec/views/with_windows.erb +1 -1
  218. data/lib/capybara/spec/views/within_frames.erb +1 -1
  219. data/lib/capybara/version.rb +1 -1
  220. data/lib/capybara/window.rb +14 -18
  221. data/lib/capybara.rb +91 -126
  222. data/spec/basic_node_spec.rb +30 -16
  223. data/spec/capybara_spec.rb +40 -28
  224. data/spec/counter_spec.rb +35 -0
  225. data/spec/css_builder_spec.rb +3 -1
  226. data/spec/css_splitter_spec.rb +1 -1
  227. data/spec/dsl_spec.rb +33 -22
  228. data/spec/filter_set_spec.rb +5 -5
  229. data/spec/fixtures/selenium_driver_rspec_failure.rb +3 -3
  230. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  231. data/spec/minitest_spec.rb +24 -2
  232. data/spec/minitest_spec_spec.rb +60 -45
  233. data/spec/per_session_config_spec.rb +1 -1
  234. data/spec/rack_test_spec.rb +131 -98
  235. data/spec/regexp_dissassembler_spec.rb +53 -39
  236. data/spec/result_spec.rb +68 -66
  237. data/spec/rspec/features_spec.rb +9 -4
  238. data/spec/rspec/scenarios_spec.rb +6 -2
  239. data/spec/rspec/shared_spec_matchers.rb +137 -98
  240. data/spec/rspec_matchers_spec.rb +25 -0
  241. data/spec/rspec_spec.rb +23 -21
  242. data/spec/sauce_spec_chrome.rb +43 -0
  243. data/spec/selector_spec.rb +77 -21
  244. data/spec/selenium_spec_chrome.rb +141 -39
  245. data/spec/selenium_spec_chrome_remote.rb +32 -17
  246. data/spec/selenium_spec_edge.rb +36 -8
  247. data/spec/selenium_spec_firefox.rb +110 -68
  248. data/spec/selenium_spec_firefox_remote.rb +22 -15
  249. data/spec/selenium_spec_ie.rb +29 -22
  250. data/spec/selenium_spec_safari.rb +162 -0
  251. data/spec/server_spec.rb +153 -81
  252. data/spec/session_spec.rb +11 -4
  253. data/spec/shared_selenium_node.rb +79 -0
  254. data/spec/shared_selenium_session.rb +179 -74
  255. data/spec/spec_helper.rb +80 -5
  256. data/spec/whitespace_normalizer_spec.rb +54 -0
  257. data/spec/xpath_builder_spec.rb +3 -1
  258. metadata +218 -30
  259. data/lib/capybara/spec/session/source_spec.rb +0 -0
  260. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -3,6 +3,25 @@
3
3
  require 'capybara/selenium/nodes/firefox_node'
4
4
 
5
5
  module Capybara::Selenium::Driver::FirefoxDriver
6
+ def self.extended(driver)
7
+ driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver
8
+ bridge = driver.send(:bridge)
9
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
10
+ end
11
+ end
12
+
13
+ module Capybara::Selenium::Driver::W3CFirefoxDriver
14
+ class << self
15
+ def extended(driver)
16
+ require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(driver.browser)
17
+ driver.options[:native_displayed] = false if driver.options[:native_displayed].nil?
18
+ end
19
+
20
+ def pause_broken?(sel_driver)
21
+ sel_driver.capabilities['moz:geckodriverVersion']&.start_with?('0.22.')
22
+ end
23
+ end
24
+
6
25
  def resize_window_to(handle, width, height)
7
26
  within_given_window(handle) do
8
27
  # Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
@@ -18,8 +37,17 @@ module Capybara::Selenium::Driver::FirefoxDriver
18
37
  # Use instance variable directly so we avoid starting the browser just to reset the session
19
38
  return unless @browser
20
39
 
40
+ if browser_version >= 68
41
+ begin
42
+ # Firefox 68 hangs if we try to switch windows while a modal is visible
43
+ browser.switch_to.alert&.dismiss
44
+ rescue Selenium::WebDriver::Error::NoSuchAlertError
45
+ # Swallow
46
+ end
47
+ end
48
+
21
49
  switch_to_window(window_handles.first)
22
- window_handles.slice(1..-1).each { |win| close_window(win) }
50
+ window_handles.slice(1..).each { |win| close_window(win) }
23
51
  super
24
52
  end
25
53
 
@@ -28,7 +56,7 @@ module Capybara::Selenium::Driver::FirefoxDriver
28
56
  accept_modal :confirm, wait: 0.1 do
29
57
  super
30
58
  end
31
- rescue Capybara::ModalNotFound # rubocop:disable Lint/HandleExceptions
59
+ rescue Capybara::ModalNotFound
32
60
  # No modal was opened - page has refreshed - ignore
33
61
  end
34
62
 
@@ -39,7 +67,7 @@ module Capybara::Selenium::Driver::FirefoxDriver
39
67
  # so we have to move to the default_content and iterate back through the frames
40
68
  handles = @frame_handles[current_window_handle]
41
69
  browser.switch_to.default_content
42
- handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
70
+ handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
43
71
  end
44
72
 
45
73
  private
@@ -47,4 +75,10 @@ private
47
75
  def build_node(native_node, initial_cache = {})
48
76
  ::Capybara::Selenium::FirefoxNode.new(self, native_node, initial_cache)
49
77
  end
78
+
79
+ def browser_version
80
+ browser.capabilities[:browser_version].to_f
81
+ end
50
82
  end
83
+
84
+ Capybara::Selenium::Driver.register_specialization :firefox, Capybara::Selenium::Driver::FirefoxDriver
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/selenium/nodes/ie_node'
4
+
3
5
  module Capybara::Selenium::Driver::InternetExplorerDriver
4
6
  def switch_to_frame(frame)
5
7
  return super unless frame == :parent
@@ -8,6 +10,17 @@ module Capybara::Selenium::Driver::InternetExplorerDriver
8
10
  # so we have to move to the default_content and iterate back through the frames
9
11
  handles = @frame_handles[current_window_handle]
10
12
  browser.switch_to.default_content
11
- handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
13
+ handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
14
+ end
15
+
16
+ private
17
+
18
+ def build_node(native_node, initial_cache = {})
19
+ ::Capybara::Selenium::IENode.new(self, native_node, initial_cache)
12
20
  end
13
21
  end
22
+
23
+ module Capybara::Selenium
24
+ Driver.register_specialization :ie, Driver::InternetExplorerDriver
25
+ Driver.register_specialization :internet_explorer, Driver::InternetExplorerDriver
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selenium/nodes/safari_node'
4
+
5
+ module Capybara::Selenium::Driver::SafariDriver
6
+ def switch_to_frame(frame)
7
+ return super unless frame == :parent
8
+
9
+ # safaridriver/safari has an issue where switch_to_frame(:parent)
10
+ # behaves like switch_to_frame(:top)
11
+ handles = @frame_handles[current_window_handle]
12
+ browser.switch_to.default_content
13
+ handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh.native) }
14
+ end
15
+
16
+ private
17
+
18
+ def build_node(native_node, initial_cache = {})
19
+ ::Capybara::Selenium::SafariNode.new(self, native_node, initial_cache)
20
+ end
21
+ end
22
+
23
+ Capybara::Selenium::Driver.register_specialization(/^(safari|Safari_Technology_Preview)$/,
24
+ Capybara::Selenium::Driver::SafariDriver)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Capybara::Selenium::Node
4
+ module FileInputClickEmulation
5
+ def click(keys = [], **options)
6
+ super
7
+ rescue Selenium::WebDriver::Error::InvalidArgumentError
8
+ return emulate_click if attaching_file? && visible_file_field?
9
+
10
+ raise
11
+ end
12
+
13
+ private
14
+
15
+ def visible_file_field?
16
+ (attrs(:tagName, :type).map { |val| val&.downcase } == %w[input file]) && visible?
17
+ end
18
+
19
+ def attaching_file?
20
+ caller_locations.any? { |cl| cl.base_label == 'attach_file' }
21
+ end
22
+
23
+ def emulate_click
24
+ driver.execute_script(<<~JS, self)
25
+ arguments[0].dispatchEvent(
26
+ new MouseEvent('click', {
27
+ view: window,
28
+ bubbles: true,
29
+ cancelable: true
30
+ }));
31
+ JS
32
+ end
33
+ end
34
+ end
@@ -3,66 +3,47 @@
3
3
  module Capybara
4
4
  module Selenium
5
5
  module Find
6
- def find_xpath(selector, uses_visibility: false, styles: nil, **_options)
7
- find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles)
6
+ def find_xpath(selector, uses_visibility: false, styles: nil, position: false, **_options)
7
+ find_by(:xpath, selector, uses_visibility: uses_visibility, texts: [], styles: styles, position: position)
8
8
  end
9
9
 
10
- def find_css(selector, uses_visibility: false, texts: [], styles: nil, **_options)
11
- find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles)
10
+ def find_css(selector, uses_visibility: false, texts: [], styles: nil, position: false, **_options)
11
+ find_by(:css, selector, uses_visibility: uses_visibility, texts: texts, styles: styles, position: position)
12
12
  end
13
13
 
14
14
  private
15
15
 
16
- def find_by(format, selector, uses_visibility:, texts:, styles:)
16
+ def find_by(format, selector, uses_visibility:, texts:, styles:, position:)
17
17
  els = find_context.find_elements(format, selector)
18
18
  hints = []
19
19
 
20
20
  if (els.size > 2) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
21
21
  els = filter_by_text(els, texts) unless texts.empty?
22
-
23
- hints_js = +''
24
- functions = []
25
- if uses_visibility && !is_displayed_atom.empty?
26
- hints_js << <<~VISIBILITY_JS
27
- var vis_func = #{is_displayed_atom};
28
- VISIBILITY_JS
29
- functions << 'vis_func'
30
- end
31
-
32
- if styles.is_a? Hash
33
- hints_js << <<~STYLE_JS
34
- var style_func = function(el){
35
- var el_styles = window.getComputedStyle(el);
36
- return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
37
- res[style] = el_styles[style];
38
- return res;
39
- }, {});
40
- };
41
- STYLE_JS
42
- functions << 'style_func'
43
- end
44
-
45
- unless functions.empty?
46
- hints_js << <<~EACH_JS
47
- return arguments[0].map(function(el){
48
- return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) }); });
49
- EACH_JS
50
-
51
- hints = es_context.execute_script hints_js, els
52
- hints.map! do |results|
53
- result = {}
54
- result[:style] = results.pop if styles.is_a? Hash
55
- result[:visible] = results.pop if uses_visibility
56
- result
57
- end
58
- end
22
+ hints = gather_hints(els, uses_visibility: uses_visibility, styles: styles, position: position)
59
23
  end
60
24
  els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
61
25
  end
62
26
 
27
+ def gather_hints(elements, uses_visibility:, styles:, position:)
28
+ hints_js, functions = build_hints_js(uses_visibility, styles, position)
29
+ return [] unless functions.any?
30
+
31
+ (es_context.execute_script(hints_js, elements) || []).map! do |results|
32
+ hint = {}
33
+ hint[:style] = results.pop if functions.include?(:style_func)
34
+ hint[:position] = results.pop if functions.include?(:position_func)
35
+ hint[:visible] = results.pop if functions.include?(:vis_func)
36
+ hint
37
+ end
38
+ rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
39
+ ::Capybara::NotSupportedByDriverError
40
+ # warn 'Unexpected Stale Element Error - skipping optimization'
41
+ []
42
+ end
43
+
63
44
  def filter_by_text(elements, texts)
64
45
  es_context.execute_script <<~JS, elements, texts
65
- var texts = arguments[1]
46
+ var texts = arguments[1];
66
47
  return arguments[0].filter(function(el){
67
48
  var content = el.textContent.toLowerCase();
68
49
  return texts.every(function(txt){ return content.indexOf(txt.toLowerCase()) != -1 });
@@ -70,16 +51,58 @@ module Capybara
70
51
  JS
71
52
  end
72
53
 
54
+ def build_hints_js(uses_visibility, styles, position)
55
+ functions = []
56
+ hints_js = +''
57
+
58
+ if uses_visibility && !is_displayed_atom.empty?
59
+ hints_js << <<~VISIBILITY_JS
60
+ var vis_func = #{is_displayed_atom};
61
+ VISIBILITY_JS
62
+ functions << :vis_func
63
+ end
64
+
65
+ if position
66
+ hints_js << <<~POSITION_JS
67
+ var position_func = function(el){
68
+ return el.getBoundingClientRect();
69
+ };
70
+ POSITION_JS
71
+ functions << :position_func
72
+ end
73
+
74
+ if styles.is_a? Hash
75
+ hints_js << <<~STYLE_JS
76
+ var style_func = function(el){
77
+ var el_styles = window.getComputedStyle(el);
78
+ return #{styles.keys.map(&:to_s)}.reduce(function(res, style){
79
+ res[style] = el_styles[style];
80
+ return res;
81
+ }, {});
82
+ };
83
+ STYLE_JS
84
+ functions << :style_func
85
+ end
86
+
87
+ hints_js << <<~EACH_JS
88
+ return arguments[0].map(function(el){
89
+ return [#{functions.join(',')}].map(function(fn){ return fn.call(null, el) });
90
+ });
91
+ EACH_JS
92
+
93
+ [hints_js, functions]
94
+ end
95
+
73
96
  def es_context
74
97
  respond_to?(:execute_script) ? self : driver
75
98
  end
76
99
 
77
100
  def is_displayed_atom # rubocop:disable Naming/PredicateName
78
- @@is_displayed_atom ||= begin
101
+ @@is_displayed_atom ||= begin # rubocop:disable Style/ClassVars
79
102
  browser.send(:bridge).send(:read_atom, 'isDisplayed')
80
103
  rescue StandardError
81
104
  # If the atom doesn't exist or other error
82
- ""
105
+ ''
83
106
  end
84
107
  end
85
108
  end
@@ -2,37 +2,215 @@
2
2
 
3
3
  class Capybara::Selenium::Node
4
4
  module Html5Drag
5
- private
5
+ # Implement methods to emulate HTML5 drag and drop
6
+
7
+ def drag_to(element, html5: nil, delay: 0.05, drop_modifiers: [])
8
+ drop_modifiers = Array(drop_modifiers)
6
9
 
7
- def html5_drag_to(element)
8
10
  driver.execute_script MOUSEDOWN_TRACKER
9
11
  scroll_if_needed { browser_action.click_and_hold(native).perform }
10
- if driver.evaluate_script('window.capybara_mousedown_prevented')
11
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
12
+ html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
13
+ if html5
14
+ perform_html5_drag(element, delay, drop_modifiers)
12
15
  else
13
- driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
14
- browser_action.release.perform
16
+ perform_legacy_drag(element, drop_modifiers)
15
17
  end
16
18
  end
17
19
 
18
- def html5_draggable?
19
- # Workaround https://github.com/SeleniumHQ/selenium/issues/6396
20
- native.property('draggable')
20
+ private
21
+
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
29
+ end
30
+
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)
33
+ browser_action.release.perform
34
+ end
35
+
36
+ def html5_drop(*args)
37
+ if args[0].is_a? String
38
+ input = driver.evaluate_script ATTACH_FILE
39
+ input.set_file(args)
40
+ driver.execute_script DROP_FILE, self, input
41
+ else
42
+ items = args.flat_map do |arg|
43
+ arg.map { |(type, data)| { type: type, data: data } }
44
+ end
45
+ driver.execute_script DROP_STRING, items, self
46
+ end
21
47
  end
22
48
 
49
+ DROP_STRING = <<~JS
50
+ var strings = arguments[0],
51
+ el = arguments[1],
52
+ dt = new DataTransfer(),
53
+ opts = { cancelable: true, bubbles: true, dataTransfer: dt };
54
+ for (var i=0; i < strings.length; i++){
55
+ if (dt.items) {
56
+ dt.items.add(strings[i]['data'], strings[i]['type']);
57
+ } else {
58
+ dt.setData(strings[i]['type'], strings[i]['data']);
59
+ }
60
+ }
61
+ var dropEvent = new DragEvent('drop', opts);
62
+ el.dispatchEvent(dropEvent);
63
+ JS
64
+
65
+ DROP_FILE = <<~JS
66
+ var el = arguments[0],
67
+ input = arguments[1],
68
+ files = input.files,
69
+ dt = new DataTransfer(),
70
+ opts = { cancelable: true, bubbles: true, dataTransfer: dt };
71
+ input.parentElement.removeChild(input);
72
+ if (dt.items){
73
+ for (var i=0; i<files.length; i++){
74
+ dt.items.add(files[i]);
75
+ }
76
+ } else {
77
+ Object.defineProperty(dt, "files", {
78
+ value: files,
79
+ writable: false
80
+ });
81
+ }
82
+ var dropEvent = new DragEvent('drop', opts);
83
+ el.dispatchEvent(dropEvent);
84
+ JS
85
+
86
+ ATTACH_FILE = <<~JS
87
+ (function(){
88
+ var input = document.createElement('INPUT');
89
+ input.type = "file";
90
+ input.id = "_capybara_drop_file";
91
+ input.multiple = true;
92
+ document.body.appendChild(input);
93
+ return input;
94
+ })()
95
+ JS
96
+
23
97
  MOUSEDOWN_TRACKER = <<~JS
98
+ window.capybara_mousedown_prevented = null;
24
99
  document.addEventListener('mousedown', ev => {
25
100
  window.capybara_mousedown_prevented = ev.defaultPrevented;
26
101
  }, { once: true, passive: true })
27
102
  JS
28
103
 
104
+ LEGACY_DRAG_CHECK = <<~JS
105
+ (function(el){
106
+ if ([true, null].indexOf(window.capybara_mousedown_prevented) >= 0){
107
+ return true;
108
+ }
109
+
110
+ do {
111
+ if (el.draggable) return false;
112
+ } while (el = el.parentElement );
113
+ return true;
114
+ })(arguments[0])
115
+ JS
116
+
29
117
  HTML5_DRAG_DROP_SCRIPT = <<~JS
30
- var source = arguments[0];
31
- var target = arguments[1];
118
+ function rectCenter(rect){
119
+ return new DOMPoint(
120
+ (rect.left + rect.right)/2,
121
+ (rect.top + rect.bottom)/2
122
+ );
123
+ }
124
+
125
+ function pointOnRect(pt, rect) {
126
+ var rectPt = rectCenter(rect);
127
+ var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);
128
+
129
+ if (pt.x <= rectPt.x) { // left side
130
+ var minXy = slope * (rect.left - pt.x) + pt.y;
131
+ if (rect.top <= minXy && minXy <= rect.bottom)
132
+ return new DOMPoint(rect.left, minXy);
133
+ }
134
+
135
+ if (pt.x >= rectPt.x) { // right side
136
+ var maxXy = slope * (rect.right - pt.x) + pt.y;
137
+ if (rect.top <= maxXy && maxXy <= rect.bottom)
138
+ return new DOMPoint(rect.right, maxXy);
139
+ }
140
+
141
+ if (pt.y <= rectPt.y) { // top side
142
+ var minYx = (rectPt.top - pt.y) / slope + pt.x;
143
+ if (rect.left <= minYx && minYx <= rect.right)
144
+ return new DOMPoint(minYx, rect.top);
145
+ }
146
+
147
+ if (pt.y >= rectPt.y) { // bottom side
148
+ var maxYx = (rect.bottom - pt.y) / slope + pt.x;
149
+ if (rect.left <= maxYx && maxYx <= rect.right)
150
+ return new DOMPoint(maxYx, rect.bottom);
151
+ }
152
+
153
+ return new DOMPoint(pt.x,pt.y);
154
+ }
155
+
156
+ function dragEnterTarget() {
157
+ target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
158
+ var targetRect = target.getBoundingClientRect();
159
+ var sourceCenter = rectCenter(source.getBoundingClientRect());
160
+
161
+ for (var i = 0; i < drop_modifier_keys.length; i++) {
162
+ key = drop_modifier_keys[i];
163
+ if (key == "control"){
164
+ key = "ctrl"
165
+ }
166
+ opts[key + 'Key'] = true;
167
+ }
168
+
169
+ var dragEnterEvent = new DragEvent('dragenter', opts);
170
+ target.dispatchEvent(dragEnterEvent);
171
+
172
+ // fire 2 dragover events to simulate dragging with a direction
173
+ var entryPoint = pointOnRect(sourceCenter, targetRect)
174
+ var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
175
+ var dragOverEvent = new DragEvent('dragover', dragOverOpts);
176
+ target.dispatchEvent(dragOverEvent);
177
+ window.setTimeout(dragOnTarget, step_delay);
178
+ }
179
+
180
+ function dragOnTarget() {
181
+ var targetCenter = rectCenter(target.getBoundingClientRect());
182
+ var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
183
+ var dragOverEvent = new DragEvent('dragover', dragOverOpts);
184
+ target.dispatchEvent(dragOverEvent);
185
+ window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
186
+ }
187
+
188
+ function dragLeave(drop, dragOverOpts) {
189
+ var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
190
+ var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
191
+ target.dispatchEvent(dragLeaveEvent);
192
+ if (drop) {
193
+ var dropEvent = new DragEvent('drop', dragLeaveOptions);
194
+ target.dispatchEvent(dropEvent);
195
+ }
196
+ var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
197
+ source.dispatchEvent(dragEndEvent);
198
+ callback.call(true);
199
+ }
200
+
201
+ var source = arguments[0],
202
+ target = arguments[1],
203
+ step_delay = arguments[2],
204
+ drop_modifier_keys = arguments[3],
205
+ callback = arguments[4];
32
206
 
33
207
  var dt = new DataTransfer();
34
208
  var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
35
209
 
210
+ while (source && !source.draggable) {
211
+ source = source.parentElement;
212
+ }
213
+
36
214
  if (source.tagName == 'A'){
37
215
  dt.setData('text/uri-list', source.href);
38
216
  dt.setData('text', source.href);
@@ -41,19 +219,11 @@ class Capybara::Selenium::Node
41
219
  dt.setData('text/uri-list', source.src);
42
220
  dt.setData('text', source.src);
43
221
  }
222
+
44
223
  var dragEvent = new DragEvent('dragstart', opts);
45
224
  source.dispatchEvent(dragEvent);
46
- target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
47
- var dragOverEvent = new DragEvent('dragover', opts);
48
- target.dispatchEvent(dragOverEvent);
49
- var dragLeaveEvent = new DragEvent('dragleave', opts);
50
- target.dispatchEvent(dragLeaveEvent);
51
- if (dragOverEvent.defaultPrevented) {
52
- var dropEvent = new DragEvent('drop', opts);
53
- target.dispatchEvent(dropEvent);
54
- }
55
- var dragEndEvent = new DragEvent('dragend', opts);
56
- source.dispatchEvent(dragEndEvent);
225
+
226
+ window.setTimeout(dragEnterTarget, step_delay);
57
227
  JS
58
228
  end
59
229
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Capybara::Selenium::Node
4
+ #
5
+ # @api private
6
+ #
7
+ class ModifierKeysStack
8
+ def initialize
9
+ @stack = []
10
+ end
11
+
12
+ def include?(key)
13
+ @stack.flatten.include?(key)
14
+ end
15
+
16
+ def press(key)
17
+ @stack.last.push(key)
18
+ end
19
+
20
+ def push
21
+ @stack.push []
22
+ end
23
+
24
+ def pop
25
+ @stack.pop
26
+ end
27
+ end
28
+ end
@@ -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