capybara 2.18.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 (316) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/History.md +945 -12
  4. data/License.txt +1 -1
  5. data/README.md +264 -90
  6. data/lib/capybara/config.rb +29 -57
  7. data/lib/capybara/cucumber.rb +2 -3
  8. data/lib/capybara/driver/base.rb +35 -18
  9. data/lib/capybara/driver/node.rb +40 -10
  10. data/lib/capybara/dsl.rb +10 -7
  11. data/lib/capybara/helpers.rb +70 -31
  12. data/lib/capybara/minitest/spec.rb +185 -83
  13. data/lib/capybara/minitest.rb +232 -112
  14. data/lib/capybara/node/actions.rb +274 -171
  15. data/lib/capybara/node/base.rb +42 -34
  16. data/lib/capybara/node/document.rb +15 -3
  17. data/lib/capybara/node/document_matchers.rb +19 -21
  18. data/lib/capybara/node/element.rb +362 -135
  19. data/lib/capybara/node/finders.rb +151 -137
  20. data/lib/capybara/node/matchers.rb +394 -209
  21. data/lib/capybara/node/simple.rb +59 -26
  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 +12 -9
  25. data/lib/capybara/queries/base_query.rb +39 -28
  26. data/lib/capybara/queries/current_path_query.rb +21 -27
  27. data/lib/capybara/queries/match_query.rb +14 -7
  28. data/lib/capybara/queries/selector_query.rb +659 -149
  29. data/lib/capybara/queries/sibling_query.rb +11 -9
  30. data/lib/capybara/queries/style_query.rb +45 -0
  31. data/lib/capybara/queries/text_query.rb +56 -38
  32. data/lib/capybara/queries/title_query.rb +8 -11
  33. data/lib/capybara/rack_test/browser.rb +114 -42
  34. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  35. data/lib/capybara/rack_test/driver.rb +22 -17
  36. data/lib/capybara/rack_test/errors.rb +6 -0
  37. data/lib/capybara/rack_test/form.rb +93 -58
  38. data/lib/capybara/rack_test/node.rb +184 -81
  39. data/lib/capybara/rails.rb +3 -7
  40. data/lib/capybara/registration_container.rb +41 -0
  41. data/lib/capybara/registrations/drivers.rb +42 -0
  42. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  43. data/lib/capybara/registrations/servers.rb +66 -0
  44. data/lib/capybara/result.rb +97 -63
  45. data/lib/capybara/rspec/features.rb +17 -50
  46. data/lib/capybara/rspec/matcher_proxies.rb +52 -15
  47. data/lib/capybara/rspec/matchers/base.rb +113 -0
  48. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  49. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  50. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  51. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  52. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  53. data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
  54. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  55. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  56. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  57. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  58. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  59. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  60. data/lib/capybara/rspec/matchers.rb +146 -310
  61. data/lib/capybara/rspec.rb +7 -11
  62. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  63. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  64. data/lib/capybara/selector/css.rb +85 -13
  65. data/lib/capybara/selector/definition/button.rb +68 -0
  66. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  67. data/lib/capybara/selector/definition/css.rb +10 -0
  68. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  69. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  70. data/lib/capybara/selector/definition/element.rb +28 -0
  71. data/lib/capybara/selector/definition/field.rb +40 -0
  72. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  73. data/lib/capybara/selector/definition/file_field.rb +13 -0
  74. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  75. data/lib/capybara/selector/definition/frame.rb +17 -0
  76. data/lib/capybara/selector/definition/id.rb +6 -0
  77. data/lib/capybara/selector/definition/label.rb +62 -0
  78. data/lib/capybara/selector/definition/link.rb +55 -0
  79. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  80. data/lib/capybara/selector/definition/option.rb +27 -0
  81. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  82. data/lib/capybara/selector/definition/select.rb +81 -0
  83. data/lib/capybara/selector/definition/table.rb +109 -0
  84. data/lib/capybara/selector/definition/table_row.rb +21 -0
  85. data/lib/capybara/selector/definition/xpath.rb +5 -0
  86. data/lib/capybara/selector/definition.rb +280 -0
  87. data/lib/capybara/selector/filter.rb +2 -17
  88. data/lib/capybara/selector/filter_set.rb +80 -33
  89. data/lib/capybara/selector/filters/base.rb +50 -6
  90. data/lib/capybara/selector/filters/expression_filter.rb +8 -26
  91. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  92. data/lib/capybara/selector/filters/node_filter.rb +16 -12
  93. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  94. data/lib/capybara/selector/selector.rb +93 -210
  95. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  96. data/lib/capybara/selector.rb +475 -523
  97. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  98. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  99. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  100. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  101. data/lib/capybara/selenium/driver.rb +298 -267
  102. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  103. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  104. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +84 -0
  105. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  106. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  107. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  108. data/lib/capybara/selenium/extensions/find.rb +110 -0
  109. data/lib/capybara/selenium/extensions/html5_drag.rb +229 -0
  110. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  111. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  112. data/lib/capybara/selenium/node.rb +517 -145
  113. data/lib/capybara/selenium/nodes/chrome_node.rb +125 -0
  114. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  115. data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
  116. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  117. data/lib/capybara/selenium/nodes/safari_node.rb +118 -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 +80 -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 +59 -67
  127. data/lib/capybara/session/config.rb +81 -67
  128. data/lib/capybara/session/matchers.rb +28 -20
  129. data/lib/capybara/session.rb +337 -365
  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 +151 -12
  133. data/lib/capybara/spec/session/accept_alert_spec.rb +12 -11
  134. data/lib/capybara/spec/session/accept_confirm_spec.rb +6 -5
  135. data/lib/capybara/spec/session/accept_prompt_spec.rb +10 -10
  136. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  137. data/lib/capybara/spec/session/all_spec.rb +161 -57
  138. data/lib/capybara/spec/session/ancestor_spec.rb +27 -24
  139. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +68 -38
  140. data/lib/capybara/spec/session/assert_current_path_spec.rb +75 -0
  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_spec.rb +258 -0
  144. data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +22 -12
  145. data/lib/capybara/spec/session/attach_file_spec.rb +144 -69
  146. data/lib/capybara/spec/session/body_spec.rb +12 -13
  147. data/lib/capybara/spec/session/check_spec.rb +117 -55
  148. data/lib/capybara/spec/session/choose_spec.rb +64 -31
  149. data/lib/capybara/spec/session/click_button_spec.rb +231 -173
  150. data/lib/capybara/spec/session/click_link_or_button_spec.rb +55 -35
  151. data/lib/capybara/spec/session/click_link_spec.rb +93 -58
  152. data/lib/capybara/spec/session/current_scope_spec.rb +12 -11
  153. data/lib/capybara/spec/session/current_url_spec.rb +57 -39
  154. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +4 -4
  155. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +3 -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 +9 -7
  159. data/lib/capybara/spec/session/element/matches_selector_spec.rb +71 -57
  160. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +8 -7
  161. data/lib/capybara/spec/session/evaluate_script_spec.rb +29 -8
  162. data/lib/capybara/spec/session/execute_script_spec.rb +10 -8
  163. data/lib/capybara/spec/session/fill_in_spec.rb +134 -43
  164. data/lib/capybara/spec/session/find_button_spec.rb +25 -24
  165. data/lib/capybara/spec/session/find_by_id_spec.rb +10 -9
  166. data/lib/capybara/spec/session/find_field_spec.rb +37 -41
  167. data/lib/capybara/spec/session/find_link_spec.rb +46 -17
  168. data/lib/capybara/spec/session/find_spec.rb +260 -145
  169. data/lib/capybara/spec/session/first_spec.rb +80 -52
  170. data/lib/capybara/spec/session/frame/frame_title_spec.rb +23 -0
  171. data/lib/capybara/spec/session/frame/frame_url_spec.rb +23 -0
  172. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +33 -20
  173. data/lib/capybara/spec/session/frame/within_frame_spec.rb +52 -32
  174. data/lib/capybara/spec/session/go_back_spec.rb +2 -1
  175. data/lib/capybara/spec/session/go_forward_spec.rb +2 -1
  176. data/lib/capybara/spec/session/has_all_selectors_spec.rb +31 -31
  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 +100 -13
  180. data/lib/capybara/spec/session/has_css_spec.rb +272 -137
  181. data/lib/capybara/spec/session/has_current_path_spec.rb +60 -61
  182. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  183. data/lib/capybara/spec/session/has_field_spec.rb +139 -59
  184. data/lib/capybara/spec/session/has_link_spec.rb +47 -6
  185. data/lib/capybara/spec/session/has_none_selectors_spec.rb +42 -40
  186. data/lib/capybara/spec/session/has_select_spec.rb +107 -72
  187. data/lib/capybara/spec/session/has_selector_spec.rb +120 -71
  188. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  189. data/lib/capybara/spec/session/has_table_spec.rb +183 -5
  190. data/lib/capybara/spec/session/has_text_spec.rb +106 -62
  191. data/lib/capybara/spec/session/has_title_spec.rb +20 -14
  192. data/lib/capybara/spec/session/has_xpath_spec.rb +57 -38
  193. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +3 -2
  194. data/lib/capybara/spec/session/html_spec.rb +14 -6
  195. data/lib/capybara/spec/session/matches_style_spec.rb +37 -0
  196. data/lib/capybara/spec/session/node_spec.rb +1024 -153
  197. data/lib/capybara/spec/session/node_wrapper_spec.rb +39 -0
  198. data/lib/capybara/spec/session/refresh_spec.rb +12 -6
  199. data/lib/capybara/spec/session/reset_session_spec.rb +82 -35
  200. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +2 -1
  201. data/lib/capybara/spec/session/save_and_open_page_spec.rb +3 -2
  202. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +8 -12
  203. data/lib/capybara/spec/session/save_page_spec.rb +42 -55
  204. data/lib/capybara/spec/session/save_screenshot_spec.rb +16 -14
  205. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  206. data/lib/capybara/spec/session/scroll_spec.rb +119 -0
  207. data/lib/capybara/spec/session/select_spec.rb +107 -81
  208. data/lib/capybara/spec/session/selectors_spec.rb +52 -19
  209. data/lib/capybara/spec/session/sibling_spec.rb +10 -10
  210. data/lib/capybara/spec/session/text_spec.rb +37 -21
  211. data/lib/capybara/spec/session/title_spec.rb +17 -5
  212. data/lib/capybara/spec/session/uncheck_spec.rb +43 -23
  213. data/lib/capybara/spec/session/unselect_spec.rb +39 -38
  214. data/lib/capybara/spec/session/visit_spec.rb +85 -53
  215. data/lib/capybara/spec/session/window/become_closed_spec.rb +24 -20
  216. data/lib/capybara/spec/session/window/current_window_spec.rb +5 -3
  217. data/lib/capybara/spec/session/window/open_new_window_spec.rb +5 -3
  218. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +27 -22
  219. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +12 -6
  220. data/lib/capybara/spec/session/window/window_spec.rb +97 -63
  221. data/lib/capybara/spec/session/window/windows_spec.rb +12 -10
  222. data/lib/capybara/spec/session/window/within_window_spec.rb +31 -86
  223. data/lib/capybara/spec/session/within_spec.rb +83 -44
  224. data/lib/capybara/spec/spec_helper.rb +54 -44
  225. data/lib/capybara/spec/test_app.rb +158 -43
  226. data/lib/capybara/spec/views/animated.erb +49 -0
  227. data/lib/capybara/spec/views/form.erb +163 -42
  228. data/lib/capybara/spec/views/frame_child.erb +4 -3
  229. data/lib/capybara/spec/views/frame_one.erb +2 -1
  230. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  231. data/lib/capybara/spec/views/frame_two.erb +1 -1
  232. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  233. data/lib/capybara/spec/views/layout.erb +10 -0
  234. data/lib/capybara/spec/views/obscured.erb +47 -0
  235. data/lib/capybara/spec/views/offset.erb +33 -0
  236. data/lib/capybara/spec/views/path.erb +2 -2
  237. data/lib/capybara/spec/views/popup_one.erb +1 -1
  238. data/lib/capybara/spec/views/popup_two.erb +1 -1
  239. data/lib/capybara/spec/views/react.erb +45 -0
  240. data/lib/capybara/spec/views/scroll.erb +21 -0
  241. data/lib/capybara/spec/views/spatial.erb +31 -0
  242. data/lib/capybara/spec/views/tables.erb +68 -1
  243. data/lib/capybara/spec/views/with_animation.erb +81 -0
  244. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  245. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  246. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  247. data/lib/capybara/spec/views/with_hover.erb +3 -2
  248. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  249. data/lib/capybara/spec/views/with_html.erb +69 -12
  250. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  251. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  252. data/lib/capybara/spec/views/with_js.erb +30 -5
  253. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  254. data/lib/capybara/spec/views/with_namespace.erb +21 -0
  255. data/lib/capybara/spec/views/with_scope.erb +2 -2
  256. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  257. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  258. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  259. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  260. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  261. data/lib/capybara/spec/views/with_windows.erb +1 -1
  262. data/lib/capybara/spec/views/within_frames.erb +5 -2
  263. data/lib/capybara/version.rb +2 -1
  264. data/lib/capybara/window.rb +36 -34
  265. data/lib/capybara.rb +134 -107
  266. data/spec/basic_node_spec.rb +60 -34
  267. data/spec/capybara_spec.rb +63 -88
  268. data/spec/counter_spec.rb +35 -0
  269. data/spec/css_builder_spec.rb +101 -0
  270. data/spec/css_splitter_spec.rb +38 -0
  271. data/spec/dsl_spec.rb +85 -64
  272. data/spec/filter_set_spec.rb +27 -9
  273. data/spec/fixtures/certificate.pem +25 -0
  274. data/spec/fixtures/key.pem +27 -0
  275. data/spec/fixtures/selenium_driver_rspec_failure.rb +6 -5
  276. data/spec/fixtures/selenium_driver_rspec_success.rb +6 -5
  277. data/spec/minitest_spec.rb +52 -7
  278. data/spec/minitest_spec_spec.rb +94 -63
  279. data/spec/per_session_config_spec.rb +14 -13
  280. data/spec/rack_test_spec.rb +194 -125
  281. data/spec/regexp_dissassembler_spec.rb +250 -0
  282. data/spec/result_spec.rb +111 -50
  283. data/spec/rspec/features_spec.rb +37 -31
  284. data/spec/rspec/scenarios_spec.rb +10 -8
  285. data/spec/rspec/shared_spec_matchers.rb +473 -422
  286. data/spec/rspec/views_spec.rb +5 -3
  287. data/spec/rspec_matchers_spec.rb +52 -11
  288. data/spec/rspec_spec.rb +109 -89
  289. data/spec/sauce_spec_chrome.rb +43 -0
  290. data/spec/selector_spec.rb +397 -68
  291. data/spec/selenium_spec_chrome.rb +187 -40
  292. data/spec/selenium_spec_chrome_remote.rb +96 -0
  293. data/spec/selenium_spec_edge.rb +60 -0
  294. data/spec/selenium_spec_firefox.rb +201 -41
  295. data/spec/selenium_spec_firefox_remote.rb +94 -0
  296. data/spec/selenium_spec_ie.rb +149 -0
  297. data/spec/selenium_spec_safari.rb +162 -0
  298. data/spec/server_spec.rb +213 -102
  299. data/spec/session_spec.rb +53 -16
  300. data/spec/shared_selenium_node.rb +79 -0
  301. data/spec/shared_selenium_session.rb +473 -122
  302. data/spec/spec_helper.rb +126 -7
  303. data/spec/whitespace_normalizer_spec.rb +54 -0
  304. data/spec/xpath_builder_spec.rb +93 -0
  305. metadata +355 -73
  306. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  307. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  308. data/.yard/yard_extensions.rb +0 -78
  309. data/lib/capybara/query.rb +0 -7
  310. data/lib/capybara/rspec/compound.rb +0 -95
  311. data/lib/capybara/spec/session/assert_current_path.rb +0 -72
  312. data/lib/capybara/spec/session/assert_selector.rb +0 -148
  313. data/lib/capybara/spec/session/assert_text.rb +0 -234
  314. data/lib/capybara/spec/session/source_spec.rb +0 -0
  315. data/lib/capybara/spec/views/with_title.erb +0 -5
  316. data/spec/selenium_spec_marionette.rb +0 -127
