capybara 3.3.0 → 3.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (308) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +803 -13
  4. data/License.txt +1 -1
  5. data/README.md +257 -84
  6. data/lib/capybara/config.rb +25 -9
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +17 -3
  9. data/lib/capybara/driver/node.rb +31 -6
  10. data/lib/capybara/dsl.rb +9 -7
  11. data/lib/capybara/helpers.rb +31 -7
  12. data/lib/capybara/minitest/spec.rb +180 -88
  13. data/lib/capybara/minitest.rb +262 -149
  14. data/lib/capybara/node/actions.rb +202 -116
  15. data/lib/capybara/node/base.rb +34 -19
  16. data/lib/capybara/node/document.rb +14 -2
  17. data/lib/capybara/node/document_matchers.rb +10 -12
  18. data/lib/capybara/node/element.rb +269 -115
  19. data/lib/capybara/node/finders.rb +99 -77
  20. data/lib/capybara/node/matchers.rb +327 -151
  21. data/lib/capybara/node/simple.rb +48 -13
  22. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  23. data/lib/capybara/queries/active_element_query.rb +18 -0
  24. data/lib/capybara/queries/ancestor_query.rb +8 -9
  25. data/lib/capybara/queries/base_query.rb +23 -16
  26. data/lib/capybara/queries/current_path_query.rb +16 -6
  27. data/lib/capybara/queries/match_query.rb +1 -0
  28. data/lib/capybara/queries/selector_query.rb +587 -130
  29. data/lib/capybara/queries/sibling_query.rb +8 -6
  30. data/lib/capybara/queries/style_query.rb +6 -2
  31. data/lib/capybara/queries/text_query.rb +28 -14
  32. data/lib/capybara/queries/title_query.rb +2 -2
  33. data/lib/capybara/rack_test/browser.rb +92 -25
  34. data/lib/capybara/rack_test/driver.rb +16 -7
  35. data/lib/capybara/rack_test/errors.rb +6 -0
  36. data/lib/capybara/rack_test/form.rb +68 -41
  37. data/lib/capybara/rack_test/node.rb +106 -39
  38. data/lib/capybara/rails.rb +1 -1
  39. data/lib/capybara/registration_container.rb +41 -0
  40. data/lib/capybara/registrations/drivers.rb +42 -0
  41. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  42. data/lib/capybara/registrations/servers.rb +66 -0
  43. data/lib/capybara/result.rb +75 -52
  44. data/lib/capybara/rspec/features.rb +7 -7
  45. data/lib/capybara/rspec/matcher_proxies.rb +39 -18
  46. data/lib/capybara/rspec/matchers/base.rb +113 -0
  47. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  48. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  49. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  50. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  51. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  52. data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
  53. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  54. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  55. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  56. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  57. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  58. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  59. data/lib/capybara/rspec/matchers.rb +141 -339
  60. data/lib/capybara/rspec.rb +2 -0
  61. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  62. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  63. data/lib/capybara/selector/css.rb +27 -25
  64. data/lib/capybara/selector/definition/button.rb +68 -0
  65. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  66. data/lib/capybara/selector/definition/css.rb +10 -0
  67. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  68. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  69. data/lib/capybara/selector/definition/element.rb +28 -0
  70. data/lib/capybara/selector/definition/field.rb +40 -0
  71. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  72. data/lib/capybara/selector/definition/file_field.rb +13 -0
  73. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  74. data/lib/capybara/selector/definition/frame.rb +17 -0
  75. data/lib/capybara/selector/definition/id.rb +6 -0
  76. data/lib/capybara/selector/definition/label.rb +62 -0
  77. data/lib/capybara/selector/definition/link.rb +55 -0
  78. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  79. data/lib/capybara/selector/definition/option.rb +27 -0
  80. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  81. data/lib/capybara/selector/definition/select.rb +81 -0
  82. data/lib/capybara/selector/definition/table.rb +109 -0
  83. data/lib/capybara/selector/definition/table_row.rb +21 -0
  84. data/lib/capybara/selector/definition/xpath.rb +5 -0
  85. data/lib/capybara/selector/definition.rb +280 -0
  86. data/lib/capybara/selector/filter.rb +1 -0
  87. data/lib/capybara/selector/filter_set.rb +73 -25
  88. data/lib/capybara/selector/filters/base.rb +24 -5
  89. data/lib/capybara/selector/filters/expression_filter.rb +3 -3
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +16 -2
  92. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  93. data/lib/capybara/selector/selector.rb +85 -348
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +474 -447
  96. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  97. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  98. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  99. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  100. data/lib/capybara/selenium/driver.rb +255 -143
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +93 -11
  102. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  103. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +84 -0
  104. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  105. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  106. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  107. data/lib/capybara/selenium/extensions/find.rb +110 -0
  108. data/lib/capybara/selenium/extensions/html5_drag.rb +229 -0
  109. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  110. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  111. data/lib/capybara/selenium/node.rb +436 -134
  112. data/lib/capybara/selenium/nodes/chrome_node.rb +125 -0
  113. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  114. data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
  115. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  116. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  117. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  118. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  119. data/lib/capybara/selenium/patches/logs.rb +45 -0
  120. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  121. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  122. data/lib/capybara/server/animation_disabler.rb +56 -19
  123. data/lib/capybara/server/checker.rb +9 -3
  124. data/lib/capybara/server/middleware.rb +28 -12
  125. data/lib/capybara/server.rb +33 -10
  126. data/lib/capybara/session/config.rb +34 -10
  127. data/lib/capybara/session/matchers.rb +23 -16
  128. data/lib/capybara/session.rb +230 -170
  129. data/lib/capybara/spec/public/jquery.js +5 -5
  130. data/lib/capybara/spec/public/offset.js +6 -0
  131. data/lib/capybara/spec/public/test.js +121 -8
  132. data/lib/capybara/spec/session/accept_alert_spec.rb +11 -11
  133. data/lib/capybara/spec/session/accept_confirm_spec.rb +3 -3
  134. data/lib/capybara/spec/session/accept_prompt_spec.rb +9 -10
  135. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  136. data/lib/capybara/spec/session/all_spec.rb +127 -40
  137. data/lib/capybara/spec/session/ancestor_spec.rb +24 -19
  138. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +67 -38
  139. data/lib/capybara/spec/session/assert_current_path_spec.rb +21 -18
  140. data/lib/capybara/spec/session/assert_selector_spec.rb +52 -58
  141. data/lib/capybara/spec/session/assert_style_spec.rb +7 -7
  142. data/lib/capybara/spec/session/assert_text_spec.rb +74 -50
  143. data/lib/capybara/spec/session/assert_title_spec.rb +12 -12
  144. data/lib/capybara/spec/session/attach_file_spec.rb +126 -72
  145. data/lib/capybara/spec/session/body_spec.rb +6 -6
  146. data/lib/capybara/spec/session/check_spec.rb +102 -47
  147. data/lib/capybara/spec/session/choose_spec.rb +58 -32
  148. data/lib/capybara/spec/session/click_button_spec.rb +219 -163
  149. data/lib/capybara/spec/session/click_link_or_button_spec.rb +49 -23
  150. data/lib/capybara/spec/session/click_link_spec.rb +77 -54
  151. data/lib/capybara/spec/session/current_scope_spec.rb +8 -8
  152. data/lib/capybara/spec/session/current_url_spec.rb +38 -29
  153. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
  154. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +2 -2
  155. data/lib/capybara/spec/session/element/assert_match_selector_spec.rb +8 -8
  156. data/lib/capybara/spec/session/element/match_css_spec.rb +16 -10
  157. data/lib/capybara/spec/session/element/match_xpath_spec.rb +6 -6
  158. data/lib/capybara/spec/session/element/matches_selector_spec.rb +68 -56
  159. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +7 -7
  160. data/lib/capybara/spec/session/evaluate_script_spec.rb +28 -8
  161. data/lib/capybara/spec/session/execute_script_spec.rb +8 -7
  162. data/lib/capybara/spec/session/fill_in_spec.rb +101 -46
  163. data/lib/capybara/spec/session/find_button_spec.rb +23 -23
  164. data/lib/capybara/spec/session/find_by_id_spec.rb +7 -7
  165. data/lib/capybara/spec/session/find_field_spec.rb +32 -30
  166. data/lib/capybara/spec/session/find_link_spec.rb +31 -21
  167. data/lib/capybara/spec/session/find_spec.rb +244 -141
  168. data/lib/capybara/spec/session/first_spec.rb +43 -43
  169. data/lib/capybara/spec/session/frame/frame_title_spec.rb +5 -5
  170. data/lib/capybara/spec/session/frame/frame_url_spec.rb +5 -5
  171. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +30 -18
  172. data/lib/capybara/spec/session/frame/within_frame_spec.rb +45 -18
  173. data/lib/capybara/spec/session/go_back_spec.rb +1 -1
  174. data/lib/capybara/spec/session/go_forward_spec.rb +1 -1
  175. data/lib/capybara/spec/session/has_all_selectors_spec.rb +23 -23
  176. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  177. data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
  178. data/lib/capybara/spec/session/has_button_spec.rb +94 -13
  179. data/lib/capybara/spec/session/has_css_spec.rb +272 -132
  180. data/lib/capybara/spec/session/has_current_path_spec.rb +50 -35
  181. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  182. data/lib/capybara/spec/session/has_field_spec.rb +137 -58
  183. data/lib/capybara/spec/session/has_link_spec.rb +44 -4
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +31 -31
  185. data/lib/capybara/spec/session/has_select_spec.rb +84 -50
  186. data/lib/capybara/spec/session/has_selector_spec.rb +111 -71
  187. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  188. data/lib/capybara/spec/session/has_table_spec.rb +181 -4
  189. data/lib/capybara/spec/session/has_text_spec.rb +101 -53
  190. data/lib/capybara/spec/session/has_title_spec.rb +19 -14
  191. data/lib/capybara/spec/session/has_xpath_spec.rb +56 -38
  192. data/lib/capybara/spec/session/headers_spec.rb +1 -1
  193. data/lib/capybara/spec/session/html_spec.rb +13 -6
  194. data/lib/capybara/spec/session/matches_style_spec.rb +37 -0
  195. data/lib/capybara/spec/session/node_spec.rb +894 -142
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +10 -7
  197. data/lib/capybara/spec/session/refresh_spec.rb +9 -7
  198. data/lib/capybara/spec/session/reset_session_spec.rb +63 -35
  199. data/lib/capybara/spec/session/response_code_spec.rb +1 -1
  200. data/lib/capybara/spec/session/save_and_open_page_spec.rb +2 -2
  201. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  202. data/lib/capybara/spec/session/save_page_spec.rb +37 -37
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +10 -10
  204. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  205. data/lib/capybara/spec/session/scroll_spec.rb +119 -0
  206. data/lib/capybara/spec/session/select_spec.rb +85 -85
  207. data/lib/capybara/spec/session/selectors_spec.rb +49 -18
  208. data/lib/capybara/spec/session/sibling_spec.rb +9 -9
  209. data/lib/capybara/spec/session/text_spec.rb +25 -24
  210. data/lib/capybara/spec/session/title_spec.rb +7 -6
  211. data/lib/capybara/spec/session/uncheck_spec.rb +33 -21
  212. data/lib/capybara/spec/session/unselect_spec.rb +37 -37
  213. data/lib/capybara/spec/session/visit_spec.rb +68 -49
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +1 -1
  216. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +20 -16
  217. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +6 -2
  218. data/lib/capybara/spec/session/window/window_spec.rb +62 -63
  219. data/lib/capybara/spec/session/window/windows_spec.rb +5 -1
  220. data/lib/capybara/spec/session/window/within_window_spec.rb +14 -14
  221. data/lib/capybara/spec/session/within_spec.rb +79 -42
  222. data/lib/capybara/spec/spec_helper.rb +41 -53
  223. data/lib/capybara/spec/test_app.rb +132 -43
  224. data/lib/capybara/spec/views/animated.erb +49 -0
  225. data/lib/capybara/spec/views/form.erb +139 -42
  226. data/lib/capybara/spec/views/frame_child.erb +4 -3
  227. data/lib/capybara/spec/views/frame_one.erb +2 -1
  228. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  229. data/lib/capybara/spec/views/frame_two.erb +1 -1
  230. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  231. data/lib/capybara/spec/views/layout.erb +10 -0
  232. data/lib/capybara/spec/views/obscured.erb +47 -0
  233. data/lib/capybara/spec/views/offset.erb +33 -0
  234. data/lib/capybara/spec/views/path.erb +2 -2
  235. data/lib/capybara/spec/views/popup_one.erb +1 -1
  236. data/lib/capybara/spec/views/popup_two.erb +1 -1
  237. data/lib/capybara/spec/views/react.erb +45 -0
  238. data/lib/capybara/spec/views/scroll.erb +21 -0
  239. data/lib/capybara/spec/views/spatial.erb +31 -0
  240. data/lib/capybara/spec/views/tables.erb +67 -0
  241. data/lib/capybara/spec/views/with_animation.erb +39 -4
  242. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  243. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  244. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  245. data/lib/capybara/spec/views/with_hover.erb +3 -2
  246. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  247. data/lib/capybara/spec/views/with_html.erb +37 -9
  248. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  249. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  250. data/lib/capybara/spec/views/with_js.erb +26 -5
  251. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  252. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  253. data/lib/capybara/spec/views/with_scope.erb +2 -2
  254. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  255. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  256. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  257. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  258. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  259. data/lib/capybara/spec/views/with_windows.erb +1 -1
  260. data/lib/capybara/spec/views/within_frames.erb +1 -1
  261. data/lib/capybara/version.rb +1 -1
  262. data/lib/capybara/window.rb +19 -25
  263. data/lib/capybara.rb +126 -111
  264. data/spec/basic_node_spec.rb +59 -34
  265. data/spec/capybara_spec.rb +56 -44
  266. data/spec/counter_spec.rb +35 -0
  267. data/spec/css_builder_spec.rb +101 -0
  268. data/spec/css_splitter_spec.rb +8 -8
  269. data/spec/dsl_spec.rb +79 -52
  270. data/spec/filter_set_spec.rb +9 -9
  271. data/spec/fixtures/selenium_driver_rspec_failure.rb +4 -4
  272. data/spec/fixtures/selenium_driver_rspec_success.rb +4 -4
  273. data/spec/minitest_spec.rb +45 -7
  274. data/spec/minitest_spec_spec.rb +87 -64
  275. data/spec/per_session_config_spec.rb +6 -6
  276. data/spec/rack_test_spec.rb +172 -116
  277. data/spec/regexp_dissassembler_spec.rb +250 -0
  278. data/spec/result_spec.rb +80 -72
  279. data/spec/rspec/features_spec.rb +21 -16
  280. data/spec/rspec/scenarios_spec.rb +10 -6
  281. data/spec/rspec/shared_spec_matchers.rb +407 -365
  282. data/spec/rspec/views_spec.rb +3 -3
  283. data/spec/rspec_matchers_spec.rb +35 -10
  284. data/spec/rspec_spec.rb +63 -41
  285. data/spec/sauce_spec_chrome.rb +43 -0
  286. data/spec/selector_spec.rb +334 -89
  287. data/spec/selenium_spec_chrome.rb +176 -62
  288. data/spec/selenium_spec_chrome_remote.rb +54 -14
  289. data/spec/selenium_spec_edge.rb +41 -8
  290. data/spec/selenium_spec_firefox.rb +228 -0
  291. data/spec/selenium_spec_firefox_remote.rb +94 -0
  292. data/spec/selenium_spec_ie.rb +129 -11
  293. data/spec/selenium_spec_safari.rb +162 -0
  294. data/spec/server_spec.rb +171 -97
  295. data/spec/session_spec.rb +34 -18
  296. data/spec/shared_selenium_node.rb +79 -0
  297. data/spec/shared_selenium_session.rb +344 -80
  298. data/spec/spec_helper.rb +124 -2
  299. data/spec/whitespace_normalizer_spec.rb +54 -0
  300. data/spec/xpath_builder_spec.rb +93 -0
  301. metadata +326 -28
  302. data/lib/capybara/rspec/compound.rb +0 -94
  303. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +0 -31
  304. data/lib/capybara/selenium/nodes/marionette_node.rb +0 -31
  305. data/lib/capybara/spec/session/has_style_spec.rb +0 -25
  306. data/lib/capybara/spec/session/source_spec.rb +0 -0
  307. data/lib/capybara/spec/views/with_title.erb +0 -5
  308. data/spec/selenium_spec_marionette.rb +0 -167
