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
@@ -4,22 +4,22 @@
4
4
 
5
5
  require 'capybara/selenium/extensions/find'
6
6
  require 'capybara/selenium/extensions/scroll'
7
+ require 'capybara/node/whitespace_normalizer'
7
8
 
8
9
  class Capybara::Selenium::Node < Capybara::Driver::Node
10
+ include Capybara::Node::WhitespaceNormalizer
9
11
  include Capybara::Selenium::Find
10
12
  include Capybara::Selenium::Scroll
11
13
 
12
14
  def visible_text
15
+ raise NotImplementedError, 'Getting visible text is not currently supported directly on shadow roots' if shadow_root?
16
+
13
17
  native.text
14
18
  end
15
19
 
16
20
  def all_text
17
- text = driver.evaluate_script('arguments[0].textContent', self)
18
- text.gsub(/[\u200b\u200e\u200f]/, '')
19
- .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
20
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
21
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
22
- .tr("\u00a0", ' ')
21
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
22
+ normalize_spacing(text)
23
23
  end
24
24
 
25
25
  def [](name)
@@ -37,9 +37,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
37
37
  end
38
38
 
39
39
  def style(styles)
40
- styles.each_with_object({}) do |style, result|
41
- result[style] = native.css_value(style)
42
- end
40
+ styles.to_h { |style| [style, native.css_value(style)] }
43
41
  end
44
42
 
45
43
  ##
@@ -53,12 +51,22 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
53
51
  # :none => append the new value to the existing value <br/>
54
52
  # :backspace => send backspace keystrokes to clear the field <br/>
55
53
  # Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
54
+ # @option options [Boolean] :rapid (nil) Whether setting text inputs should use a faster &quot;rapid&quot; mode<br/>
55
+ # nil => Text inputs with length greater than 30 characters will be set using a faster driver script mode<br/>
56
+ # true => Rapid mode will be used regardless of input length<br/>
57
+ # false => Sends keys via conventional mode. This may be required to avoid losing key-presses if you have certain
58
+ # Javascript interactions on form inputs<br/>
56
59
  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?
60
+ if value.is_a?(Array) && !multiple?
61
+ raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
62
+ end
63
+
64
+ tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
65
+ @tag_name ||= tag_name
58
66
 
59
67
  case tag_name
60
68
  when 'input'
61
- case self[:type]
69
+ case type
62
70
  when 'radio'
63
71
  click
64
72
  when 'checkbox'
@@ -71,13 +79,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
71
79
  set_time(value)
72
80
  when 'datetime-local'
73
81
  set_datetime_local(value)
82
+ when 'color'
83
+ set_color(value)
84
+ when 'range'
85
+ set_range(value)
74
86
  else
75
- set_text(value, options)
87
+ set_text(value, **options)
76
88
  end
77
89
  when 'textarea'
78
- set_text(value, options)
90
+ set_text(value, **options)
79
91
  else
80
- set_content_editable(value) if content_editable?
92
+ set_content_editable(value)
81
93
  end
82
94
  end
83
95
 
@@ -95,26 +107,45 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
95
107
  click_options = ClickOptions.new(keys, options)
96
108
  return native.click if click_options.empty?
97
109
 
98
- click_with_options(click_options)
99
- rescue StandardError => err
100
- if err.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
101
- err.message =~ /Other element would receive the click/
110
+ perform_with_options(click_options) do |action|
111
+ target = click_options.coords? ? nil : native
112
+ if click_options.delay.zero?
113
+ action.click(target)
114
+ else
115
+ action.click_and_hold(target)
116
+ action_pause(action, click_options.delay)
117
+ action.release
118
+ end
119
+ end
120
+ rescue StandardError => e
121
+ if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
122
+ e.message.include?('Other element would receive the click')
102
123
  scroll_to_center
103
124
  end
104
125
 
105
- raise err
126
+ raise e
106
127
  end
107
128
 
