capybara 3.0.0 → 3.36.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 -1
  3. data/History.md +777 -10
  4. data/License.txt +1 -1
  5. data/README.md +75 -58
  6. data/lib/capybara/config.rb +29 -10
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +22 -4
  9. data/lib/capybara/driver/node.rb +34 -9
  10. data/lib/capybara/dsl.rb +14 -6
  11. data/lib/capybara/helpers.rb +52 -7
  12. data/lib/capybara/minitest/spec.rb +173 -84
  13. data/lib/capybara/minitest.rb +250 -144
  14. data/lib/capybara/node/actions.rb +248 -124
  15. data/lib/capybara/node/base.rb +34 -20
  16. data/lib/capybara/node/document.rb +14 -2
  17. data/lib/capybara/node/document_matchers.rb +13 -15
  18. data/lib/capybara/node/element.rb +339 -113
  19. data/lib/capybara/node/finders.rb +95 -82
  20. data/lib/capybara/node/matchers.rb +338 -157
  21. data/lib/capybara/node/simple.rb +54 -15
  22. data/lib/capybara/queries/active_element_query.rb +18 -0
  23. data/lib/capybara/queries/ancestor_query.rb +9 -10
  24. data/lib/capybara/queries/base_query.rb +25 -18
  25. data/lib/capybara/queries/current_path_query.rb +16 -6
  26. data/lib/capybara/queries/match_query.rb +11 -0
  27. data/lib/capybara/queries/selector_query.rb +600 -103
  28. data/lib/capybara/queries/sibling_query.rb +9 -7
  29. data/lib/capybara/queries/style_query.rb +45 -0
  30. data/lib/capybara/queries/text_query.rb +40 -22
  31. data/lib/capybara/queries/title_query.rb +2 -2
  32. data/lib/capybara/rack_test/browser.rb +47 -28
  33. data/lib/capybara/rack_test/driver.rb +12 -3
  34. data/lib/capybara/rack_test/errors.rb +6 -0
  35. data/lib/capybara/rack_test/form.rb +53 -50
  36. data/lib/capybara/rack_test/node.rb +114 -37
  37. data/lib/capybara/rails.rb +1 -1
  38. data/lib/capybara/registration_container.rb +44 -0
  39. data/lib/capybara/registrations/drivers.rb +42 -0
  40. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  41. data/lib/capybara/registrations/servers.rb +45 -0
  42. data/lib/capybara/result.rb +86 -52
  43. data/lib/capybara/rspec/features.rb +8 -10
  44. data/lib/capybara/rspec/matcher_proxies.rb +39 -18
  45. data/lib/capybara/rspec/matchers/base.rb +111 -0
  46. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  47. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  48. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  49. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  50. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  51. data/lib/capybara/rspec/matchers/have_selector.rb +77 -0
  52. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  53. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  54. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  55. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  56. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  57. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  58. data/lib/capybara/rspec/matchers.rb +138 -316
  59. data/lib/capybara/rspec.rb +3 -2
  60. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  61. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  62. data/lib/capybara/selector/css.rb +89 -12
  63. data/lib/capybara/selector/definition/button.rb +68 -0
  64. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  65. data/lib/capybara/selector/definition/css.rb +10 -0
  66. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  67. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  68. data/lib/capybara/selector/definition/element.rb +28 -0
  69. data/lib/capybara/selector/definition/field.rb +40 -0
  70. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  71. data/lib/capybara/selector/definition/file_field.rb +13 -0
  72. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  73. data/lib/capybara/selector/definition/frame.rb +17 -0
  74. data/lib/capybara/selector/definition/id.rb +6 -0
  75. data/lib/capybara/selector/definition/label.rb +62 -0
  76. data/lib/capybara/selector/definition/link.rb +54 -0
  77. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  78. data/lib/capybara/selector/definition/option.rb +27 -0
  79. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  80. data/lib/capybara/selector/definition/select.rb +81 -0
  81. data/lib/capybara/selector/definition/table.rb +109 -0
  82. data/lib/capybara/selector/definition/table_row.rb +21 -0
  83. data/lib/capybara/selector/definition/xpath.rb +5 -0
  84. data/lib/capybara/selector/definition.rb +279 -0
  85. data/lib/capybara/selector/filter.rb +1 -0
  86. data/lib/capybara/selector/filter_set.rb +72 -27
  87. data/lib/capybara/selector/filters/base.rb +45 -2
  88. data/lib/capybara/selector/filters/expression_filter.rb +5 -6
  89. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  90. data/lib/capybara/selector/filters/node_filter.rb +18 -4
  91. data/lib/capybara/selector/regexp_disassembler.rb +214 -0
  92. data/lib/capybara/selector/selector.rb +85 -200
  93. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  94. data/lib/capybara/selector.rb +226 -537
  95. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  96. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  97. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  98. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  99. data/lib/capybara/selenium/driver.rb +296 -238
  100. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  101. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  102. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -0
  103. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  104. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  105. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  106. data/lib/capybara/selenium/extensions/find.rb +110 -0
  107. data/lib/capybara/selenium/extensions/html5_drag.rb +228 -0
  108. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  109. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  110. data/lib/capybara/selenium/logger_suppressor.rb +40 -0
  111. data/lib/capybara/selenium/node.rb +436 -166
  112. data/lib/capybara/selenium/nodes/chrome_node.rb +137 -0
  113. data/lib/capybara/selenium/nodes/edge_node.rb +104 -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/action_pauser.rb +26 -0
  118. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  119. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  120. data/lib/capybara/selenium/patches/logs.rb +45 -0
  121. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  122. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  123. data/lib/capybara/server/animation_disabler.rb +69 -0
  124. data/lib/capybara/server/checker.rb +44 -0
  125. data/lib/capybara/server/middleware.rb +71 -0
  126. data/lib/capybara/server.rb +58 -67
  127. data/lib/capybara/session/config.rb +38 -6
  128. data/lib/capybara/session/matchers.rb +26 -19
  129. data/lib/capybara/session.rb +258 -190
  130. data/lib/capybara/spec/public/jquery.js +5 -5
  131. data/lib/capybara/spec/public/offset.js +6 -0
  132. data/lib/capybara/spec/public/test.js +122 -8
  133. data/lib/capybara/spec/session/accept_alert_spec.rb +11 -11
  134. data/lib/capybara/spec/session/accept_confirm_spec.rb +3 -3
  135. data/lib/capybara/spec/session/accept_prompt_spec.rb +9 -10
  136. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  137. data/lib/capybara/spec/session/all_spec.rb +135 -42
  138. data/lib/capybara/spec/session/ancestor_spec.rb +24 -19
  139. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +67 -38
  140. data/lib/capybara/spec/session/{assert_current_path.rb → assert_current_path_spec.rb} +20 -18
  141. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  142. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  143. data/lib/capybara/spec/session/{assert_text.rb → assert_text_spec.rb} +62 -38
  144. data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +12 -12
  145. data/lib/capybara/spec/session/attach_file_spec.rb +120 -72
  146. data/lib/capybara/spec/session/body_spec.rb +11 -13
  147. data/lib/capybara/spec/session/check_spec.rb +111 -51
  148. data/lib/capybara/spec/session/choose_spec.rb +62 -30
  149. data/lib/capybara/spec/session/click_button_spec.rb +227 -161
  150. data/lib/capybara/spec/session/click_link_or_button_spec.rb +49 -30
  151. data/lib/capybara/spec/session/click_link_spec.rb +78 -55
  152. data/lib/capybara/spec/session/current_scope_spec.rb +7 -7
  153. data/lib/capybara/spec/session/current_url_spec.rb +44 -37
  154. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
  155. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +2 -2
  156. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +11 -9
  157. data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
  158. data/lib/capybara/spec/session/element/match_xpath_spec.rb +8 -6
  159. data/lib/capybara/spec/session/element/matches_selector_spec.rb +70 -56
  160. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +7 -7
  161. data/lib/capybara/spec/session/evaluate_script_spec.rb +28 -8
  162. data/lib/capybara/spec/session/execute_script_spec.rb +8 -7
  163. data/lib/capybara/spec/session/fill_in_spec.rb +104 -44
  164. data/lib/capybara/spec/session/find_button_spec.rb +23 -23
  165. data/lib/capybara/spec/session/find_by_id_spec.rb +8 -8
  166. data/lib/capybara/spec/session/find_field_spec.rb +33 -31
  167. data/lib/capybara/spec/session/find_link_spec.rb +32 -14
  168. data/lib/capybara/spec/session/find_spec.rb +236 -141
  169. data/lib/capybara/spec/session/first_spec.rb +44 -43
  170. data/lib/capybara/spec/session/frame/frame_title_spec.rb +6 -6
  171. data/lib/capybara/spec/session/frame/frame_url_spec.rb +6 -6
  172. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +32 -20
  173. data/lib/capybara/spec/session/frame/within_frame_spec.rb +44 -19
  174. data/lib/capybara/spec/session/go_back_spec.rb +1 -1
  175. data/lib/capybara/spec/session/go_forward_spec.rb +1 -1
  176. data/lib/capybara/spec/session/has_all_selectors_spec.rb +18 -18
  177. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  178. data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
  179. data/lib/capybara/spec/session/has_button_spec.rb +92 -12
  180. data/lib/capybara/spec/session/has_css_spec.rb +271 -137
  181. data/lib/capybara/spec/session/has_current_path_spec.rb +48 -33
  182. data/lib/capybara/spec/session/has_field_spec.rb +137 -58
  183. data/lib/capybara/spec/session/has_link_spec.rb +30 -6
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +26 -24
  185. data/lib/capybara/spec/session/has_select_spec.rb +75 -47
  186. data/lib/capybara/spec/session/has_selector_spec.rb +102 -69
  187. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  188. data/lib/capybara/spec/session/has_table_spec.rb +170 -4
  189. data/lib/capybara/spec/session/has_text_spec.rb +108 -52
  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.rb → 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 +35 -0
  195. data/lib/capybara/spec/session/node_spec.rb +871 -122
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +15 -12
  197. data/lib/capybara/spec/session/refresh_spec.rb +9 -7
  198. data/lib/capybara/spec/session/reset_session_spec.rb +52 -37
  199. data/lib/capybara/spec/session/{response_code.rb → 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 +5 -4
  202. data/lib/capybara/spec/session/save_page_spec.rb +41 -38
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +13 -11
  204. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  205. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  206. data/lib/capybara/spec/session/select_spec.rb +102 -76
  207. data/lib/capybara/spec/session/selectors_spec.rb +51 -18
  208. data/lib/capybara/spec/session/sibling_spec.rb +9 -9
  209. data/lib/capybara/spec/session/text_spec.rb +26 -24
  210. data/lib/capybara/spec/session/title_spec.rb +8 -6
  211. data/lib/capybara/spec/session/uncheck_spec.rb +40 -21
  212. data/lib/capybara/spec/session/unselect_spec.rb +37 -37
  213. data/lib/capybara/spec/session/visit_spec.rb +59 -53
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +22 -19
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +4 -3
  216. data/lib/capybara/spec/session/window/open_new_window_spec.rb +4 -3
  217. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +25 -21
  218. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +10 -5
  219. data/lib/capybara/spec/session/window/window_spec.rb +88 -54
  220. data/lib/capybara/spec/session/window/windows_spec.rb +10 -7
  221. data/lib/capybara/spec/session/window/within_window_spec.rb +17 -16
  222. data/lib/capybara/spec/session/within_spec.rb +69 -44
  223. data/lib/capybara/spec/spec_helper.rb +41 -53
  224. data/lib/capybara/spec/test_app.rb +79 -40
  225. data/lib/capybara/spec/views/animated.erb +49 -0
  226. data/lib/capybara/spec/views/form.erb +134 -42
  227. data/lib/capybara/spec/views/frame_child.erb +4 -3
  228. data/lib/capybara/spec/views/frame_one.erb +2 -1
  229. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  230. data/lib/capybara/spec/views/frame_two.erb +1 -1
  231. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  232. data/lib/capybara/spec/views/layout.erb +10 -0
  233. data/lib/capybara/spec/views/obscured.erb +47 -0
  234. data/lib/capybara/spec/views/offset.erb +33 -0
  235. data/lib/capybara/spec/views/path.erb +2 -2
  236. data/lib/capybara/spec/views/popup_one.erb +1 -1
  237. data/lib/capybara/spec/views/popup_two.erb +1 -1
  238. data/lib/capybara/spec/views/react.erb +45 -0
  239. data/lib/capybara/spec/views/scroll.erb +21 -0
  240. data/lib/capybara/spec/views/spatial.erb +31 -0
  241. data/lib/capybara/spec/views/tables.erb +68 -1
  242. data/lib/capybara/spec/views/with_animation.erb +81 -0
  243. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  244. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  245. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  246. data/lib/capybara/spec/views/with_hover.erb +3 -2
  247. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  248. data/lib/capybara/spec/views/with_html.erb +43 -10
  249. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  250. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  251. data/lib/capybara/spec/views/with_js.erb +30 -5
  252. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  253. data/lib/capybara/spec/views/with_namespace.erb +21 -0
  254. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  255. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  256. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  257. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  258. data/lib/capybara/spec/views/with_windows.erb +1 -1
  259. data/lib/capybara/spec/views/within_frames.erb +1 -1
  260. data/lib/capybara/version.rb +1 -1
  261. data/lib/capybara/window.rb +31 -25
  262. data/lib/capybara.rb +120 -100
  263. data/spec/basic_node_spec.rb +59 -34
  264. data/spec/capybara_spec.rb +54 -52
  265. data/spec/css_builder_spec.rb +101 -0
  266. data/spec/css_splitter_spec.rb +38 -0
  267. data/spec/dsl_spec.rb +80 -53
  268. data/spec/filter_set_spec.rb +24 -7
  269. data/spec/fixtures/certificate.pem +25 -0
  270. data/spec/fixtures/key.pem +27 -0
  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 +38 -5
  274. data/spec/minitest_spec_spec.rb +88 -62
  275. data/spec/per_session_config_spec.rb +5 -5
  276. data/spec/rack_test_spec.rb +169 -115
  277. data/spec/regexp_dissassembler_spec.rb +250 -0
  278. data/spec/result_spec.rb +86 -33
  279. data/spec/rspec/features_spec.rb +26 -23
  280. data/spec/rspec/scenarios_spec.rb +8 -4
  281. data/spec/rspec/shared_spec_matchers.rb +393 -363
  282. data/spec/rspec/views_spec.rb +4 -3
  283. data/spec/rspec_matchers_spec.rb +10 -10
  284. data/spec/rspec_spec.rb +109 -85
  285. data/spec/sauce_spec_chrome.rb +43 -0
  286. data/spec/selector_spec.rb +391 -61
  287. data/spec/selenium_spec_chrome.rb +180 -41
  288. data/spec/selenium_spec_chrome_remote.rb +101 -0
  289. data/spec/selenium_spec_edge.rb +28 -8
  290. data/spec/selenium_spec_firefox.rb +211 -0
  291. data/spec/selenium_spec_firefox_remote.rb +80 -0
  292. data/spec/selenium_spec_ie.rb +127 -11
  293. data/spec/selenium_spec_safari.rb +156 -0
  294. data/spec/server_spec.rb +177 -78
  295. data/spec/session_spec.rb +52 -16
  296. data/spec/shared_selenium_node.rb +79 -0
  297. data/spec/shared_selenium_session.rb +455 -123
  298. data/spec/spec_helper.rb +91 -2
  299. data/spec/xpath_builder_spec.rb +93 -0
  300. metadata +343 -42
  301. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  302. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  303. data/.yard/yard_extensions.rb +0 -78
  304. data/lib/capybara/rspec/compound.rb +0 -90
  305. data/lib/capybara/spec/session/assert_selector.rb +0 -149
  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 -143
@@ -1,29 +1,24 @@
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
+
3
8
  class Capybara::Selenium::Node < Capybara::Driver::Node
4
- SET_FORMATS = Hash.new(date: '%Y-%m-%d', time: '%H:%M', datetime: "%m%d%Y\t%I%M%P").merge(
5
- firefox: {
6
- date: '%Y-%m-%d',
7
- time: '%H:%M',
8
- datetime: "%m%d%Y\t%I%M%P"
9
- },
10
- chrome: {
11
- date: '%m%d%Y',
12
- time: '%I%M%P',
13
- datetime: "%m%d%Y\t%I%M%P"
14
- }
15
- )
9
+ include Capybara::Selenium::Find
10
+ include Capybara::Selenium::Scroll
16
11
 
17
12
  def visible_text
18
13
  native.text
19
14
  end
20
15
 
21
16
  def all_text
22
- text = driver.execute_script("return arguments[0].textContent", self)
17
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
23
18
  text.gsub(/[\u200b\u200e\u200f]/, '')
24
19
  .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
25
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
26
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
20
+ .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
21
+ .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
27
22
  .tr("\u00a0", ' ')
28
23
  end
29
24
 
@@ -34,13 +29,19 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
34
29
  end
35
30
 
36
31
  def value
37
- if tag_name == "select" and multiple?
38
- 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 }
39
34
  else