@@ -1,17 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Selenium specific implementation of the Capybara::Driver::Node API
4
+
5
+ require 'capybara/selenium/extensions/find'
6
+ require 'capybara/selenium/extensions/scroll'
7
+ require 'capybara/node/whitespace_normalizer'
8
+
3
9
  class Capybara::Selenium::Node < Capybara::Driver::Node
10
+ include Capybara::Node::WhitespaceNormalizer
11
+ include Capybara::Selenium::Find
12
+ include Capybara::Selenium::Scroll
13
+
4
14
  def visible_text
15
+ raise NotImplementedError, 'Getting visible text is not currently supported directly on shadow roots' if shadow_root?
16
+
5
17
  native.text
6
18
  end
7
19
 
8
20
  def all_text
9
- text = driver.execute_script("return arguments[0].textContent", self)
10
- text.gsub(/[\u200b\u200e\u200f]/, '')
11
- .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
12
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
13
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
14
- .tr("\u00a0", ' ')
21
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
22
+ normalize_spacing(text)
15
23
  end
16
24
 
17
25
  def [](name)
@@ -21,17 +29,15 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
21
29
  end
22
30
 
23
31
  def value
24
- if tag_name == "select" && multiple?
25
- native.find_elements(:css, "option:checked").map { |n| n[:value] || n.text }
32
+ if tag_name == 'select' && multiple?
33
+ native.find_elements(:css, 'option:checked').map { |el| el[:value] || el.text }
26
34
  else
