capybara 3.0.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 (312) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/History.md +891 -12
  4. data/License.txt +1 -1
  5. data/README.md +257 -84
  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 +38 -9
  10. data/lib/capybara/dsl.rb +9 -7
  11. data/lib/capybara/helpers.rb +57 -8
  12. data/lib/capybara/minitest/spec.rb +185 -84
  13. data/lib/capybara/minitest.rb +264 -145
  14. data/lib/capybara/node/actions.rb +248 -124
  15. data/lib/capybara/node/base.rb +35 -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 +350 -113
  19. data/lib/capybara/node/finders.rb +104 -82
  20. data/lib/capybara/node/matchers.rb +363 -157
  21. data/lib/capybara/node/simple.rb +54 -15
  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 +9 -10
  25. data/lib/capybara/queries/base_query.rb +25 -18
  26. data/lib/capybara/queries/current_path_query.rb +16 -6
  27. data/lib/capybara/queries/match_query.rb +11 -0
  28. data/lib/capybara/queries/selector_query.rb +617 -104
  29. data/lib/capybara/queries/sibling_query.rb +9 -7
  30. data/lib/capybara/queries/style_query.rb +45 -0
  31. data/lib/capybara/queries/text_query.rb +40 -22
  32. data/lib/capybara/queries/title_query.rb +2 -2
  33. data/lib/capybara/rack_test/browser.rb +106 -31
  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 +74 -49
  37. data/lib/capybara/rack_test/node.rb +120 -47
  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 +87 -53
  44. data/lib/capybara/rspec/features.rb +8 -10
  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 +142 -315
  60. data/lib/capybara/rspec.rb +3 -2
  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 +85 -8
  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 +72 -28
  88. data/lib/capybara/selector/filters/base.rb +45 -2
  89. data/lib/capybara/selector/filters/expression_filter.rb +5 -6
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +18 -4
  92. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  93. data/lib/capybara/selector/selector.rb +89 -200
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +474 -534
  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 +270 -245
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  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 +460 -170
  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 +80 -0
  123. data/lib/capybara/server/checker.rb +44 -0
  124. data/lib/capybara/server/middleware.rb +71 -0
  125. data/lib/capybara/server.rb +58 -67
  126. data/lib/capybara/session/config.rb +40 -6
  127. data/lib/capybara/session/matchers.rb +26 -19
  128. data/lib/capybara/session.rb +252 -194
  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 +126 -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 +135 -44
  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.rb → assert_current_path_spec.rb} +20 -18
  140. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  141. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  142. data/lib/capybara/spec/session/{assert_text.rb → assert_text_spec.rb} +76 -52
  143. data/lib/capybara/spec/session/{assert_title.rb → 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 +11 -13
  146. data/lib/capybara/spec/session/check_spec.rb +112 -51
  147. data/lib/capybara/spec/session/choose_spec.rb +62 -30
  148. data/lib/capybara/spec/session/click_button_spec.rb +227 -161
  149. data/lib/capybara/spec/session/click_link_or_button_spec.rb +49 -30
  150. data/lib/capybara/spec/session/click_link_spec.rb +89 -55
  151. data/lib/capybara/spec/session/current_scope_spec.rb +8 -8
  152. data/lib/capybara/spec/session/current_url_spec.rb +44 -37
  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.rb → assert_match_selector_spec.rb} +11 -9
  156. data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
  157. data/lib/capybara/spec/session/element/match_xpath_spec.rb +8 -6
  158. data/lib/capybara/spec/session/element/matches_selector_spec.rb +70 -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 +110 -44
  163. data/lib/capybara/spec/session/find_button_spec.rb +23 -23
  164. data/lib/capybara/spec/session/find_by_id_spec.rb +8 -8
  165. data/lib/capybara/spec/session/find_field_spec.rb +33 -31
  166. data/lib/capybara/spec/session/find_link_spec.rb +42 -14
  167. data/lib/capybara/spec/session/find_spec.rb +251 -142
  168. data/lib/capybara/spec/session/first_spec.rb +45 -44
  169. data/lib/capybara/spec/session/frame/frame_title_spec.rb +6 -6
  170. data/lib/capybara/spec/session/frame/frame_url_spec.rb +6 -6
  171. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +32 -20
  172. data/lib/capybara/spec/session/frame/within_frame_spec.rb +46 -19
  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 +98 -12
  179. data/lib/capybara/spec/session/has_css_spec.rb +271 -137
  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 +46 -6
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +33 -31
  185. data/lib/capybara/spec/session/has_select_spec.rb +84 -50
  186. data/lib/capybara/spec/session/has_selector_spec.rb +117 -69
  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.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 +37 -0
  195. data/lib/capybara/spec/session/node_spec.rb +958 -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 +65 -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 +119 -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 +41 -22
  212. data/lib/capybara/spec/session/unselect_spec.rb +37 -37
  213. data/lib/capybara/spec/session/visit_spec.rb +79 -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 +11 -8
  221. data/lib/capybara/spec/session/window/within_window_spec.rb +17 -16
  222. data/lib/capybara/spec/session/within_spec.rb +82 -44
  223. data/lib/capybara/spec/spec_helper.rb +46 -52
  224. data/lib/capybara/spec/test_app.rb +148 -41
  225. data/lib/capybara/spec/views/animated.erb +49 -0
  226. data/lib/capybara/spec/views/form.erb +156 -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 +46 -11
  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.erb +2 -2
  255. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  256. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  257. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  258. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  259. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  260. data/lib/capybara/spec/views/with_windows.erb +1 -1
  261. data/lib/capybara/spec/views/within_frames.erb +1 -1
  262. data/lib/capybara/version.rb +1 -1
  263. data/lib/capybara/window.rb +32 -26
  264. data/lib/capybara.rb +128 -104
  265. data/spec/basic_node_spec.rb +59 -34
  266. data/spec/capybara_spec.rb +65 -51
  267. data/spec/counter_spec.rb +35 -0
  268. data/spec/css_builder_spec.rb +101 -0
  269. data/spec/css_splitter_spec.rb +38 -0
  270. data/spec/dsl_spec.rb +84 -55
  271. data/spec/filter_set_spec.rb +24 -7
  272. data/spec/fixtures/certificate.pem +25 -0
  273. data/spec/fixtures/key.pem +27 -0
  274. data/spec/fixtures/selenium_driver_rspec_failure.rb +5 -5
  275. data/spec/fixtures/selenium_driver_rspec_success.rb +5 -5
  276. data/spec/minitest_spec.rb +49 -5
  277. data/spec/minitest_spec_spec.rb +92 -62
  278. data/spec/per_session_config_spec.rb +6 -6
  279. data/spec/rack_test_spec.rb +183 -115
  280. data/spec/regexp_dissassembler_spec.rb +250 -0
  281. data/spec/result_spec.rb +99 -39
  282. data/spec/rspec/features_spec.rb +28 -25
  283. data/spec/rspec/scenarios_spec.rb +10 -6
  284. data/spec/rspec/shared_spec_matchers.rb +418 -364
  285. data/spec/rspec/views_spec.rb +4 -3
  286. data/spec/rspec_matchers_spec.rb +35 -10
  287. data/spec/rspec_spec.rb +109 -85
  288. data/spec/sauce_spec_chrome.rb +43 -0
  289. data/spec/selector_spec.rb +392 -62
  290. data/spec/selenium_spec_chrome.rb +183 -41
  291. data/spec/selenium_spec_chrome_remote.rb +96 -0
  292. data/spec/selenium_spec_edge.rb +41 -8
  293. data/spec/selenium_spec_firefox.rb +228 -0
  294. data/spec/selenium_spec_firefox_remote.rb +94 -0
  295. data/spec/selenium_spec_ie.rb +129 -11
  296. data/spec/selenium_spec_safari.rb +162 -0
  297. data/spec/server_spec.rb +192 -81
  298. data/spec/session_spec.rb +52 -16
  299. data/spec/shared_selenium_node.rb +79 -0
  300. data/spec/shared_selenium_session.rb +460 -123
  301. data/spec/spec_helper.rb +124 -2
  302. data/spec/whitespace_normalizer_spec.rb +54 -0
  303. data/spec/xpath_builder_spec.rb +93 -0
  304. metadata +344 -45
  305. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  306. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  307. data/.yard/yard_extensions.rb +0 -78
  308. data/lib/capybara/rspec/compound.rb +0 -90
  309. data/lib/capybara/spec/session/assert_selector.rb +0 -149
  310. data/lib/capybara/spec/session/source_spec.rb +0 -0
  311. data/lib/capybara/spec/views/with_title.erb +0 -5
  312. data/spec/selenium_spec_marionette.rb +0 -143