40
35
  native[:value]
41
36
  end
42
37
  end
43
38
 
39
+ def style(styles)
40
+ styles.each_with_object({}) do |style, result|
41
+ result[style] = native.css_value(style)
42
+ end
43
+ end
44
+
44
45
  ##
45
46
  #
46
47
  # Set the value of the form element to the given value.
@@ -53,11 +54,16 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
53
54
  # :backspace => send backspace keystrokes to clear the field <br/>
54
55
  # Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
55
56
  def set(value, **options)
56
- raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}" if value.is_a?(Array) && !multiple?
57
+ if value.is_a?(Array) && !multiple?
58
+ raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
59
+ end
60
+
61
+ tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
62
+ @tag_name ||= tag_name
57
63
 
58
64
  case tag_name
59
65
  when 'input'
60
- case self[:type]
66
+ case type
61
67
  when 'radio'
62
68
  click
63
69
  when 'checkbox'
@@ -70,71 +76,80 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
70
76
  set_time(value)
71
77
  when 'datetime-local'
72
78
  set_datetime_local(value)
79
+ when 'color'
80
+ set_color(value)
81
+ when 'range'
82
+ set_range(value)
73
83
  else
74
- set_text(value, options)
84
+ set_text(value, **options)
75
85
  end
76
86
  when 'textarea'