@@ -1,14 +1,25 @@
1
1
  # frozen_string_literal: true
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
+
2
9
  class Capybara::Selenium::Node < Capybara::Driver::Node
10
+ include Capybara::Node::WhitespaceNormalizer
11
+ include Capybara::Selenium::Find
12
+ include Capybara::Selenium::Scroll
3
13
 
4
14
  def visible_text
5
- # Selenium doesn't normalize Unicode whitespace.
6
- Capybara::Helpers.normalize_whitespace(native.text)
15
+ raise NotImplementedError, 'Getting visible text is not currently supported directly on shadow roots' if shadow_root?
16
+
17
+ native.text
7
18
  end
8
19
 
9
20
  def all_text
10
- text = driver.execute_script("return arguments[0].textContent", self)
11
- Capybara::Helpers.normalize_whitespace(text)
21
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
22
+ normalize_spacing(text)
12
23
  end
13
24
 
14
25
  def [](name)
@@ -18,13 +29,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
18
29
  end
19
30
 
20
31
  def value
21
- if tag_name == "select" and multiple?
22
- 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 }
23
34
  else
24
35
  native[:value]
25
36
  end
26
37
  end
27
38
 
39
+ def style(styles)
40
+ styles.to_h { |style| [style, native.css_value(style)] }
41
+ end
42
+
28
43
  ##