27
35
  native[:value]
28
36
  end
29
37
  end
30
38
 
31
39
  def style(styles)
32
- styles.each_with_object({}) do |style, result|
33
- result[style] = native.css_value(style)
34
- end
40
+ styles.to_h { |style| [style, native.css_value(style)] }
35
41
  end
36
42
 
37
43
  ##
@@ -45,12 +51,22 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
45
51
  # :none => append the new value to the existing value <br/>
46
52
  # :backspace => send backspace keystrokes to clear the field <br/>
47
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/>
48
59
  def set(value, **options)
49
- 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
50
66
 
51
67
  case tag_name
52
68
  when 'input'
53
- case self[:type]
69
+ case type
54
70
  when 'radio'
55
71
  click
56
72
  when 'checkbox'
@@ -63,38 +79,47 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
63
79
  set_time(value)
64
80
  when 'datetime-local'
65
81
  set_datetime_local(value)
82
+ when 'color'
83
+ set_color(value)
84
+ when 'range'
85
+ set_range(value)
66
86
  else
67
- set_text(value, options)
87
+ set_text(value, **options)
68
88
  end
69
89
  when 'textarea'
70
- set_text(value, options)
90
+ set_text(value, **options)
71
91
  else
72
- set_content_editable(value) if content_editable?
92
+ set_content_editable(value)
73
93
  end