77
- set_text(value, options)
87
+ set_text(value, **options)
78
88
  else
79
- set_content_editable(value) if content_editable?
89
+ set_content_editable(value)
80
90
  end
81
91
  end
82
92
 
83
93
  def select_option
84
- native.click unless selected? || disabled?
94
+ click unless selected? || disabled?
85
95
  end
86
96
 
87
97
  def unselect_option
88
- raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." unless select_node.multiple?
89
- native.click if selected?
98
+ raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
99
+
100
+ click if selected?
90
101
  end
91
102
 
92
- def click(keys = [], options = {})
93
- if keys.empty? && !(options[:x] && options[:y])
94
- native.click
95
- else
96
- scroll_if_needed do
97
- action_with_modifiers(keys, options) do |a|
98
- if options[:x] && options[:y]
99
- a.click
100
- else
101
- a.click(native)
102
- end
103
+ def click(keys = [], **options)
104
+ click_options = ClickOptions.new(keys, options)
105
+ return native.click if click_options.empty?
106
+
107
+ perform_with_options(click_options) do |action|
108
+ target = click_options.coords? ? nil : native
109
+ if click_options.delay.zero?
110
+ action.click(target)
111
+ else
112
+ action.click_and_hold(target)
113
+ if w3c?
114
+ action.pause(action.pointer_inputs.first, click_options.delay)
115
+ else
116
+ action.pause(click_options.delay)
103
117
  end