108
129
  def right_click(keys = [], **options)
109
130
  click_options = ClickOptions.new(keys, options)
110
- click_with_options(click_options) do |action|
111
- click_options.coords? ? action.context_click : action.context_click(native)
131
+ perform_with_options(click_options) do |action|
132
+ target = click_options.coords? ? nil : native
133
+ if click_options.delay.zero?
134
+ action.context_click(target)
135
+ else
136
+ action.move_to(target) if target
137
+ action.pointer_down(:right).then do |act|
138
+ action_pause(act, click_options.delay)
139
+ end.pointer_up(:right)
140
+ end
112
141
  end
113
142
  end
114
143
 
115
144
  def double_click(keys = [], **options)
116
145
  click_options = ClickOptions.new(keys, options)
117
- click_with_options(click_options) do |action|
146
+ raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?
147
+
148
+ perform_with_options(click_options) do |action|
118
149
  click_options.coords? ? action.double_click : action.double_click(native)
119
150
  end
120
151
  end
@@ -127,15 +158,30 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
127
158
  scroll_if_needed { browser_action.move_to(native).perform }
128
159
  end
129
160
 
130
- def drag_to(element)
161
+ def drag_to(element, drop_modifiers: [], **)
162
+ drop_modifiers = Array(drop_modifiers)
131
163
  # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
132
164
  # which means Seleniums `drag_and_drop` is now broken - do it manually
133
165
  scroll_if_needed { browser_action.click_and_hold(native).perform }
134
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
166
+ # element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
167
+ element.scroll_if_needed do
168
+ keys_down = modifiers_down(browser_action, drop_modifiers)
169
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
170
+ keys_up.perform
171
+ end
172
+ end
173
+
174
+ def drop(*_)
175
+ raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
135
176
  end
136
177
 
137
178
  def tag_name
138
- native.tag_name.downcase
179
+ @tag_name ||=
180
+ if native.respond_to? :tag_name
181
+ native.tag_name.downcase
182
+ else
183
+ shadow_root? ? 'ShadowRoot' : 'Unknown'
184
+ end
139
185
  end
140
186
 
141
187
  def visible?; boolean_attr(native.displayed?); end
@@ -148,21 +194,33 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
148
194
  return true unless native.enabled?
149
195
 
150
196
  # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
151
- tag_name == 'fieldset' && find_xpath('ancestor-or-self::fieldset[@disabled]').any?
197
+ find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
152
198
  end
153
199
 
154
200
  def content_editable?
155
- native.attribute('isContentEditable')
156
- end
157
-
158
- def ==(other)
159
- native == other.native
201
+ native.attribute('isContentEditable') == 'true'
160
202
  end
161
203
 
162
204
  def path
163
205
  driver.evaluate_script GET_XPATH_SCRIPT, self
164
206
  end
165
207
 
208
+ def obscured?(x: nil, y: nil)
209
+ res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
210
+ return true if res == true
211
+
212
+ driver.frame_obscured_at?(x: res['x'], y: res['y'])
213
+ end
214
+
215
+ def rect
216
+ native.rect
217
+ end
218
+
219
+ def shadow_root
220
+ root = native.shadow_root
221
+ root && build_node(native.shadow_root)
222
+ end
223
+
166
224
  protected
167
225
 
168
226
  def scroll_if_needed
@@ -172,6 +230,21 @@ protected
172
230
  yield
173
231
  end
174
232
 
233
+ def scroll_to_center
234
+ script = <<-JS
235
+ try {
236
+ arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
237
+ } catch(e) {
238
+ arguments[0].scrollIntoView(true);
239
+ }
240
+ JS
241
+ begin
242
+ driver.execute_script(script, self)
243
+ rescue StandardError
244
+ # Swallow error if scrollIntoView with options isn't supported
245
+ end
246
+ end
247
+
175
248
  private
176
249
 
177
250
  def sibling_index(parent, node, selector)