29
44
  #
30
45
  # Set the value of the form element to the given value.
@@ -36,91 +51,102 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
36
51
  # :none => append the new value to the existing value <br/>
37
52
  # :backspace => send backspace keystrokes to clear the field <br/>
38
53
  # Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
39
- def set(value, options={})
40
- tag_name = self.tag_name
41
- type = self[:type]
42
-
43
- if (Array === value) && !multiple?
44
- raise ArgumentError.new "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
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/>
59
+ def set(value, **options)
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}"
45
62
  end
46
63
 
64
+ tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
65
+ @tag_name ||= tag_name
66
+
47
67
  case tag_name
48
68
  when 'input'
49
69
  case type
50
70
  when 'radio'
51
71
  click
52
72
  when 'checkbox'
53
- click if value ^ native.attribute('checked').to_s.eql?("true")
73
+ click if value ^ checked?
54
74
  when 'file'
55
- path_names = value.to_s.empty? ? [] : value
56
- if driver.chrome?
57
- native.send_keys(Array(path_names).join("\n"))
58
- else
59
- native.send_keys(*path_names)
60
- end
75
+ set_file(value)
76
+ when 'date'
77
+ set_date(value)
78
+ when 'time'
79
+ set_time(value)
80
+ when 'datetime-local'
81
+ set_datetime_local(value)
82
+ when 'color'
83
+ set_color(value)
84
+ when 'range'
85
+ set_range(value)
61
86
  else