118
+ action.release
104
119
  end
105
120
  end
106
- rescue => e
121
+ rescue StandardError => e
107
122
  if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
108
- e.message =~ /Other element would receive the click/
109
- begin
110
- driver.execute_script("arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'})", self)
111
- rescue # Swallow error if scrollIntoView with options isn't supported
112
- end
123
+ e.message.include?('Other element would receive the click')
124
+ scroll_to_center
113
125
  end
126
+
114
127
  raise e
115
128
  end
116
129
 
117
- def right_click(keys = [], options = {})
118
- scroll_if_needed do
119
- action_with_modifiers(keys, options) do |a|
120
- if options[:x] && options[:y]
121
- a.context_click
122
- else
123
- a.context_click(native)
124
- end
130
+ def right_click(keys = [], **options)
131
+ click_options = ClickOptions.new(keys, options)
132
+ perform_with_options(click_options) do |action|
133
+ target = click_options.coords? ? nil : native
134
+ if click_options.delay.zero?
135
+ action.context_click(target)
136
+ elsif w3c?
137
+ action.move_to(target) if target
138
+ action.pointer_down(:right)
139
+ .pause(action.pointer_inputs.first, click_options.delay)
140
+ .pointer_up(:right)
141
+ else
142
+ raise ArgumentError, 'Delay is not supported when right clicking with legacy (non-w3c) selenium driver'
125
143
  end
