capybara 3.3.0 → 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 (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