62
- set_text(value, options)
87
+ set_text(value, **options)
63
88
  end
64
89
  when 'textarea'
65
- set_text(value, options)
90
+ set_text(value, **options)
66
91
  else
67
- if content_editable?
68
- #ensure we are focused on the element
69
- click
70
-
71
- script = <<-JS
72
- var range = document.createRange();
73
- var sel = window.getSelection();
74
- arguments[0].focus();
75
- range.selectNodeContents(arguments[0]);
76
- sel.removeAllRanges();
77
- sel.addRange(range);
78
- JS
79
- driver.execute_script script, self
80
-
81
- if driver.chrome? || driver.firefox?
82
- # chromedriver raises a can't focus element for child elements if we use native.send_keys
83
- # we've already focused it so just use action api
84
- driver.browser.action.send_keys(value.to_s).perform
85
- else
86
- # action api is really slow here just use native.send_keys
87
- native.send_keys(value.to_s)
88
- end
89
- end
92
+ set_content_editable(value)
90
93
  end
91
94
  end
92
95
 
93
96
  def select_option
94
- native.click unless selected? || disabled?
97
+ click unless selected? || disabled?
95
98
  end
96
99
 
97
100
  def unselect_option
98
- raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." if !select_node.multiple?
99
- native.click if selected?
101
+ raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
102
+
103
+ click if selected?
100
104
  end