@@ -197,29 +270,38 @@ private
197
270
  find_xpath(XPath.ancestor(:select)[1]).first
198
271
  end
199
272
 
200
- def set_text(value, clear: nil, **_unused)
273
+ def set_text(value, clear: nil, rapid: nil, **_unused)
201
274
  value = value.to_s
202
275
  if value.empty? && clear.nil?
203
276
  native.clear
204
277
  elsif clear == :backspace
205
278
  # Clear field by sending the correct number of backspace keys.
206
279
  backspaces = [:backspace] * self.value.to_s.length
207
- send_keys(*([:end] + backspaces + [value]))
280
+ send_keys(:end, *backspaces, value)
208
281
  elsif clear.is_a? Array
209
282
  send_keys(*clear, value)
210
283
  else
211
- # Clear field by JavaScript assignment of the value property.
212
- # Script can change a readonly element which user input cannot, so
213
- # don't execute if readonly.
214
- driver.execute_script "arguments[0].value = ''", self unless clear == :none
215
- send_keys(value)
284
+ driver.execute_script 'arguments[0].select()', self unless clear == :none
285
+ if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
286
+ send_keys(value[0..3])
287
+ driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
288
+ send_keys(value[-3..])
289
+ else
290
+ send_keys(value)
291
+ end
216
292
  end
217
293
  end
218
294
 
219
- def click_with_options(click_options)
295
+ def auto_rapid_set_length
296
+ 30
297
+ end
298
+
299
+ def perform_with_options(click_options, &block)
300
+ raise ArgumentError, 'A block must be provided' unless block
301
+
220
302
  scroll_if_needed do
221
303
  action_with_modifiers(click_options) do |action|
222
- if block_given?
304
+ if block
223
305
  yield action
224
306
  else
225
307
  click_options.coords? ? action.click : action.click(native)
@@ -228,21 +310,6 @@ private
228
310
  end
229
311
  end
230
312
 
231
- def scroll_to_center
232
- script = <<-'JS'
233
- try {
234
- arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
235
- } catch(e) {
236
- arguments[0].scrollIntoView(true);
237
- }
238
- JS
239
- begin
240
- driver.execute_script(script, self)
241
- rescue StandardError # rubocop:disable Lint/HandleExceptions
242
- # Swallow error if scrollIntoView with options isn't supported
243
- end
244
- end
245
-
246
313
  def set_date(value) # rubocop:disable Naming/AccessorMethodName
247
314
  value = SettableValue.new(value)
248
315
  return set_text(value) unless value.dateable?
@@ -267,8 +334,17 @@ private
267
334
  update_value_js(value.to_datetime_str)
268
335
  end
269
336
 
337
+ def set_color(value) # rubocop:disable Naming/AccessorMethodName
338
+ update_value_js(value)
339
+ end
340
+
341
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
342
+ update_value_js(value)
343
+ end
344
+
270
345
  def update_value_js(value)
271
346
  driver.execute_script(<<-JS, self, value)
347
+ if (arguments[0].readOnly) { return };
272
348
  if (document.activeElement !== arguments[0]){
273
349
  arguments[0].focus();
274
350
  }
@@ -281,32 +357,75 @@ private
281
357
  end
282
358
 
283
359
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
284
- path_names = value.to_s.empty? ? [] : value
285
- native.send_keys(Array(path_names).join("\n"))
360
+ with_file_detector do
361
+ path_names = value.to_s.empty? ? [] : value
362
+ file_names = Array(path_names).map do |pn|
363
+ Pathname.new(pn).absolute? ? pn : File.expand_path(pn)
364
+ end.join("\n")
365
+ native.send_keys(file_names)
366
+ end
367
+ end
368
+
369
+ def with_file_detector
370
+ if driver.options[:browser] == :remote &&
371
+ bridge.respond_to?(:file_detector) &&
372
+ bridge.file_detector.nil?
373
+ begin
374
+ bridge.file_detector = lambda do |(fn, *)|
375
+ str = fn.to_s
376
+ str if File.exist?(str)
377
+ end
378
+ yield
379
+ ensure
380
+ bridge.file_detector = nil
381
+ end
382
+ else
383
+ yield
384
+ end
286
385
  end