74
94
  end
75
95
 
76
96
  def select_option
77
- native.click unless selected? || disabled?
97
+ click unless selected? || disabled?
78
98
  end
79
99
 
80
100
  def unselect_option
81
- raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." unless select_node.multiple?
82
- native.click if selected?
101
+ raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
102
+
103
+ click if selected?
83
104
  end
84
105
 
85
106
  def click(keys = [], **options)
86
- if keys.empty? && !coords?(options)
87
- native.click
88
- else
89
- scroll_if_needed do
90
- action_with_modifiers(keys, options) do |a|
91
- coords?(options) ? a.click : a.click(native)
92
- end
107
+ click_options = ClickOptions.new(keys, options)
108
+ return native.click if click_options.empty?
109
+
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
93
118
  end
94
119
  end
95
120
  rescue StandardError => e
96
121
  if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
97
- e.message =~ /Other element would receive the click/
122
+ e.message.include?('Other element would receive the click')
98
123
  scroll_to_center
99
124
  end
100
125
 
@@ -102,18 +127,26 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
102
127
  end
103
128
 
104
129
  def right_click(keys = [], **options)
105
- scroll_if_needed do
106
- action_with_modifiers(keys, options) do |a|
107
- coords?(options) ? a.context_click : a.context_click(native)
130
+ click_options = ClickOptions.new(keys, options)
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)
108
140
  end