101
105
 
102
- def click
103
- native.click
104
- rescue => e
105
- if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
106
- e.message =~ /Other element would receive the click/
107
- begin
108
- driver.execute_script("arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'})", self)
109
- rescue
106
+ def click(keys = [], **options)
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
110
118
  end
111
119
  end
120
+ rescue StandardError => e
121
+ if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
122
+ e.message.include?('Other element would receive the click')
123
+ scroll_to_center
124
+ end
125
+
112
126
  raise e
113
127
  end
114
128
 
115
- def right_click
116
- scroll_if_needed do
117
- driver.browser.action.context_click(native).perform
129
+ def right_click(keys = [], **options)
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)
140
+ end
118
141
  end
119
142
  end
120
143
 
121
- def double_click
122
- scroll_if_needed do
123
- driver.browser.action.double_click(native).perform
144
+ def double_click(keys = [], **options)
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)
124
150
  end
125
151
  end
126
152
 
@@ -129,136 +155,482 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
129
155
  end
130
156
 
131
157
  def hover
132
- scroll_if_needed do
133
- driver.browser.action.move_to(native).perform
134
- end
158
+ scroll_if_needed { browser_action.move_to(native).perform }
135
159
  end
136
160
 
137
- def drag_to(element)
138
- scroll_if_needed do
139
- driver.browser.action.drag_and_drop(native, element.native).perform
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
140
171
  end