@@ -1,30 +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
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
- )
10
+ include Capybara::Node::WhitespaceNormalizer
11
+ include Capybara::Selenium::Find
12
+ include Capybara::Selenium::Scroll
16
13
 
17
14
  def visible_text
15
+ raise NotImplementedError, 'Getting visible text is not currently supported directly on shadow roots' if shadow_root?
16
+
18
17
  native.text
19
18
  end
20
19
 
21
20
  def all_text
22
- text = driver.execute_script("return arguments[0].textContent", self)
23
- text.gsub(/[\u200b\u200e\u200f]/, '')
24
- .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
25
- .gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
26
- .gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
27
- .tr("\u00a0", ' ')
21
+ text = driver.evaluate_script('arguments[0].textContent', self) || ''
22
+ normalize_spacing(text)
28
23
  end
29
24
 
30
25
  def [](name)
@@ -34,13 +29,17 @@ 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.to_h { |style| [style, native.css_value(style)] }
41
+ end
42
+
44
43
  ##
45
44
  #
46
45
  # Set the value of the form element to the given value.
@@ -52,12 +51,22 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
52
51
  # :none => append the new value to the existing value <br/>
53
52
  # :backspace => send backspace keystrokes to clear the field <br/>
54
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/>
55
59
  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?
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
57
66
 