109
141
  end
110
142
  end
111
143
 
112
144
  def double_click(keys = [], **options)
113
- scroll_if_needed do
114
- action_with_modifiers(keys, options) do |a|
115
- coords?(options) ? a.double_click : a.double_click(native)
116
- end
145
+ click_options = ClickOptions.new(keys, options)
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|
149
+ click_options.coords? ? action.double_click : action.double_click(native)
117
150
  end
118
151
  end
119
152
 
@@ -122,15 +155,33 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
122
155
  end
123
156
 
124
157
  def hover
125
- scroll_if_needed { driver.browser.action.move_to(native).perform }
158
+ scroll_if_needed { browser_action.move_to(native).perform }
159
+ end
160
+
161
+ def drag_to(element, drop_modifiers: [], **)
162
+ drop_modifiers = Array(drop_modifiers)
163
+ # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
164
+ # which means Seleniums `drag_and_drop` is now broken - do it manually
165
+ scroll_if_needed { browser_action.click_and_hold(native).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
126
172
  end
127
173
 
128
- def drag_to(element)
129
- scroll_if_needed { driver.browser.action.drag_and_drop(native, element.native).perform }
174
+ def drop(*_)
175
+ raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
130
176
  end
131
177
 
132
178
  def tag_name
133
- 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
134
185
  end
135
186
 
136
187
  def visible?; boolean_attr(native.displayed?); end
@@ -140,56 +191,78 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
140
191
  alias :checked? :selected?
141
192
 
142
193
  def disabled?
143
- !native.enabled?
194
+ return true unless native.enabled?
195
+
196
+ # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
197
+ find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
144
198
  end
145
199
 
146
200
  def content_editable?
147
- native.attribute('isContentEditable')
201
+ native.attribute('isContentEditable') == 'true'
148
202
  end
149
203
 
150
- def find_xpath(locator)
151
- native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
204
+ def path
205
+ driver.evaluate_script GET_XPATH_SCRIPT, self
152
206
  end
153
207
 
154
- def find_css(locator)
155
- native.find_elements(:css, locator).map { |n| self.class.new(driver, n) }
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'])
156
213
  end
157
214
 
158
- def ==(other)
159
- native == other.native
215
+ def rect
216
+ native.rect
160
217
  end
161
218
 
162
- def path
163
- path = find_xpath(XPath.ancestor_or_self).reverse
164
-
165
- result = []
166
- default_ns = path.last[:namespaceURI]
167
- while (node = path.shift)
168
- parent = path.first
169
- selector = node[:tagName]
170
- if node[:namespaceURI] != default_ns
171
- selector = XPath.child.where((XPath.local_name == selector) & (XPath.namespace_uri == node[:namespaceURI])).to_s
172
- selector
173
- end
219
+ def shadow_root
220
+ root = native.shadow_root
221
+ root && build_node(native.shadow_root)
222
+ end
174
223
 
175
- if parent
176
- siblings = parent.find_xpath(selector)
177
- selector += "[#{siblings.index(node) + 1}]" unless siblings.size == 1
178
- end
179
- result.push selector
180
- end
224
+ protected
181
225
 
182
- '/' + result.reverse.join('/')
226
+ def scroll_if_needed
227
+ yield
228
+ rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
229
+ scroll_to_center
230
+ yield
231
+ end
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
183
246
  end
184
247
 
185
248
  private
186
249
 
187
- def coords?(options)
188
- options[:x] && options[:y]
250
+ def sibling_index(parent, node, selector)
251
+ siblings = parent.find_xpath(selector)
252
+ case siblings.size
253
+ when 0
254
+ '[ERROR]' # IE doesn't support full XPath (namespace-uri, etc)
255
+ when 1
256
+ '' # index not necessary when only one matching element
257
+ else
258
+ idx = siblings.index(node)
259
+ # Element may not be found in the siblings if it has gone away
260
+ idx.nil? ? '[ERROR]' : "[#{idx + 1}]"
261
+ end
189
262
  end
190
263
 
191
264
  def boolean_attr(val)
192
- val && (val != "false")
265
+ val && (val != 'false')
193
266
  end
194
267
 
195
268
  # a reference to the select node if this is an option node
@@ -197,68 +270,81 @@ private
197
270
  find_xpath(XPath.ancestor(:select)[1]).first
198
271
  end
199
272
 
200
- def set_text(value, clear: nil, **_unused)
201
- if value.to_s.empty? && clear.nil?
273
+ def set_text(value, clear: nil, rapid: nil, **_unused)
274
+ value = value.to_s
275
+ if value.empty? && clear.nil?
202
276
  native.clear
203
277
  elsif clear == :backspace
204
278
  # Clear field by sending the correct number of backspace keys.
205
279
  backspaces = [:backspace] * self.value.to_s.length
206
- native.send_keys(*(backspaces + [value.to_s]))
207
- elsif clear == :none
208
- native.send_keys(value.to_s)
280
+ send_keys(:end, *backspaces, value)
209
281
  elsif clear.is_a? Array
210
- native.send_keys(*clear, value.to_s)
282
+ send_keys(*clear, value)
211
283
  else
212
- # Clear field by JavaScript assignment of the value property.
213
- # Script can change a readonly element which user input cannot, so
214
- # don't execute if readonly.
215
- driver.execute_script "arguments[0].value = ''", self
216
- native.send_keys(value.to_s)
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
217
292
  end
218
293
  end
219
294
 
220
- def scroll_if_needed
221
- yield
222
- rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
223
- scroll_to_center
224
- yield
295
+ def auto_rapid_set_length
296
+ 30
225
297
  end
226
298
 
227
- def scroll_to_center
228
- script = <<-'JS'
229
- try {
230
- arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
231
- } catch(e) {
232
- arguments[0].scrollIntoView(true);
233
- }
234
- JS
235
- begin
236
- driver.execute_script(script, self)
237
- rescue StandardError # rubocop:disable Lint/HandleExceptions
238
- # Swallow error if scrollIntoView with options isn't supported
299
+ def perform_with_options(click_options, &block)
300
+ raise ArgumentError, 'A block must be provided' unless block
301
+
302
+ scroll_if_needed do
303
+ action_with_modifiers(click_options) do |action|
304
+ if block
305
+ yield action
306
+ else
307
+ click_options.coords? ? action.click : action.click(native)
308
+ end
309
+ end
239
310
  end
240
311
  end
241
312
 
242
313
  def set_date(value) # rubocop:disable Naming/AccessorMethodName
243
- return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_date)
314
+ value = SettableValue.new(value)
315
+ return set_text(value) unless value.dateable?
316
+
244
317
  # TODO: this would be better if locale can be detected and correct keystrokes sent