141
172
  end
142
173
 
143
- def tag_name
144
- native.tag_name.downcase
174
+ def drop(*_)
175
+ raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
145
176
  end
146
177
 
147
- def visible?
148
- displayed = native.displayed?
149
- displayed and displayed != "false"
178
+ def tag_name
179
+ @tag_name ||=
180
+ if native.respond_to? :tag_name
181
+ native.tag_name.downcase
182
+ else
183
+ shadow_root? ? 'ShadowRoot' : 'Unknown'
184
+ end
150
185
  end
151
186
 
152
- def selected?
153
- selected = native.selected?
154
- selected and selected != "false"
155
- end
187
+ def visible?; boolean_attr(native.displayed?); end
188
+ def readonly?; boolean_attr(self[:readonly]); end
189
+ def multiple?; boolean_attr(self[:multiple]); end
190
+ def selected?; boolean_attr(native.selected?); end
156
191
  alias :checked? :selected?
157
192
 
158
193
  def disabled?
159
- # workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
160
- if driver.marionette?
161
- if %w(option optgroup).include? tag_name
162
- !native.enabled? || find_xpath("parent::*[self::optgroup or self::select]")[0].disabled?
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?
198
+ end
199
+
200
+ def content_editable?
201
+ native.attribute('isContentEditable') == 'true'
202
+ end
203
+
204
+ def path
205
+ driver.evaluate_script GET_XPATH_SCRIPT, self
206
+ end
207
+
208
+ def obscured?(x: nil, y: nil)
209
+ res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
210
+ return true if res == true
211
+
212
+ driver.frame_obscured_at?(x: res['x'], y: res['y'])
213
+ end
214
+
215
+ def rect
216
+ native.rect
217
+ end
218
+
219
+ def shadow_root
220
+ root = native.shadow_root
221
+ root && build_node(native.shadow_root)
222
+ end
223
+
224
+ protected
225
+
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
246
+ end
247
+
248
+ private
249
+
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
262
+ end
263
+
264
+ def boolean_attr(val)
265
+ val && (val != 'false')
266
+ end
267
+
268
+ # a reference to the select node if this is an option node
269
+ def select_node
270
+ find_xpath(XPath.ancestor(:select)[1]).first
271
+ end
272
+
273
+ def set_text(value, clear: nil, rapid: nil, **_unused)
274
+ value = value.to_s
275
+ if value.empty? && clear.nil?
276
+ native.clear
277
+ elsif clear == :backspace
278
+ # Clear field by sending the correct number of backspace keys.
279
+ backspaces = [:backspace] * self.value.to_s.length
280
+ send_keys(:end, *backspaces, value)
281
+ elsif clear.is_a? Array
282
+ send_keys(*clear, value)
283
+ else
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..])
163
289
  else
164
- !native.enabled? || !find_xpath("parent::fieldset[@disabled] | ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]").empty?
290
+ send_keys(value)
165
291
  end
166
- else
167
- !native.enabled?
168
292
  end
169
293
  end
170
294
 
171
- def readonly?
172
- readonly = self[:readonly]
173
- readonly and readonly != "false"
295
+ def auto_rapid_set_length
296
+ 30
174
297
  end
175
298
 
176
- def multiple?
177
- multiple = self[:multiple]
178
- multiple and multiple != "false"
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
310
+ end
179
311
  end
180
312
 
181
- def content_editable?
182
- native.attribute('isContentEditable')
313
+ def set_date(value) # rubocop:disable Naming/AccessorMethodName
314
+ value = SettableValue.new(value)
315
+ return set_text(value) unless value.dateable?
316
+
317
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
318
+ update_value_js(value.to_date_str)
183
319
  end
184
320
 
185
- def find_xpath(locator)
186
- native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
321
+ def set_time(value) # rubocop:disable Naming/AccessorMethodName
322
+ value = SettableValue.new(value)
323
+ return set_text(value) unless value.timeable?
324
+
325
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
326
+ update_value_js(value.to_time_str)
187
327
  end