58
67
  case tag_name
59
68
  when 'input'
60
- case self[:type]
69
+ case type
61
70
  when 'radio'
62
71
  click
63
72
  when 'checkbox'
@@ -70,71 +79,74 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
70
79
  set_time(value)
71
80
  when 'datetime-local'
72
81
  set_datetime_local(value)
82
+ when 'color'
83
+ set_color(value)
84
+ when 'range'
85
+ set_range(value)
73
86
  else
74
- set_text(value, options)
87
+ set_text(value, **options)
75
88
  end
76
89
  when 'textarea'
77
- set_text(value, options)
90
+ set_text(value, **options)
78
91
  else
79
- set_content_editable(value) if content_editable?
92
+ set_content_editable(value)
80
93
  end
81
94
  end
82
95
 
83
96
  def select_option
84
- native.click unless selected? || disabled?
97
+ click unless selected? || disabled?
85
98
  end
86
99
 
87
100
  def unselect_option
88
- raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." unless select_node.multiple?
89
- native.click if selected?
101
+ raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
102
+
103
+ click if selected?
90
104
  end
91
105
 
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
- end
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
104
118
  end
105
119
  end
106
- rescue => e
120
+ rescue StandardError => e
107
121
  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
122
+ e.message.include?('Other element would receive the click')
123
+ scroll_to_center
113
124
  end
125
+
114
126
  raise e
115
127
  end
116
128
 
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
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)
125
140
  end
126
141
  end
127
142
  end
128
143
 
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
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)
138
150
  end
139
151
  end
140
152
 
@@ -143,15 +155,33 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
143
155
  end
144
156
 
145
157
  def hover
146
- 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
147
172
  end
148
173
 
149
- def drag_to(element)
150
- 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'
151
176
  end
152
177
 
153
178
  def tag_name
154
- 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
155
185
  end
156
186
 
157
187
  def visible?; boolean_attr(native.displayed?); end
@@ -163,55 +193,76 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
163
193
  def disabled?