245
- update_value_js(value.to_date.strftime('%Y-%m-%d'))
318
+ update_value_js(value.to_date_str)
246
319
  end
247
320
 
248
321
  def set_time(value) # rubocop:disable Naming/AccessorMethodName
249
- return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_time)
322
+ value = SettableValue.new(value)
323
+ return set_text(value) unless value.timeable?
324
+
250
325
  # TODO: this would be better if locale can be detected and correct keystrokes sent
251
- update_value_js(value.to_time.strftime('%H:%M'))
326
+ update_value_js(value.to_time_str)
252
327
  end
253
328
 
254
329
  def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
255
- return set_text(value) if value.is_a?(String) || !value.respond_to?(:to_time)
330
+ value = SettableValue.new(value)
331
+ return set_text(value) unless value.timeable?
332
+
256
333
  # TODO: this would be better if locale can be detected and correct keystrokes sent
257
- update_value_js(value.to_time.strftime('%Y-%m-%dT%H:%M'))
334
+ update_value_js(value.to_datetime_str)
335
+ end
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)
258
343
  end
259
344
 
260
345
  def update_value_js(value)
261
346
  driver.execute_script(<<-JS, self, value)
347
+ if (arguments[0].readOnly) { return };
262
348
  if (document.activeElement !== arguments[0]){
263
349
  arguments[0].focus();
264
350
  }