188
328
 
189
- def find_css(locator)
190
- native.find_elements(:css, locator).map { |n| self.class.new(driver, n) }
329
+ def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
330
+ value = SettableValue.new(value)
331
+ return set_text(value) unless value.timeable?
332
+
333
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
334
+ update_value_js(value.to_datetime_str)
191
335
  end
192
336
 
193
- def ==(other)
194
- native == other.native
337
+ def set_color(value) # rubocop:disable Naming/AccessorMethodName
338
+ update_value_js(value)
195
339
  end
196
340
 
197
- def path
198
- path = find_xpath('ancestor::*').reverse
199
- path.unshift self
341
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
342
+ update_value_js(value)
343
+ end
344
+
345
+ def update_value_js(value)
346
+ driver.execute_script(<<-JS, self, value)
347
+ if (arguments[0].readOnly) { return };
348
+ if (document.activeElement !== arguments[0]){
349
+ arguments[0].focus();
350
+ }
351
+ if (arguments[0].value != arguments[1]) {
352
+ arguments[0].value = arguments[1]
353
+ arguments[0].dispatchEvent(new InputEvent('input'));
354
+ arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
355
+ }
356
+ JS
357
+ end
200
358
 
201
- result = []
202
- while node = path.shift
203
- parent = path.first
359
+ def set_file(value) # rubocop:disable Naming/AccessorMethodName
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
204
368
 
205
- if parent
206
- siblings = parent.find_xpath(node.tag_name)
207
- if siblings.size == 1
208
- result.unshift node.tag_name
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
385
+ end
386
+
387
+ def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
388
+ # Ensure we are focused on the element
389
+ click
390
+
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;
402
+ JS
403
+
404
+ # The action api has a speed problem but both chrome and firefox 58 raise errors
405
+ # if we use the faster direct send_keys. For now just send_keys to the element
406
+ # we've already focused.
407
+ # native.send_keys(value.to_s)
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)
209
416
  else
210
- index = siblings.index(node)
211
- result.unshift "#{node.tag_name}[#{index+1}]"
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)
212
424
  end
213
425
  else
214
- result.unshift node.tag_name
426
+ acts.move_to(native)
215
427
  end
216
428
  end
429
+ modifiers_down(actions, click_options.keys)
430
+ yield actions
431
+ modifiers_up(actions, click_options.keys)
432
+ actions.perform
433
+ ensure
434
+ act = browser_action
435
+ act.release_actions if act.respond_to?(:release_actions)
436
+ end
217
437
 
218
- '/' + result.join('/')
438
+ def modifiers_down(actions, keys)
439
+ each_key(keys) { |key| actions.key_down(key) }
440
+ actions
219
441
  end
220
442
 
221
- private
222
- # a reference to the select node if this is an option node
223
- def select_node
224
- find_xpath('./ancestor::select[1]').first
443
+ def modifiers_up(actions, keys)
444
+ each_key(keys) { |key| actions.key_up(key) }
445
+ actions
225
446
  end
226
447
 
227
- def set_text(value, options)
228
- if readonly?
229
- warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
230
- elsif value.to_s.empty? && options[:clear].nil?
231
- native.clear
232
- else
233
- if options[:clear] == :backspace
234
- # Clear field by sending the correct number of backspace keys.
235
- backspaces = [:backspace] * self.value.to_s.length
236
- native.send_keys(*(backspaces + [value.to_s]))
237
- elsif options[:clear] == :none
238
- native.send_keys(value.to_s)
239
- elsif options[:clear].is_a? Array
240
- native.send_keys(*options[:clear], value.to_s)
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
471
+ when :ctrl then :control
472
+ when :command, :cmd then :meta
241
473
  else
242
- # Clear field by JavaScript assignment of the value property.
243
- # Script can change a readonly element which user input cannot, so
244
- # don't execute if readonly.
245
- driver.execute_script "arguments[0].value = ''", self
246
- native.send_keys(value.to_s)
474
+ key
247
475
  end
248
476
  end
249
477
  end
250
478
 
251
- def scroll_if_needed(&block)
252
- block.call
253
- rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
254
- script = <<-JS
255
- try {
256
- arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
257
- } catch(e) {
258
- arguments[0].scrollIntoView(true);
259
- }
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]);
260
500
  JS
261
- driver.execute_script(script, self)
262
- block.call
263
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
264
636
  end