287
386
 
288
387
  def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
289
388
  # Ensure we are focused on the element
290
389
  click
291
390
 
292
- driver.execute_script <<-JS, self
293
- var range = document.createRange();
294
- var sel = window.getSelection();
295
- arguments[0].focus();
296
- range.selectNodeContents(arguments[0]);
297
- sel.removeAllRanges();
298
- sel.addRange(range);
391
+ editable = driver.execute_script <<-JS, self
392
+ if (arguments[0].isContentEditable) {
393
+ var range = document.createRange();
394
+ var sel = window.getSelection();
395
+ arguments[0].focus();
396
+ range.selectNodeContents(arguments[0]);
397
+ sel.removeAllRanges();
398
+ sel.addRange(range);
399
+ return true;
400
+ }
401
+ return false;
299
402
  JS
300
403
 
301
404
  # The action api has a speed problem but both chrome and firefox 58 raise errors
302
405
  # if we use the faster direct send_keys. For now just send_keys to the element
303
406
  # we've already focused.
304
407
  # native.send_keys(value.to_s)
305
- browser_action.send_keys(value.to_s).perform
408
+ browser_action.send_keys(value.to_s).perform if editable
306
409
  end
307
410
 
308
411
  def action_with_modifiers(click_options)
309
- actions = browser_action.move_to(native, *click_options.coords)
412
+ actions = browser_action.tap do |acts|
413
+ if click_options.coords?
414
+ if click_options.center_offset?
415
+ acts.move_to(native, *click_options.coords)
416
+ else
417
+ right_by, down_by = *click_options.coords
418
+ size = native.size
419
+ left_offset = (size[:width] / 2).to_i
420
+ top_offset = (size[:height] / 2).to_i
421
+ left = -left_offset + right_by
422
+ top = -top_offset + down_by
423
+ acts.move_to(native, left, top)
424
+ end
425
+ else
426
+ acts.move_to(native)
427
+ end
428
+ end
310
429
  modifiers_down(actions, click_options.keys)
311
430
  yield actions
312
431
  modifiers_up(actions, click_options.keys)
@@ -318,32 +437,49 @@ private
318
437
 
319
438
  def modifiers_down(actions, keys)
320
439
  each_key(keys) { |key| actions.key_down(key) }
440
+ actions
321
441
  end
322
442
 
323
443
  def modifiers_up(actions, keys)
324
444
  each_key(keys) { |key| actions.key_up(key) }
445
+ actions
325
446
  end
326
447
 
327
448
  def browser
328
449
  driver.browser
329
450
  end
330
451
 
452
+ def bridge
453
+ browser.send(:bridge)
454
+ end
455
+
331
456
  def browser_action
332
457
  browser.action
333
458
  end
334
459
 
335
- def each_key(keys)
336
- keys.each do |key|
337
- key = case key
460
+ def capabilities
461
+ browser.capabilities
462
+ end
463
+
464
+ def action_pause(action, duration)
465
+ action.pause(device: action.pointer_inputs.first, duration: duration)
466
+ end
467
+
468
+ def normalize_keys(keys)
469
+ keys.map do |key|
470
+ case key
338
471
  when :ctrl then :control
339
472
  when :command, :cmd then :meta
340
473
  else
341
474
  key
342
475
  end
343
- yield key
344
476
  end
345
477
  end
346
478
 
479
+ def each_key(keys, &block)
480
+ normalize_keys(keys).each(&block)
481
+ end
482
+
347
483
  def find_context
348
484
  native
349
485
  end
@@ -352,11 +488,36 @@ private
352
488
  self.class.new(driver, native_node, initial_cache)
353
489
  end
354
490
 