@@ -271,64 +357,280 @@ private
271
357
  end
272
358
 
273
359
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
274
- path_names = value.to_s.empty? ? [] : value
275
- 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
276
385
  end
277
386
 
278
387
  def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
279
388
  # Ensure we are focused on the element
280
389
  click
281
390
 
282
- script = <<-JS
283
- var range = document.createRange();
284
- var sel = window.getSelection();
285
- arguments[0].focus();
286
- range.selectNodeContents(arguments[0]);
287
- sel.removeAllRanges();
288
- 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;
289
402
  JS
290
- driver.execute_script script, self
291
403
 
292
404
  # The action api has a speed problem but both chrome and firefox 58 raise errors
293
405
  # if we use the faster direct send_keys. For now just send_keys to the element
294
406
  # we've already focused.
295
407
  # native.send_keys(value.to_s)
296
- driver.browser.action.send_keys(value.to_s).perform
297
- end
298
-
299
- def action_with_modifiers(keys, x: nil, y: nil)
300
- actions = driver.browser.action
301
- actions.move_to(native, x, y)
302
- modifiers_down(actions, keys)
408
+ browser_action.send_keys(value.to_s).perform if editable
409
+ end
410
+
411
+ def action_with_modifiers(click_options)
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
429
+ modifiers_down(actions, click_options.keys)
303
430
  yield actions
304
- modifiers_up(actions, keys)
431
+ modifiers_up(actions, click_options.keys)
305
432
  actions.perform
306
433
  ensure