126
144
  end
127
145
  end
128
146
 
129
- def double_click(keys = [], options = {})
130
- scroll_if_needed do
131
- action_with_modifiers(keys, options) do |a|
132
- if options[:x] && options[:y]
133
- a.double_click
134
- else
135
- a.double_click(native)
136
- end
137
- end
147
+ def double_click(keys = [], **options)
148
+ click_options = ClickOptions.new(keys, options)
149
+ raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?
150
+
151
+ perform_with_options(click_options) do |action|
152
+ click_options.coords? ? action.double_click : action.double_click(native)
138
153
  end
139
154
  end
140
155
 
@@ -143,15 +158,28 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
143
158
  end
144
159
 
145
160
  def hover
146
- scroll_if_needed { driver.browser.action.move_to(native).perform }
161
+ scroll_if_needed { browser_action.move_to(native).perform }
162
+ end
163
+
164
+ def drag_to(element, drop_modifiers: [], **)
165
+ drop_modifiers = Array(drop_modifiers)
166
+ # Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
167
+ # which means Seleniums `drag_and_drop` is now broken - do it manually
168
+ scroll_if_needed { browser_action.click_and_hold(native).perform }
169
+ # element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
170
+ element.scroll_if_needed do
171
+ keys_down = modifiers_down(browser_action, drop_modifiers)
172
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
173
+ keys_up.perform
174
+ end
147
175
  end
148
176
 
149
- def drag_to(element)
150
- scroll_if_needed { driver.browser.action.drag_and_drop(native, element.native).perform }
177
+ def drop(*_)
178
+ raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
151
179
  end
152
180
 
153
181
  def tag_name
154
- native.tag_name.downcase
182
+ @tag_name ||= native.tag_name.downcase
155
183
  end
156
184
 
157
185
  def visible?; boolean_attr(native.displayed?); end
@@ -163,55 +191,71 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
163
191
  def disabled?
164
192
  return true unless native.enabled?
165
193
 
166
- # workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
167
- if driver.marionette?
168
- if %w[option optgroup].include? tag_name
169
- find_xpath("parent::*[self::optgroup or self::select]")[0].disabled?
170
- else
171
- !find_xpath("parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]").empty?
172
- end
173
- else
174
- false
175
- end
194
+ # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
195
+ find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
176
196
  end
177
197
 
178
198
  def content_editable?
179
- native.attribute('isContentEditable')
199
+ native.attribute('isContentEditable') == 'true'
180
200
  end
181
201
 
182
- def find_xpath(locator)
183
- native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
202
+ def path
203
+ driver.evaluate_script GET_XPATH_SCRIPT, self
184
204
  end
185
205
 
186
- def find_css(locator)
187
- native.find_elements(:css, locator).map { |n| self.class.new(driver, n) }
206
+ def obscured?(x: nil, y: nil)
207
+ res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
208
+ return true if res == true
209
+
210
+ driver.frame_obscured_at?(x: res['x'], y: res['y'])
188
211
  end
189
212
 
190
- def ==(other)
191
- native == other.native
213
+ def rect
214
+ native.rect
192
215
  end
193
216
 
194
- def path
195
- path = find_xpath(XPath.ancestor_or_self).reverse
196
-
197
- result = []
198
- while (node = path.shift)
199
- parent = path.first
200
- selector = node.tag_name
201
- if parent
202
- siblings = parent.find_xpath(node.tag_name)
203
- selector += "[#{siblings.index(node) + 1}]" unless siblings.size == 1
204
- end
205
- result.push selector
206
- end
217
+ protected
218
+
219
+ def scroll_if_needed
220
+ yield
221
+ rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
222
+ scroll_to_center
223
+ yield
224
+ end
207
225
 