491
+ def attrs(*attr_names)
492
+ return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']
493
+
494
+ driver.evaluate_script <<~JS, self, attr_names.map(&:to_s)
495
+ (function(el, names){
496
+ return names.map(function(name){
497
+ return el[name]
498
+ });
499
+ })(arguments[0], arguments[1]);
500
+ JS
501
+ end
502
+
503
+ def native_id
504
+ # Selenium 3 -> 4 changed the return of ref
505
+ type_or_id, id = native.ref
506
+ id || type_or_id
507
+ end
508
+
509
+ def shadow_root?
510
+ defined?(::Selenium::WebDriver::ShadowRoot) && native.is_a?(::Selenium::WebDriver::ShadowRoot)
511
+ end
512
+
355
513
  GET_XPATH_SCRIPT = <<~'JS'
356
514
  (function(el, xml){
357
515
  var xpath = '';
358
516
  var pos, tempitem2;
359
517
 
518
+ if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
519
+ return "(: Shadow DOM element - no XPath :)";
520
+ };
360
521
  while(el !== xml.documentElement) {
361
522
  pos = 0;
362
523
  tempitem2 = el;
@@ -381,6 +542,33 @@ private
381
542
  })(arguments[0], document)
382
543
  JS
383
544
 
545
+ OBSCURED_OR_OFFSET_SCRIPT = <<~JS
546
+ (function(el, x, y) {
547
+ var box = el.getBoundingClientRect();
548
+ if (x == null) x = box.width/2;
549
+ if (y == null) y = box.height/2 ;
550
+
551
+ var px = box.left + x,
552
+ py = box.top + y,
553
+ e = document.elementFromPoint(px, py);
554
+
555
+ if (!el.contains(e))
556
+ return true;
557
+
558
+ return { x: px, y: py };
559
+ })(arguments[0], arguments[1], arguments[2])
560
+ JS
561
+
562
+ RAPID_APPEND_TEXT = <<~JS
563
+ (function(el, value) {
564
+ value = el.value + value;
565
+ if (el.maxLength && el.maxLength != -1){
566
+ value = value.slice(0, el.maxLength);
567
+ }
568
+ el.value = value;
569
+ })(arguments[0], arguments[1])
570
+ JS
571
+
384
572
  # SettableValue encapsulates time/date field formatting
385
573
  class SettableValue
386
574
  attr_reader :value
@@ -398,7 +586,7 @@ private
398
586
  end
399
587
 
400
588
  def to_date_str
401
- value.to_date.strftime('%Y-%m-%d')
589
+ value.to_date.iso8601
402
590
  end
403
591
 
404
592
  def timeable?
@@ -432,8 +620,16 @@ private
432
620
  [options[:x], options[:y]]
433
621
  end
434
622
 
623
+ def center_offset?
624
+ options[:offset] == :center
625
+ end
626
+
435
627
  def empty?
436
- keys.empty? && !coords?
628
+ keys.empty? && !coords? && delay.zero?
629
+ end
630
+
631
+ def delay
632
+ options[:delay] || 0
437
633
  end
438
634
  end
439
635
  private_constant :ClickOptions
@@ -1,29 +1,125 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/file_input_click_emulation'
4
5
 
5
6
  class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
6
7
  include Html5Drag
8
+ include FileInputClickEmulation
9
+
10
+ def set_text(value, clear: nil, **_unused)
11
+ super.tap do
12
+ # React doesn't see the chromedriver element clear
13
+ send_keys(:space, :backspace) if value.to_s.empty? && clear.nil?
14
+ end
15
+ end
7
16
 