307
- a = driver.browser.action
308
- a.release_actions if a.respond_to?(:release_actions)
434
+ act = browser_action
435
+ act.release_actions if act.respond_to?(:release_actions)
309
436
  end
310
437
 
311
438
  def modifiers_down(actions, keys)
312
- keys.each do |key|
313
- key = case key
314
- when :ctrl then :control
315
- when :command, :cmd then :meta
316
- else
317
- key
318
- end
319
- actions.key_down(key)
320
- end
439
+ each_key(keys) { |key| actions.key_down(key) }
440
+ actions
321
441
  end
322
442
 
323
443
  def modifiers_up(actions, keys)
324
- keys.each do |key|
325
- key = case key
444
+ each_key(keys) { |key| actions.key_up(key) }
445
+ actions
446
+ end
447
+
448
+ def browser
449
+ driver.browser
450
+ end
451
+
452
+ def bridge
453
+ browser.send(:bridge)
454
+ end
455
+
456
+ def browser_action
457
+ browser.action
458
+ end
459
+
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
326
471
  when :ctrl then :control
327
472
  when :command, :cmd then :meta
328
473
  else
329
474
  key
330
475
  end
331
- actions.key_up(key)
332
476
  end
333
477
  end
478
+
479
+ def each_key(keys, &block)
480
+ normalize_keys(keys).each(&block)
481
+ end
482
+
483
+ def find_context
484
+ native
485
+ end
486
+
487
+ def build_node(native_node, initial_cache = {})
488
+ self.class.new(driver, native_node, initial_cache)
489
+ end
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
+
513
+ GET_XPATH_SCRIPT = <<~'JS'
514
+ (function(el, xml){
515
+ var xpath = '';
516
+ var pos, tempitem2;
517
+
518
+ if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
519
+ return "(: Shadow DOM element - no XPath :)";
520
+ };
521
+ while(el !== xml.documentElement) {
522
+ pos = 0;
523
+ tempitem2 = el;
524
+ while(tempitem2) {
525
+ if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) { // If it is ELEMENT_NODE of the same name
526
+ pos += 1;
527
+ }
528
+ tempitem2 = tempitem2.previousSibling;
529
+ }
530
+
531
+ if (el.namespaceURI != xml.documentElement.namespaceURI) {
532
+ xpath = "*[local-name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
533
+ } else {
534
+ xpath = el.nodeName.toUpperCase()+"["+pos+"]/"+xpath;
535
+ }
536
+
537
+ el = el.parentNode;
538
+ }
539
+ xpath = '/'+xml.documentElement.nodeName.toUpperCase()+'/'+xpath;
540
+ xpath = xpath.replace(/\/$/, '');
541
+ return xpath;
542
+ })(arguments[0], document)
543
+ JS
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
+
572
+ # SettableValue encapsulates time/date field formatting
573
+ class SettableValue
574
+ attr_reader :value
575
+
576
+ def initialize(value)
577
+ @value = value
578
+ end
579
+
580
+ def to_s
581
+ value.to_s
582
+ end
583
+
584
+ def dateable?
585
+ !value.is_a?(String) && value.respond_to?(:to_date)
586
+ end
587
+
588
+ def to_date_str
589
+ value.to_date.iso8601
590
+ end
591
+
592
+ def timeable?
593
+ !value.is_a?(String) && value.respond_to?(:to_time)
594
+ end
595
+
596
+ def to_time_str
597
+ value.to_time.strftime('%H:%M')
598
+ end
599
+
600
+ def to_datetime_str
601
+ value.to_time.strftime('%Y-%m-%dT%H:%M')
602
+ end
603
+ end
604
+ private_constant :SettableValue
605
+
606
+ # ClickOptions encapsulates click option logic
607
+ class ClickOptions
608
+ attr_reader :keys, :options
609
+
610
+ def initialize(keys, options)
611
+ @keys = keys
612
+ @options = options
613
+ end
614
+
615
+ def coords?
616
+ options[:x] && options[:y]
617
+ end
618
+
619
+ def coords
620
+ [options[:x], options[:y]]
621
+ end
622
+
623
+ def center_offset?
624
+ options[:offset] == :center
625
+ end
626
+
627
+ def empty?
628
+ keys.empty? && !coords? && delay.zero?
629
+ end
630
+
631
+ def delay
632
+ options[:delay] || 0
633
+ end
634
+ end
635
+ private_constant :ClickOptions
334
636
  end