208
- '/' + result.reverse.join('/')
226
+ def scroll_to_center
227
+ script = <<-'JS'
228
+ try {
229
+ arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
230
+ } catch(e) {
231
+ arguments[0].scrollIntoView(true);
232
+ }
233
+ JS
234
+ begin
235
+ driver.execute_script(script, self)
236
+ rescue StandardError
237
+ # Swallow error if scrollIntoView with options isn't supported
238
+ end
209
239
  end
210
240
 
211
241
  private
212
242
 
243
+ def sibling_index(parent, node, selector)
244
+ siblings = parent.find_xpath(selector)
245
+ case siblings.size
246
+ when 0
247
+ '[ERROR]' # IE doesn't support full XPath (namespace-uri, etc)
248
+ when 1
249
+ '' # index not necessary when only one matching element
250
+ else
251
+ idx = siblings.index(node)
252
+ # Element may not be found in the siblings if it has gone away
253
+ idx.nil? ? '[ERROR]' : "[#{idx + 1}]"
254
+ end
255
+ end
256
+
213
257
  def boolean_attr(val)
214
- val and val != "false"
258
+ val && (val != 'false')
215
259
  end
216
260
 
217
261
  # a reference to the select node if this is an option node
@@ -219,71 +263,117 @@ private
219
263
  find_xpath(XPath.ancestor(:select)[1]).first
220
264
  end
221
265
 
222
- def set_text(value, clear: nil, **_unused)
223
- if value.to_s.empty? && clear.nil?
266
+ def set_text(value, clear: nil, rapid: nil, **_unused)
267
+ value = value.to_s
268
+ if value.empty? && clear.nil?
224
269
  native.clear
225
270
  elsif clear == :backspace
226
271
  # Clear field by sending the correct number of backspace keys.
227
272
  backspaces = [:backspace] * self.value.to_s.length
228
- native.send_keys(*(backspaces + [value.to_s]))
229
- elsif clear == :none
230
- native.send_keys(value.to_s)
273
+ send_keys(:end, *backspaces, value)
231
274
  elsif clear.is_a? Array
232
- native.send_keys(*clear, value.to_s)
275
+ send_keys(*clear, value)
233
276
  else
234
- # Clear field by JavaScript assignment of the value property.
235
- # Script can change a readonly element which user input cannot, so
236
- # don't execute if readonly.
237
- driver.execute_script "arguments[0].value = ''", self
238
- native.send_keys(value.to_s)
277
+ driver.execute_script 'arguments[0].select()', self unless clear == :none
278
+ if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
279
+ send_keys(value[0..3])
280
+ driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
281
+ send_keys(value[-3..])
282
+ else
283
+ send_keys(value)
284
+ end
239
285
  end
240
286
  end
241
287
 
242
- def scroll_if_needed
243
- yield
244
- rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
245
- script = <<-'JS'
246
- try {
247
- arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
248
- } catch(e) {
249
- arguments[0].scrollIntoView(true);
250
- }
251
- JS
252
- driver.execute_script(script, self)
253
- yield
288
+ def auto_rapid_set_length
289
+ 30
254
290
  end
255
291
 
256
- def set_date(value) # rubocop:disable Naming/AccessorMethodName
257
- if value.respond_to?(:to_date)
258
- set_text(value.to_date.strftime(SET_FORMATS[driver.options[:browser].to_sym][:date]))
259
- else
260
- set_text(value)
292
+ def perform_with_options(click_options, &block)
293
+ raise ArgumentError, 'A block must be provided' unless block
294
+
295
+ scroll_if_needed do
296
+ action_with_modifiers(click_options) do |action|
297
+ if block
298
+ yield action
299
+ else
300
+ click_options.coords? ? action.click : action.click(native)
301
+ end
302
+ end
261
303
  end
262
304
  end
263
305
 
306
+ def set_date(value) # rubocop:disable Naming/AccessorMethodName
307
+ value = SettableValue.new(value)
308
+ return set_text(value) unless value.dateable?
309
+
310
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
311
+ update_value_js(value.to_date_str)
312
+ end
313
+
264
314
  def set_time(value) # rubocop:disable Naming/AccessorMethodName
265
- if value.respond_to?(:to_time)
266
- set_text(value.to_time.strftime(SET_FORMATS[driver.options[:browser].to_sym][:time]))
267
- else
268
- set_text(value)
269
- end
315
+ value = SettableValue.new(value)
316
+ return set_text(value) unless value.timeable?
317
+
318
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
319
+ update_value_js(value.to_time_str)
270
320
  end
271
321
 
272
322
  def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