8
17
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
9
- super(value)
10
- rescue ::Selenium::WebDriver::Error::ExpectedError => err
11
- if err.message =~ /File not found : .+\n.+/m
12
- raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload"
18
+ # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
19
+ if browser_version >= 75.0
20
+ driver.execute_script(<<~JS, self)
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
+ arguments[0].value = null;
23
+ }
24
+ JS
25
+ end
26
+ super
27
+ end
28
+
29
+ def drop(*args)
30
+ html5_drop(*args)
31
+ end
32
+
33
+ def click(*, **)
34
+ super
35
+ rescue ::Selenium::WebDriver::Error::ElementClickInterceptedError
36
+ raise
37
+ rescue ::Selenium::WebDriver::Error::WebDriverError => e
38
+ # chromedriver 74 (at least on mac) raises the wrong error for this
39
+ if e.message.include?('element click intercepted')
40
+ raise ::Selenium::WebDriver::Error::ElementClickInterceptedError, e.message
13
41
  end
14
42
 
15
43
  raise
16
44
  end
17
45
 
18
- def drag_to(element)
19
- return super unless html5_draggable?
46
+ def disabled?
47
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
48
+ end
20
49
 
21
- html5_drag_to(element)
50
+ def select_option
51
+ # To optimize to only one check and then click
52
+ selected_or_disabled = driver.evaluate_script(<<~JS, self)
53
+ arguments[0].matches(':disabled, select:disabled *, :checked')
54
+ JS
55
+ click unless selected_or_disabled
56
+ end
57
+
58
+ def visible?
59
+ return super unless native_displayed?
60
+
61
+ begin
62
+ bridge.send(:execute, :is_element_displayed, id: native_id)
63
+ rescue Selenium::WebDriver::Error::UnknownCommandError
64
+ # If the is_element_displayed command is unknown, no point in trying again
65
+ driver.options[:native_displayed] = false
66
+ super
67
+ end
68
+ end
69
+
70
+ def send_keys(*args)
71
+ args.chunk { |inp| inp.is_a?(String) && inp.match?(/\p{Emoji Presentation}/) }
72
+ .each do |contains_emoji, inputs|
73
+ if contains_emoji
74
+ inputs.join.grapheme_clusters.chunk { |gc| gc.match?(/\p{Emoji Presentation}/) }
75
+ .each do |emoji, clusters|
76
+ if emoji
77
+ driver.send(:execute_cdp, 'Input.insertText', text: clusters.join)
78
+ else
79
+ super(clusters.join)
80
+ end
81
+ end
82
+ else
83
+ super(*inputs)
84
+ end
85
+ end
22
86
  end
23
87
 
24
88
  private
25
89
 
26
- def bridge
27
- driver.browser.send(:bridge)
90
+ def perform_legacy_drag(element, drop_modifiers)
91
+ return super if chromedriver_fixed_actions_key_state? || element.obscured?
92
+
93
+ raise ArgumentError, 'Modifier keys are not supported while dragging in this version of Chrome.' unless drop_modifiers.empty?
94
+
95
+ # W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
96
+ # https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
97
+ browser_action.release.perform
98
+ browser_action.click_and_hold(native).move_to(element.native).release.perform
99
+ end
100
+
101
+ def browser_version(to_float: true)
102
+ caps = capabilities
103
+ ver = caps[:browser_version] || caps[:version]
104
+ ver = ver.to_f if to_float
105
+ ver
106
+ end
107
+
108
+ def chromedriver_fixed_actions_key_state?
109
+ Gem::Requirement.new('>= 76.0.3809.68').satisfied_by?(chromedriver_version)
110
+ end
111
+
112
+ def chromedriver_supports_displayed_endpoint?
113
+ Gem::Requirement.new('>= 76.0.3809.25').satisfied_by?(chromedriver_version)
114
+ end
115
+
116
+ def chromedriver_version
117
+ Gem::Version.new(capabilities['chrome']['chromedriverVersion'].split(' ')[0]) # rubocop:disable Style/RedundantArgument
118
+ end
119
+
120
+ def native_displayed?
121
+ (driver.options[:native_displayed] != false) &&
122
+ chromedriver_supports_displayed_endpoint? &&
123
+ (!ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'])
28
124
  end
29
125
  end