164
194
  return true unless native.enabled?
165
195
 
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
196
+ # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
197
+ find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
176
198
  end
177
199
 
178
200
  def content_editable?
179
- native.attribute('isContentEditable')
201
+ native.attribute('isContentEditable') == 'true'
202
+ end
203
+
204
+ def path
205
+ driver.evaluate_script GET_XPATH_SCRIPT, self
180
206
  end
181
207
 
182
- def find_xpath(locator)
183
- native.find_elements(:xpath, 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'])
184
213
  end
185
214
 
186
- def find_css(locator)
187
- native.find_elements(:css, locator).map { |n| self.class.new(driver, n) }
215
+ def rect
216
+ native.rect
188
217
  end
189
218
 
190
- def ==(other)
191
- native == other.native
219
+ def shadow_root
220
+ root = native.shadow_root
221
+ root && build_node(native.shadow_root)
192
222
  end
193
223
 
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
224
+ protected
225
+
226
+ def scroll_if_needed
227
+ yield
228
+ rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
229
+ scroll_to_center
230
+ yield
231
+ end
207
232
 
208
- '/' + result.reverse.join('/')
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
209
246
  end
210
247
 
211
248
  private
212
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
+
213
264
  def boolean_attr(val)
214
- val and val != "false"
265
+ val && (val != 'false')
215
266
  end
216
267
 
217
268
  # a reference to the select node if this is an option node
@@ -219,71 +270,117 @@ private
219
270
  find_xpath(XPath.ancestor(:select)[1]).first
220
271
  end
221
272
 
222
- def set_text(value, clear: nil, **_unused)
223
- 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?
224
276
  native.clear
225
277
  elsif clear == :backspace
226
278
  # Clear field by sending the correct number of backspace keys.
227
279
  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)
280
+ send_keys(:end, *backspaces, value)
231
281
  elsif clear.is_a? Array
232
- native.send_keys(*clear, value.to_s)
282
+ send_keys(*clear, value)
233
283
  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)
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
239
292
  end
240
293
  end
241
294
 
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
295
+ def auto_rapid_set_length
296
+ 30
254
297
  end
255
298
 
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)
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
261
310
  end
262
311
  end
263
312
 
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)
319
+ end
320
+
264
321
  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
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)
270
327
  end
271
328
 
272
329
  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
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)
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)
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
278
357
  end
279
358
 
280
359
  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) }
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
285
382
  else
286
- native.send_keys(Array(path_names).join("\n"))
383
+ yield
287
384
  end
288
385
  end
289
386
 
@@ -291,56 +388,249 @@ private
291
388
  # Ensure we are focused on the element
292
389
  click
293
390
 
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);
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;
301
402
  JS
302
- driver.execute_script script, self
303
403
 
304
404
  # The action api has a speed problem but both chrome and firefox 58 raise errors
305
405
  # if we use the faster direct send_keys. For now just send_keys to the element
306
406
  # we've already focused.
307
407
  # native.send_keys(value.to_s)
308
- driver.browser.action.send_keys(value.to_s).perform
408
+ browser_action.send_keys(value.to_s).perform if editable
309
409
  end
310
410
 
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)
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)
315
430
  yield actions
316
- modifiers_up(actions, keys)
431
+ modifiers_up(actions, click_options.keys)
317
432
  actions.perform
318
433
  ensure
319
- a = driver.browser.action
320
- a.release_actions if a.respond_to?(:release_actions)
434
+ act = browser_action
435
+ act.release_actions if act.respond_to?(:release_actions)
321
436
  end
322
437
 
323
438
  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
439
+ each_key(keys) { |key| actions.key_down(key) }
440
+ actions
333
441
  end
334
442
 
335
443
  def modifiers_up(actions, keys)
336
- keys.each do |key|
337
- 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
338
471
  when :ctrl then :control
339
472
  when :command, :cmd then :meta
340
473
  else
341
474
  key
342
475
  end
343
- actions.key_up(key)
344
476
  end
345
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
346
636
  end