273
- if value.respond_to?(:to_time)
274
- set_text(value.to_time.strftime(SET_FORMATS[driver.options[:browser].to_sym][:datetime]))
275
- else
276
- set_text(value)
277
- end
323
+ value = SettableValue.new(value)
324
+ return set_text(value) unless value.timeable?
325
+
326
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
327
+ update_value_js(value.to_datetime_str)
328
+ end
329
+
330
+ def set_color(value) # rubocop:disable Naming/AccessorMethodName
331
+ update_value_js(value)
332
+ end
333
+
334
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
335
+ update_value_js(value)
336
+ end
337
+
338
+ def update_value_js(value)
339
+ driver.execute_script(<<-JS, self, value)
340
+ if (arguments[0].readOnly) { return };
341
+ if (document.activeElement !== arguments[0]){
342
+ arguments[0].focus();
343
+ }
344
+ if (arguments[0].value != arguments[1]) {
345
+ arguments[0].value = arguments[1]
346
+ arguments[0].dispatchEvent(new InputEvent('input'));
347
+ arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
348
+ }
349
+ JS
278
350
  end
279
351
 
280
352
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
281
- path_names = value.to_s.empty? ? [] : value
282
- if driver.marionette?
283
- native.clear
284
- Array(path_names).each { |p| native.send_keys(p) }
353
+ with_file_detector do
354
+ path_names = value.to_s.empty? ? [] : value
355
+ file_names = Array(path_names).map do |pn|
356
+ Pathname.new(pn).absolute? ? pn : File.expand_path(pn)
357
+ end.join("\n")
358
+ native.send_keys(file_names)
359
+ end
360
+ end
361
+
362
+ def with_file_detector
363
+ if driver.options[:browser] == :remote &&
364
+ bridge.respond_to?(:file_detector) &&
365
+ bridge.file_detector.nil?
366
+ begin
367
+ bridge.file_detector = lambda do |(fn, *)|
368
+ str = fn.to_s
369
+ str if File.exist?(str)
370
+ end
371
+ yield
372
+ ensure
373
+ bridge.file_detector = nil
374
+ end
285
375
  else
286
- native.send_keys(Array(path_names).join("\n"))
376
+ yield
287
377
  end
288
378
  end
289
379
 
@@ -291,56 +381,236 @@ private
291
381
  # Ensure we are focused on the element
292
382
  click
293
383
 
294
- script = <<-JS
295
- var range = document.createRange();
296
- var sel = window.getSelection();
297
- arguments[0].focus();
298
- range.selectNodeContents(arguments[0]);
299
- sel.removeAllRanges();
300
- sel.addRange(range);
384
+ editable = driver.execute_script <<-JS, self
385
+ if (arguments[0].isContentEditable) {
386
+ var range = document.createRange();
387
+ var sel = window.getSelection();
388
+ arguments[0].focus();
389
+ range.selectNodeContents(arguments[0]);
390
+ sel.removeAllRanges();
391
+ sel.addRange(range);
392
+ return true;
393
+ }
394
+ return false;
301
395
  JS
302
- driver.execute_script script, self
303
396
 
304
397
  # The action api has a speed problem but both chrome and firefox 58 raise errors
305
398
  # if we use the faster direct send_keys. For now just send_keys to the element
306
399
  # we've already focused.
307
400
  # native.send_keys(value.to_s)
308
- driver.browser.action.send_keys(value.to_s).perform
401
+ browser_action.send_keys(value.to_s).perform if editable
309
402
  end
310
403
 
311
- def action_with_modifiers(keys, x: nil, y: nil)
312
- actions = driver.browser.action
313
- actions.move_to(native, x, y)
314
- modifiers_down(actions, keys)
404
+ def action_with_modifiers(click_options)
405
+ actions = browser_action.tap do |acts|
406
+ if click_options.center_offset? && click_options.coords?
407
+ acts.move_to(native).move_by(*click_options.coords)
408
+ else
409
+ acts.move_to(native, *click_options.coords)
410
+ end
411
+ end
412
+ modifiers_down(actions, click_options.keys)
315
413
  yield actions
316
- modifiers_up(actions, keys)
414
+ modifiers_up(actions, click_options.keys)
317
415
  actions.perform
318
416
  ensure
319
- a = driver.browser.action
320
- a.release_actions if a.respond_to?(:release_actions)
417
+ act = browser_action
418
+ act.release_actions if act.respond_to?(:release_actions)
321
419
  end
322
420
 
323
421
  def modifiers_down(actions, keys)
324
- keys.each do |key|
325
- key = case key
326
- when :ctrl then :control
327
- when :command, :cmd then :meta
328
- else
329
- key
330
- end
331
- actions.key_down(key)
332
- end
422
+ each_key(keys) { |key| actions.key_down(key) }
423
+ actions
333
424
  end
334
425
 
335
426
  def modifiers_up(actions, keys)
336
- keys.each do |key|
337
- key = case key
427
+ each_key(keys) { |key| actions.key_up(key) }
428
+ actions
429
+ end
430
+
431
+ def browser
432
+ driver.browser
433
+ end
434
+
435
+ def bridge
436
+ browser.send(:bridge)
437
+ end
438
+
439
+ def browser_action
440
+ browser.action
441
+ end
442
+
443
+ def capabilities
444
+ browser.capabilities
445
+ end
446
+
447
+ def w3c?
448
+ (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
449
+ capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
450
+ end
451
+
452
+ def normalize_keys(keys)
453
+ keys.map do |key|
454
+ case key
338
455
  when :ctrl then :control
339
456
  when :command, :cmd then :meta
340
457
  else
341
458
  key
342
459
  end
343
- actions.key_up(key)
344
460
  end
345
461
  end
462
+
463
+ def each_key(keys, &block)
464
+ normalize_keys(keys).each(&block)
465
+ end
466
+
467
+ def find_context
468
+ native
469
+ end
470
+
471
+ def build_node(native_node, initial_cache = {})
472
+ self.class.new(driver, native_node, initial_cache)
473
+ end
474
+
475
+ def attrs(*attr_names)
476
+ return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']
477
+
478
+ driver.evaluate_script <<~'JS', self, attr_names.map(&:to_s)
479
+ (function(el, names){
480
+ return names.map(function(name){
481
+ return el[name]
482
+ });
483
+ })(arguments[0], arguments[1]);
484
+ JS
485
+ end
486
+
487
+ def native_id
488
+ # Selenium 3 -> 4 changed the return of ref
489
+ type_or_id, id = native.ref
490
+ id || type_or_id
491
+ end
492
+
493
+ GET_XPATH_SCRIPT = <<~'JS'
494
+ (function(el, xml){
495
+ var xpath = '';
496
+ var pos, tempitem2;
497
+
498
+ if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
499
+ return "(: Shadow DOM element - no XPath :)";
500
+ };
501
+ while(el !== xml.documentElement) {
502
+ pos = 0;
503
+ tempitem2 = el;
504
+ while(tempitem2) {
505
+ if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) { // If it is ELEMENT_NODE of the same name
506
+ pos += 1;
507
+ }
508
+ tempitem2 = tempitem2.previousSibling;
509
+ }
510
+
511
+ if (el.namespaceURI != xml.documentElement.namespaceURI) {
512
+ xpath = "*[local-name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
513
+ } else {
514
+ xpath = el.nodeName.toUpperCase()+"["+pos+"]/"+xpath;
515
+ }
516
+
517
+ el = el.parentNode;
518
+ }
519
+ xpath = '/'+xml.documentElement.nodeName.toUpperCase()+'/'+xpath;
520
+ xpath = xpath.replace(/\/$/, '');
521
+ return xpath;
522
+ })(arguments[0], document)
523
+ JS
524
+
525
+ OBSCURED_OR_OFFSET_SCRIPT = <<~'JS'
526
+ (function(el, x, y) {
527
+ var box = el.getBoundingClientRect();
528
+ if (x == null) x = box.width/2;
529
+ if (y == null) y = box.height/2 ;
530
+
531
+ var px = box.left + x,
532
+ py = box.top + y,
533
+ e = document.elementFromPoint(px, py);
534
+
535
+ if (!el.contains(e))
536
+ return true;
537
+
538
+ return { x: px, y: py };
539
+ })(arguments[0], arguments[1], arguments[2])
540
+ JS
541
+
542
+ RAPID_APPEND_TEXT = <<~'JS'
543
+ (function(el, value) {
544
+ value = el.value + value;
545
+ if (el.maxLength && el.maxLength != -1){
546
+ value = value.slice(0, el.maxLength);
547
+ }
548
+ el.value = value;
549
+ })(arguments[0], arguments[1])
550
+ JS
551
+
552
+ # SettableValue encapsulates time/date field formatting
553
+ class SettableValue
554
+ attr_reader :value
555
+
556
+ def initialize(value)
557
+ @value = value
558
+ end
559
+
560
+ def to_s
561
+ value.to_s
562
+ end
563
+
564
+ def dateable?
565
+ !value.is_a?(String) && value.respond_to?(:to_date)
566
+ end
567
+
568
+ def to_date_str
569
+ value.to_date.iso8601
570
+ end
571
+
572
+ def timeable?
573
+ !value.is_a?(String) && value.respond_to?(:to_time)
574
+ end
575
+
576
+ def to_time_str
577
+ value.to_time.strftime('%H:%M')
578
+ end
579
+
580
+ def to_datetime_str
581
+ value.to_time.strftime('%Y-%m-%dT%H:%M')
582
+ end
583
+ end
584
+ private_constant :SettableValue
585
+
586
+ # ClickOptions encapsulates click option logic
587
+ class ClickOptions
588
+ attr_reader :keys, :options
589
+
590
+ def initialize(keys, options)
591
+ @keys = keys
592
+ @options = options
593
+ end
594
+
595
+ def coords?
596
+ options[:x] && options[:y]
597
+ end
598
+
599
+ def coords
600
+ [options[:x], options[:y]]
601
+ end
602
+
603
+ def center_offset?
604
+ options[:offset] == :center
605
+ end
606
+
607
+ def empty?
608
+ keys.empty? && !coords? && delay.zero?
609
+ end
610
+
611
+ def delay
612
+ options[:delay] || 0
613
+ end
614
+ end
615
+ private_constant :ClickOptions
346
616
  end