capybara 2.7.0 → 3.35.3

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 (318) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +1 -0
  3. data/History.md +1147 -11
  4. data/License.txt +1 -1
  5. data/README.md +252 -131
  6. data/lib/capybara/config.rb +92 -0
  7. data/lib/capybara/cucumber.rb +3 -3
  8. data/lib/capybara/driver/base.rb +52 -21
  9. data/lib/capybara/driver/node.rb +48 -14
  10. data/lib/capybara/dsl.rb +16 -9
  11. data/lib/capybara/helpers.rb +72 -81
  12. data/lib/capybara/minitest/spec.rb +267 -0
  13. data/lib/capybara/minitest.rb +385 -0
  14. data/lib/capybara/node/actions.rb +337 -89
  15. data/lib/capybara/node/base.rb +50 -32
  16. data/lib/capybara/node/document.rb +19 -3
  17. data/lib/capybara/node/document_matchers.rb +22 -24
  18. data/lib/capybara/node/element.rb +388 -125
  19. data/lib/capybara/node/finders.rb +231 -121
  20. data/lib/capybara/node/matchers.rb +503 -217
  21. data/lib/capybara/node/simple.rb +64 -27
  22. data/lib/capybara/queries/ancestor_query.rb +27 -0
  23. data/lib/capybara/queries/base_query.rb +87 -11
  24. data/lib/capybara/queries/current_path_query.rb +24 -24
  25. data/lib/capybara/queries/match_query.rb +15 -10
  26. data/lib/capybara/queries/selector_query.rb +675 -81
  27. data/lib/capybara/queries/sibling_query.rb +26 -0
  28. data/lib/capybara/queries/style_query.rb +45 -0
  29. data/lib/capybara/queries/text_query.rb +88 -20
  30. data/lib/capybara/queries/title_query.rb +9 -11
  31. data/lib/capybara/rack_test/browser.rb +63 -39
  32. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  33. data/lib/capybara/rack_test/driver.rb +26 -16
  34. data/lib/capybara/rack_test/errors.rb +6 -0
  35. data/lib/capybara/rack_test/form.rb +73 -58
  36. data/lib/capybara/rack_test/node.rb +187 -67
  37. data/lib/capybara/rails.rb +4 -8
  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 +142 -14
  43. data/lib/capybara/rspec/features.rb +17 -42
  44. data/lib/capybara/rspec/matcher_proxies.rb +82 -0
  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 +143 -244
  59. data/lib/capybara/rspec.rb +10 -12
  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 +102 -0
  63. data/lib/capybara/selector/definition/button.rb +63 -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 +278 -0
  85. data/lib/capybara/selector/filter.rb +3 -46
  86. data/lib/capybara/selector/filter_set.rb +124 -0
  87. data/lib/capybara/selector/filters/base.rb +77 -0
  88. data/lib/capybara/selector/filters/expression_filter.rb +22 -0
  89. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  90. data/lib/capybara/selector/filters/node_filter.rb +31 -0
  91. data/lib/capybara/selector/regexp_disassembler.rb +214 -0
  92. data/lib/capybara/selector/selector.rb +155 -0
  93. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  94. data/lib/capybara/selector.rb +232 -369
  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 +380 -142
  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 +528 -97
  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 +63 -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 +74 -71
  127. data/lib/capybara/session/config.rb +126 -0
  128. data/lib/capybara/session/matchers.rb +44 -27
  129. data/lib/capybara/session.rb +500 -297
  130. data/lib/capybara/spec/fixtures/no_extension +1 -0
  131. data/lib/capybara/spec/public/jquery.js +5 -5
  132. data/lib/capybara/spec/public/offset.js +6 -0
  133. data/lib/capybara/spec/public/test.js +168 -14
  134. data/lib/capybara/spec/session/accept_alert_spec.rb +37 -14
  135. data/lib/capybara/spec/session/accept_confirm_spec.rb +7 -6
  136. data/lib/capybara/spec/session/accept_prompt_spec.rb +38 -10
  137. data/lib/capybara/spec/session/all_spec.rb +179 -59
  138. data/lib/capybara/spec/session/ancestor_spec.rb +88 -0
  139. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +140 -0
  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_spec.rb +93 -0
  145. data/lib/capybara/spec/session/attach_file_spec.rb +154 -48
  146. data/lib/capybara/spec/session/body_spec.rb +12 -13
  147. data/lib/capybara/spec/session/check_spec.rb +168 -41
  148. data/lib/capybara/spec/session/choose_spec.rb +75 -23
  149. data/lib/capybara/spec/session/click_button_spec.rb +243 -175
  150. data/lib/capybara/spec/session/click_link_or_button_spec.rb +57 -32
  151. data/lib/capybara/spec/session/click_link_spec.rb +100 -53
  152. data/lib/capybara/spec/session/current_scope_spec.rb +11 -10
  153. data/lib/capybara/spec/session/current_url_spec.rb +61 -35
  154. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +7 -7
  155. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +5 -4
  156. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +13 -6
  157. data/lib/capybara/spec/session/element/match_css_spec.rb +21 -7
  158. data/lib/capybara/spec/session/element/match_xpath_spec.rb +9 -7
  159. data/lib/capybara/spec/session/element/matches_selector_spec.rb +91 -34
  160. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +23 -0
  161. data/lib/capybara/spec/session/evaluate_script_spec.rb +45 -3
  162. data/lib/capybara/spec/session/execute_script_spec.rb +24 -4
  163. data/lib/capybara/spec/session/fill_in_spec.rb +166 -64
  164. data/lib/capybara/spec/session/find_button_spec.rb +37 -18
  165. data/lib/capybara/spec/session/find_by_id_spec.rb +10 -9
  166. data/lib/capybara/spec/session/find_field_spec.rb +57 -34
  167. data/lib/capybara/spec/session/find_link_spec.rb +47 -10
  168. data/lib/capybara/spec/session/find_spec.rb +290 -144
  169. data/lib/capybara/spec/session/first_spec.rb +91 -48
  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 +116 -0
  173. data/lib/capybara/spec/session/frame/within_frame_spec.rb +112 -0
  174. data/lib/capybara/spec/session/go_back_spec.rb +3 -2
  175. data/lib/capybara/spec/session/go_forward_spec.rb +3 -2
  176. data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
  177. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  178. data/lib/capybara/spec/session/has_any_selectors_spec.rb +25 -0
  179. data/lib/capybara/spec/session/has_button_spec.rb +76 -19
  180. data/lib/capybara/spec/session/has_css_spec.rb +277 -131
  181. data/lib/capybara/spec/session/has_current_path_spec.rb +98 -26
  182. data/lib/capybara/spec/session/has_field_spec.rb +177 -107
  183. data/lib/capybara/spec/session/has_link_spec.rb +13 -12
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +78 -0
  185. data/lib/capybara/spec/session/has_select_spec.rb +191 -95
  186. data/lib/capybara/spec/session/has_selector_spec.rb +128 -64
  187. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  188. data/lib/capybara/spec/session/has_table_spec.rb +172 -5
  189. data/lib/capybara/spec/session/has_text_spec.rb +126 -60
  190. data/lib/capybara/spec/session/has_title_spec.rb +35 -12
  191. data/lib/capybara/spec/session/has_xpath_spec.rb +74 -53
  192. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +3 -2
  193. data/lib/capybara/spec/session/html_spec.rb +14 -6
  194. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  195. data/lib/capybara/spec/session/node_spec.rb +1028 -131
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +39 -0
  197. data/lib/capybara/spec/session/refresh_spec.rb +34 -0
  198. data/lib/capybara/spec/session/reset_session_spec.rb +75 -34
  199. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +2 -1
  200. data/lib/capybara/spec/session/save_and_open_page_spec.rb +3 -2
  201. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +11 -15
  202. data/lib/capybara/spec/session/save_page_spec.rb +42 -55
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +16 -14
  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 +112 -85
  207. data/lib/capybara/spec/session/selectors_spec.rb +71 -8
  208. data/lib/capybara/spec/session/sibling_spec.rb +52 -0
  209. data/lib/capybara/spec/session/text_spec.rb +38 -23
  210. data/lib/capybara/spec/session/title_spec.rb +17 -5
  211. data/lib/capybara/spec/session/uncheck_spec.rb +71 -12
  212. data/lib/capybara/spec/session/unselect_spec.rb +44 -43
  213. data/lib/capybara/spec/session/visit_spec.rb +99 -32
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +33 -29
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +5 -3
  216. data/lib/capybara/spec/session/window/open_new_window_spec.rb +5 -3
  217. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +39 -30
  218. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +17 -10
  219. data/lib/capybara/spec/session/window/window_spec.rb +121 -73
  220. data/lib/capybara/spec/session/window/windows_spec.rb +12 -10
  221. data/lib/capybara/spec/session/window/within_window_spec.rb +52 -82
  222. data/lib/capybara/spec/session/within_spec.rb +76 -43
  223. data/lib/capybara/spec/spec_helper.rb +67 -33
  224. data/lib/capybara/spec/test_app.rb +85 -36
  225. data/lib/capybara/spec/views/animated.erb +49 -0
  226. data/lib/capybara/spec/views/buttons.erb +1 -1
  227. data/lib/capybara/spec/views/fieldsets.erb +1 -1
  228. data/lib/capybara/spec/views/form.erb +227 -20
  229. data/lib/capybara/spec/views/frame_child.erb +10 -2
  230. data/lib/capybara/spec/views/frame_one.erb +2 -1
  231. data/lib/capybara/spec/views/frame_parent.erb +2 -2
  232. data/lib/capybara/spec/views/frame_two.erb +1 -1
  233. data/lib/capybara/spec/views/header_links.erb +1 -1
  234. data/lib/capybara/spec/views/host_links.erb +1 -1
  235. data/lib/capybara/spec/views/initial_alert.erb +10 -0
  236. data/lib/capybara/spec/views/obscured.erb +47 -0
  237. data/lib/capybara/spec/views/offset.erb +32 -0
  238. data/lib/capybara/spec/views/path.erb +1 -1
  239. data/lib/capybara/spec/views/popup_one.erb +1 -1
  240. data/lib/capybara/spec/views/popup_two.erb +1 -1
  241. data/lib/capybara/spec/views/postback.erb +1 -1
  242. data/lib/capybara/spec/views/react.erb +45 -0
  243. data/lib/capybara/spec/views/scroll.erb +20 -0
  244. data/lib/capybara/spec/views/spatial.erb +31 -0
  245. data/lib/capybara/spec/views/tables.erb +69 -2
  246. data/lib/capybara/spec/views/with_animation.erb +82 -0
  247. data/lib/capybara/spec/views/with_base_tag.erb +1 -1
  248. data/lib/capybara/spec/views/with_count.erb +1 -1
  249. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  250. data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
  251. data/lib/capybara/spec/views/with_hover.erb +7 -1
  252. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  253. data/lib/capybara/spec/views/with_html.erb +100 -10
  254. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  255. data/lib/capybara/spec/views/with_html_entities.erb +1 -1
  256. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  257. data/lib/capybara/spec/views/with_js.erb +49 -3
  258. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  259. data/lib/capybara/spec/views/with_namespace.erb +20 -0
  260. data/lib/capybara/spec/views/with_scope.erb +1 -1
  261. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  262. data/lib/capybara/spec/views/with_simple_html.erb +1 -1
  263. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  264. data/lib/capybara/spec/views/with_title.erb +1 -1
  265. data/lib/capybara/spec/views/with_unload_alert.erb +3 -1
  266. data/lib/capybara/spec/views/with_windows.erb +7 -1
  267. data/lib/capybara/spec/views/within_frames.erb +6 -3
  268. data/lib/capybara/version.rb +2 -1
  269. data/lib/capybara/window.rb +39 -21
  270. data/lib/capybara.rb +208 -186
  271. data/spec/basic_node_spec.rb +52 -39
  272. data/spec/capybara_spec.rb +72 -50
  273. data/spec/css_builder_spec.rb +101 -0
  274. data/spec/css_splitter_spec.rb +38 -0
  275. data/spec/dsl_spec.rb +81 -61
  276. data/spec/filter_set_spec.rb +46 -0
  277. data/spec/fixtures/capybara.csv +1 -0
  278. data/spec/fixtures/certificate.pem +25 -0
  279. data/spec/fixtures/key.pem +27 -0
  280. data/spec/fixtures/selenium_driver_rspec_failure.rb +7 -3
  281. data/spec/fixtures/selenium_driver_rspec_success.rb +7 -3
  282. data/spec/minitest_spec.rb +164 -0
  283. data/spec/minitest_spec_spec.rb +162 -0
  284. data/spec/per_session_config_spec.rb +68 -0
  285. data/spec/rack_test_spec.rb +189 -96
  286. data/spec/regexp_dissassembler_spec.rb +250 -0
  287. data/spec/result_spec.rb +143 -13
  288. data/spec/rspec/features_spec.rb +38 -32
  289. data/spec/rspec/scenarios_spec.rb +9 -7
  290. data/spec/rspec/shared_spec_matchers.rb +959 -0
  291. data/spec/rspec/views_spec.rb +9 -3
  292. data/spec/rspec_matchers_spec.rb +62 -0
  293. data/spec/rspec_spec.rb +127 -30
  294. data/spec/sauce_spec_chrome.rb +43 -0
  295. data/spec/selector_spec.rb +458 -37
  296. data/spec/selenium_spec_chrome.rb +196 -9
  297. data/spec/selenium_spec_chrome_remote.rb +100 -0
  298. data/spec/selenium_spec_edge.rb +47 -0
  299. data/spec/selenium_spec_firefox.rb +210 -0
  300. data/spec/selenium_spec_firefox_remote.rb +80 -0
  301. data/spec/selenium_spec_ie.rb +150 -0
  302. data/spec/selenium_spec_safari.rb +148 -0
  303. data/spec/server_spec.rb +200 -101
  304. data/spec/session_spec.rb +91 -0
  305. data/spec/shared_selenium_node.rb +83 -0
  306. data/spec/shared_selenium_session.rb +558 -0
  307. data/spec/spec_helper.rb +94 -2
  308. data/spec/xpath_builder_spec.rb +93 -0
  309. metadata +420 -60
  310. data/lib/capybara/query.rb +0 -7
  311. data/lib/capybara/spec/session/assert_current_path.rb +0 -60
  312. data/lib/capybara/spec/session/assert_selector.rb +0 -148
  313. data/lib/capybara/spec/session/assert_text.rb +0 -196
  314. data/lib/capybara/spec/session/assert_title.rb +0 -70
  315. data/lib/capybara/spec/session/source_spec.rb +0 -0
  316. data/lib/capybara/spec/session/within_frame_spec.rb +0 -53
  317. data/spec/rspec/matchers_spec.rb +0 -827
  318. data/spec/selenium_spec.rb +0 -151
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Capybara::Selenium::Node
4
+ module Html5Drag
5
+ # Implement methods to emulate HTML5 drag and drop
6
+
7
+ def drag_to(element, html5: nil, delay: 0.05, drop_modifiers: [])
8
+ drop_modifiers = Array(drop_modifiers)
9
+
10
+ driver.execute_script MOUSEDOWN_TRACKER
11
+ scroll_if_needed { browser_action.click_and_hold(native).perform }
12
+ html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
13
+ if html5
14
+ perform_html5_drag(element, delay, drop_modifiers)
15
+ else
16
+ perform_legacy_drag(element, drop_modifiers)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def perform_legacy_drag(element, drop_modifiers)
23
+ element.scroll_if_needed do
24
+ # browser_action.move_to(element.native).release.perform
25
+ keys_down = modifiers_down(browser_action, drop_modifiers)
26
+ keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
27
+ keys_up.perform
28
+ end
29
+ end
30
+
31
+ def perform_html5_drag(element, delay, drop_modifiers)
32
+ driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000, normalize_keys(drop_modifiers)
33
+ browser_action.release.perform
34
+ end
35
+
36
+ def html5_drop(*args)
37
+ if args[0].is_a? String
38
+ input = driver.evaluate_script ATTACH_FILE
39
+ input.set_file(args)
40
+ driver.execute_script DROP_FILE, self, input
41
+ else
42
+ items = args.each_with_object([]) do |arg, arr|
43
+ arg.each_with_object(arr) do |(type, data), arr_|
44
+ arr_ << { type: type, data: data }
45
+ end
46
+ end
47
+ driver.execute_script DROP_STRING, items, self
48
+ end
49
+ end
50
+
51
+ DROP_STRING = <<~JS
52
+ var strings = arguments[0],
53
+ el = arguments[1],
54
+ dt = new DataTransfer(),
55
+ opts = { cancelable: true, bubbles: true, dataTransfer: dt };
56
+ for (var i=0; i < strings.length; i++){
57
+ if (dt.items) {
58
+ dt.items.add(strings[i]['data'], strings[i]['type']);
59
+ } else {
60
+ dt.setData(strings[i]['type'], strings[i]['data']);
61
+ }
62
+ }
63
+ var dropEvent = new DragEvent('drop', opts);
64
+ el.dispatchEvent(dropEvent);
65
+ JS
66
+
67
+ DROP_FILE = <<~JS
68
+ var el = arguments[0],
69
+ input = arguments[1],
70
+ files = input.files,
71
+ dt = new DataTransfer(),
72
+ opts = { cancelable: true, bubbles: true, dataTransfer: dt };
73
+ input.parentElement.removeChild(input);
74
+ if (dt.items){
75
+ for (var i=0; i<files.length; i++){
76
+ dt.items.add(files[i]);
77
+ }
78
+ } else {
79
+ Object.defineProperty(dt, "files", {
80
+ value: files,
81
+ writable: false
82
+ });
83
+ }
84
+ var dropEvent = new DragEvent('drop', opts);
85
+ el.dispatchEvent(dropEvent);
86
+ JS
87
+
88
+ ATTACH_FILE = <<~JS
89
+ (function(){
90
+ var input = document.createElement('INPUT');
91
+ input.type = "file";
92
+ input.id = "_capybara_drop_file";
93
+ input.multiple = true;
94
+ document.body.appendChild(input);
95
+ return input;
96
+ })()
97
+ JS
98
+
99
+ MOUSEDOWN_TRACKER = <<~JS
100
+ window.capybara_mousedown_prevented = null;
101
+ document.addEventListener('mousedown', ev => {
102
+ window.capybara_mousedown_prevented = ev.defaultPrevented;
103
+ }, { once: true, passive: true })
104
+ JS
105
+
106
+ LEGACY_DRAG_CHECK = <<~JS
107
+ (function(el){
108
+ if ([true, null].indexOf(window.capybara_mousedown_prevented) >= 0){
109
+ return true;
110
+ }
111
+
112
+ do {
113
+ if (el.draggable) return false;
114
+ } while (el = el.parentElement );
115
+ return true;
116
+ })(arguments[0])
117
+ JS
118
+
119
+ HTML5_DRAG_DROP_SCRIPT = <<~JS
120
+ function rectCenter(rect){
121
+ return new DOMPoint(
122
+ (rect.left + rect.right)/2,
123
+ (rect.top + rect.bottom)/2
124
+ );
125
+ }
126
+
127
+ function pointOnRect(pt, rect) {
128
+ var rectPt = rectCenter(rect);
129
+ var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);
130
+
131
+ if (pt.x <= rectPt.x) { // left side
132
+ var minXy = slope * (rect.left - pt.x) + pt.y;
133
+ if (rect.top <= minXy && minXy <= rect.bottom)
134
+ return new DOMPoint(rect.left, minXy);
135
+ }
136
+
137
+ if (pt.x >= rectPt.x) { // right side
138
+ var maxXy = slope * (rect.right - pt.x) + pt.y;
139
+ if (rect.top <= maxXy && maxXy <= rect.bottom)
140
+ return new DOMPoint(rect.right, maxXy);
141
+ }
142
+
143
+ if (pt.y <= rectPt.y) { // top side
144
+ var minYx = (rectPt.top - pt.y) / slope + pt.x;
145
+ if (rect.left <= minYx && minYx <= rect.right)
146
+ return new DOMPoint(minYx, rect.top);
147
+ }
148
+
149
+ if (pt.y >= rectPt.y) { // bottom side
150
+ var maxYx = (rect.bottom - pt.y) / slope + pt.x;
151
+ if (rect.left <= maxYx && maxYx <= rect.right)
152
+ return new DOMPoint(maxYx, rect.bottom);
153
+ }
154
+
155
+ return new DOMPoint(pt.x,pt.y);
156
+ }
157
+
158
+ function dragEnterTarget() {
159
+ target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
160
+ var targetRect = target.getBoundingClientRect();
161
+ var sourceCenter = rectCenter(source.getBoundingClientRect());
162
+
163
+ for (var i = 0; i < drop_modifier_keys.length; i++) {
164
+ key = drop_modifier_keys[i];
165
+ if (key == "control"){
166
+ key = "ctrl"
167
+ }
168
+ opts[key + 'Key'] = true;
169
+ }
170
+
171
+ // fire 2 dragover events to simulate dragging with a direction
172
+ var entryPoint = pointOnRect(sourceCenter, targetRect)
173
+ var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
174
+ var dragOverEvent = new DragEvent('dragover', dragOverOpts);
175
+ target.dispatchEvent(dragOverEvent);
176
+ window.setTimeout(dragOnTarget, step_delay);
177
+ }
178
+
179
+ function dragOnTarget() {
180
+ var targetCenter = rectCenter(target.getBoundingClientRect());
181
+ var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
182
+ var dragOverEvent = new DragEvent('dragover', dragOverOpts);
183
+ target.dispatchEvent(dragOverEvent);
184
+ window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
185
+ }
186
+
187
+ function dragLeave(drop, dragOverOpts) {
188
+ var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
189
+ var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
190
+ target.dispatchEvent(dragLeaveEvent);
191
+ if (drop) {
192
+ var dropEvent = new DragEvent('drop', dragLeaveOptions);
193
+ target.dispatchEvent(dropEvent);
194
+ }
195
+ var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
196
+ source.dispatchEvent(dragEndEvent);
197
+ callback.call(true);
198
+ }
199
+
200
+ var source = arguments[0],
201
+ target = arguments[1],
202
+ step_delay = arguments[2],
203
+ drop_modifier_keys = arguments[3],
204
+ callback = arguments[4];
205
+
206
+ var dt = new DataTransfer();
207
+ var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
208
+
209
+ while (source && !source.draggable) {
210
+ source = source.parentElement;
211
+ }
212
+
213
+ if (source.tagName == 'A'){
214
+ dt.setData('text/uri-list', source.href);
215
+ dt.setData('text', source.href);
216
+ }
217
+ if (source.tagName == 'IMG'){
218
+ dt.setData('text/uri-list', source.src);
219
+ dt.setData('text', source.src);
220
+ }
221
+
222
+ var dragEvent = new DragEvent('dragstart', opts);
223
+ source.dispatchEvent(dragEvent);
224
+
225
+ window.setTimeout(dragEnterTarget, step_delay);
226
+ JS
227
+ end
228
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Capybara::Selenium::Node
4
+ #
5
+ # @api private
6
+ #
7
+ class ModifierKeysStack
8
+ def initialize
9
+ @stack = []
10
+ end
11
+
12
+ def include?(key)
13
+ @stack.flatten.include?(key)
14
+ end
15
+
16
+ def press(key)
17
+ @stack.last.push(key)
18
+ end
19
+
20
+ def push
21
+ @stack.push []
22
+ end
23
+
24
+ def pop
25
+ @stack.pop
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module Scroll
6
+ def scroll_by(x, y)
7
+ driver.execute_script <<~JS, self, x, y
8
+ var el = arguments[0];
9
+ if (el.scrollBy){
10
+ el.scrollBy(arguments[1], arguments[2]);
11
+ } else {
12
+ el.scrollTop = el.scrollTop + arguments[2];
13
+ el.scrollLeft = el.scrollLeft + arguments[1];
14
+ }
15
+ JS
16
+ end
17
+
18
+ def scroll_to(element, location, position = nil)
19
+ # location, element = element, nil if element.is_a? Symbol
20
+ if element.is_a? Capybara::Selenium::Node
21
+ scroll_element_to_location(element, location)
22
+ elsif location.is_a? Symbol
23
+ scroll_to_location(location)
24
+ else
25
+ scroll_to_coords(*position)
26
+ end
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ def scroll_element_to_location(element, location)
33
+ scroll_opts = case location
34
+ when :top
35
+ 'true'
36
+ when :bottom
37
+ 'false'
38
+ when :center
39
+ "{behavior: 'instant', block: 'center'}"
40
+ else
41
+ raise ArgumentError, "Invalid scroll_to location: #{location}"
42
+ end
43
+ driver.execute_script <<~JS, element
44
+ arguments[0].scrollIntoView(#{scroll_opts})
45
+ JS
46
+ end
47
+
48
+ SCROLL_POSITIONS = {
49
+ top: '0',
50
+ bottom: 'arguments[0].scrollHeight',
51
+ center: '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
52
+ }.freeze
53
+
54
+ def scroll_to_location(location)
55
+ driver.execute_script <<~JS, self
56
+ if (arguments[0].scrollTo){
57
+ arguments[0].scrollTo(0, #{SCROLL_POSITIONS[location]});
58
+ } else {
59
+ arguments[0].scrollTop = #{SCROLL_POSITIONS[location]};
60
+ }
61
+ JS
62
+ end
63
+
64
+ def scroll_to_coords(x, y)
65
+ driver.execute_script <<~JS, self, x, y
66
+ if (arguments[0].scrollTo){
67
+ arguments[0].scrollTo(arguments[1], arguments[2]);
68
+ } else {
69
+ arguments[0].scrollTop = arguments[2];
70
+ arguments[0].scrollLeft = arguments[1];
71
+ }
72
+ JS
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module DeprecationSuppressor
6
+ def initialize(*)
7
+ @suppress_for_capybara = false
8
+ super
9
+ end
10
+
11
+ def deprecate(*args, **opts, &block)
12
+ return if @suppress_for_capybara
13
+
14
+ if opts.empty?
15
+ super(*args, &block) # support Selenium 3
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def suppress_deprecations
22
+ prev_suppress_for_capybara, @suppress_for_capybara = @suppress_for_capybara, true
23
+ yield
24
+ ensure
25
+ @suppress_for_capybara = prev_suppress_for_capybara
26
+ end
27
+ end
28
+
29
+ module ErrorSuppressor
30
+ def for_code(*)
31
+ ::Selenium::WebDriver.logger.suppress_deprecations do
32
+ super
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ Selenium::WebDriver::Logger.prepend Capybara::Selenium::DeprecationSuppressor
40
+ Selenium::WebDriver::Error.singleton_class.prepend Capybara::Selenium::ErrorSuppressor
@@ -1,13 +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
+
2
8
  class Capybara::Selenium::Node < Capybara::Driver::Node
9
+ include Capybara::Selenium::Find
10
+ include Capybara::Selenium::Scroll
11
+
3
12
  def visible_text
4
- # Selenium doesn't normalize Unicode whitespace.
5
- Capybara::Helpers.normalize_whitespace(native.text)
13
+ native.text
6
14
  end
7
15
 
8
16
  def all_text
9
- text = driver.browser.execute_script("return arguments[0].textContent", native)
10
- Capybara::Helpers.normalize_whitespace(text)
17
+ text = driver.evaluate_script('arguments[0].textContent', self)
18
+ text.gsub(/[\u200b\u200e\u200f]/, '')
19
+ .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
20
+ .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
21
+ .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
22
+ .tr("\u00a0", ' ')
11
23
  end
12
24
 
13
25
  def [](name)
@@ -17,13 +29,19 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
17
29
  end
18
30
 
19
31
  def value
20
- if tag_name == "select" and self[:multiple] and not self[:multiple] == "false"
21
- native.find_elements(:xpath, ".//option").select { |n| n.selected? }.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 }
22
34
  else
23
35
  native[:value]
24
36
  end
25
37
  end
26
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
+
27
45
  ##
28
46
  #
29
47
  # Set the value of the form element to the given value.
@@ -35,75 +53,104 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
35
53
  # :none => append the new value to the existing value <br/>
36
54
  # :backspace => send backspace keystrokes to clear the field <br/>
37
55
  # Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
38
- def set(value, options={})
39
- tag_name = self.tag_name
40
- type = self[:type]
41
- if (Array === value) && !self[:multiple]
42
- raise ArgumentError.new "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
43
- end
44
- if tag_name == 'input' and type == 'radio'
45
- click
46
- elsif tag_name == 'input' and type == 'checkbox'
47
- click if value ^ native.attribute('checked').to_s.eql?("true")
48
- elsif tag_name == 'input' and type == 'file'
49
- path_names = value.to_s.empty? ? [] : value
50
- native.send_keys(*path_names)
51
- elsif tag_name == 'textarea' or tag_name == 'input'
52
- if self[:readonly]
53
- warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
54
- elsif value.to_s.empty?
55
- native.clear
56
+ def set(value, **options)
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
63
+
64
+ case tag_name
65
+ when 'input'
66
+ case type
67
+ when 'radio'
68
+ click
69
+ when 'checkbox'
70
+ click if value ^ checked?
71
+ when 'file'
72
+ set_file(value)
73
+ when 'date'
74
+ set_date(value)
75
+ when 'time'
76
+ set_time(value)
77
+ when 'datetime-local'
78
+ set_datetime_local(value)
79
+ when 'color'
80
+ set_color(value)
81
+ when 'range'
82
+ set_range(value)
56
83
  else
57
- if options[:clear] == :backspace
58
- # Clear field by sending the correct number of backspace keys.
59
- backspaces = [:backspace] * self.value.to_s.length
60
- native.send_keys(*(backspaces + [value.to_s]))
61
- elsif options[:clear] == :none
62
- native.send_keys(value.to_s)
63
- elsif options[:clear].is_a? Array
64
- native.send_keys(*options[:clear], value.to_s)
65
- else
66
- # Clear field by JavaScript assignment of the value property.
67
- # Script can change a readonly element which user input cannot, so
68
- # don't execute if readonly.
69
- driver.browser.execute_script "arguments[0].value = ''", native
70
- native.send_keys(value.to_s)
71
- end
84
+ set_text(value, **options)
72
85
  end
73
- elsif native.attribute('isContentEditable')
74
- #ensure we are focused on the element
75
- script = <<-JS
76
- var range = document.createRange();
77
- arguments[0].focus();
78
- range.selectNodeContents(arguments[0]);
79
- window.getSelection().addRange(range);
80
- JS
81
- driver.browser.execute_script script, native
82
- native.send_keys(value.to_s)
86
+ when 'textarea'
87
+ set_text(value, **options)
88
+ else
89
+ set_content_editable(value)
83
90
  end
84
91
  end
85
92
 
86
93
  def select_option
87
- native.click unless selected?
94
+ click unless selected? || disabled?
88
95
  end
89
96
 
90
97
  def unselect_option
91
- if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true'
92
- raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
93
- end
94
- native.click if selected?
98
+ raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
99
+
100
+ click if selected?
95
101
  end
96
102
 
97
- def click
98
- native.click
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)
117
+ end
118
+ action.release
119
+ end
120
+ end
121
+ rescue StandardError => e
122
+ if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
123
+ e.message.include?('Other element would receive the click')
124
+ scroll_to_center
125
+ end
126
+
127
+ raise e
99
128
  end
100
129
 
101
- def right_click
102
- driver.browser.action.context_click(native).perform
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'
143
+ end
144
+ end
103
145
  end
104
146
 
105
- def double_click
106
- driver.browser.action.double_click(native).perform
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)
153
+ end
107
154
  end
108
155
 
109
156
  def send_keys(*args)
@@ -111,39 +158,45 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
111
158
  end
112
159
 
113
160
  def hover
114
- driver.browser.action.move_to(native).perform
161
+ scroll_if_needed { browser_action.move_to(native).perform }
115
162
  end
116
163
 
117
- def drag_to(element)
118
- driver.browser.action.drag_and_drop(native, element.native).perform
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
119
175
  end
120
176
 
121
- def tag_name
122
- native.tag_name.downcase
177
+ def drop(*_)
178
+ raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
123
179
  end
124
180
 
125
- def visible?
126
- displayed = native.displayed?
127
- displayed and displayed != "false"
181
+ def tag_name
182
+ @tag_name ||= native.tag_name.downcase
128
183
  end
129
184
 
130
- def selected?
131
- selected = native.selected?
132
- selected and selected != "false"
133
- end
185
+ def visible?; boolean_attr(native.displayed?); end
186
+ def readonly?; boolean_attr(self[:readonly]); end
187
+ def multiple?; boolean_attr(self[:multiple]); end
188
+ def selected?; boolean_attr(native.selected?); end
189
+ alias :checked? :selected?
134
190
 
135
191
  def disabled?
136
- !native.enabled?
137
- end
192
+ return true unless native.enabled?
138
193
 
139
- alias :checked? :selected?
140
-
141
- def find_xpath(locator)
142
- native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
194
+ # WebDriver only defines `disabled?` for form controls but fieldset makes sense too
195
+ find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
143
196
  end
144
197
 
145
- def find_css(locator)
146
- native.find_elements(:css, locator).map { |n| self.class.new(driver, n) }
198
+ def content_editable?
199
+ native.attribute('isContentEditable') == 'true'
147
200
  end
148
201
 
149
202
  def ==(other)
@@ -151,33 +204,411 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
151
204
  end
152
205
 
153
206
  def path
154
- path = find_xpath('ancestor::*').reverse
155
- path.unshift self
207
+ driver.evaluate_script GET_XPATH_SCRIPT, self
208
+ end
209
+
210
+ def obscured?(x: nil, y: nil)
211
+ res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
212
+ return true if res == true
213
+
214
+ driver.frame_obscured_at?(x: res['x'], y: res['y'])
215
+ end
156
216
 
157
- result = []
158
- while node = path.shift
159
- parent = path.first
217
+ def rect
218
+ native.rect
219
+ end
220
+
221
+ protected
222
+
223
+ def scroll_if_needed
224
+ yield
225
+ rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
226
+ scroll_to_center
227
+ yield
228
+ end
229
+
230
+ def scroll_to_center
231
+ script = <<-'JS'
232
+ try {
233
+ arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
234
+ } catch(e) {
235
+ arguments[0].scrollIntoView(true);
236
+ }
237
+ JS
238
+ begin
239
+ driver.execute_script(script, self)
240
+ rescue StandardError
241
+ # Swallow error if scrollIntoView with options isn't supported
242
+ end
243
+ end
244
+
245
+ private
246
+
247
+ def sibling_index(parent, node, selector)
248
+ siblings = parent.find_xpath(selector)
249
+ case siblings.size
250
+ when 0
251
+ '[ERROR]' # IE doesn't support full XPath (namespace-uri, etc)
252
+ when 1
253
+ '' # index not necessary when only one matching element
254
+ else
255
+ idx = siblings.index(node)
256
+ # Element may not be found in the siblings if it has gone away
257
+ idx.nil? ? '[ERROR]' : "[#{idx + 1}]"
258
+ end
259
+ end
260
+
261
+ def boolean_attr(val)
262
+ val && (val != 'false')
263
+ end
264
+
265
+ # a reference to the select node if this is an option node
266
+ def select_node
267
+ find_xpath(XPath.ancestor(:select)[1]).first
268
+ end
269
+
270
+ def set_text(value, clear: nil, rapid: nil, **_unused)
271
+ value = value.to_s
272
+ if value.empty? && clear.nil?
273
+ native.clear
274
+ elsif clear == :backspace
275
+ # Clear field by sending the correct number of backspace keys.
276
+ backspaces = [:backspace] * self.value.to_s.length
277
+ send_keys(*([:end] + backspaces + [value]))
278
+ elsif clear.is_a? Array
279
+ send_keys(*clear, value)
280
+ else
281
+ driver.execute_script 'arguments[0].select()', self unless clear == :none
282
+ if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
283
+ send_keys(value[0..3])
284
+ driver.execute_script RAPID_APPEND_TEXT, self, value[4...-3]
285
+ send_keys(value[-3..-1])
286
+ else
287
+ send_keys(value)
288
+ end
289
+ end
290
+ end
291
+
292
+ def auto_rapid_set_length
293
+ 30
294
+ end
295
+
296
+ def perform_with_options(click_options, &block)
297
+ raise ArgumentError, 'A block must be provided' unless block
160
298
 
161
- if parent
162
- siblings = parent.find_xpath(node.tag_name)
163
- if siblings.size == 1
164
- result.unshift node.tag_name
299
+ scroll_if_needed do
300
+ action_with_modifiers(click_options) do |action|
301
+ if block_given?
302
+ yield action
165
303
  else
166
- index = siblings.index(node)
167
- result.unshift "#{node.tag_name}[#{index+1}]"
304
+ click_options.coords? ? action.click : action.click(native)
168
305
  end
306
+ end
307
+ end
308
+ end
309
+
310
+ def set_date(value) # rubocop:disable Naming/AccessorMethodName
311
+ value = SettableValue.new(value)
312
+ return set_text(value) unless value.dateable?
313
+
314
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
315
+ update_value_js(value.to_date_str)
316
+ end
317
+
318
+ def set_time(value) # rubocop:disable Naming/AccessorMethodName
319
+ value = SettableValue.new(value)
320
+ return set_text(value) unless value.timeable?
321
+
322
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
323
+ update_value_js(value.to_time_str)
324
+ end
325
+
326
+ def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
327
+ value = SettableValue.new(value)
328
+ return set_text(value) unless value.timeable?
329
+
330
+ # TODO: this would be better if locale can be detected and correct keystrokes sent
331
+ update_value_js(value.to_datetime_str)
332
+ end
333
+
334
+ def set_color(value) # rubocop:disable Naming/AccessorMethodName
335
+ update_value_js(value)
336
+ end
337
+
338
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
339
+ update_value_js(value)
340
+ end
341
+
342
+ def update_value_js(value)
343
+ driver.execute_script(<<-JS, self, value)
344
+ if (arguments[0].readOnly) { return };
345
+ if (document.activeElement !== arguments[0]){
346
+ arguments[0].focus();
347
+ }
348
+ if (arguments[0].value != arguments[1]) {
349
+ arguments[0].value = arguments[1]
350
+ arguments[0].dispatchEvent(new InputEvent('input'));
351
+ arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
352
+ }
353
+ JS
354
+ end
355
+
356
+ def set_file(value) # rubocop:disable Naming/AccessorMethodName
357
+ with_file_detector do
358
+ path_names = value.to_s.empty? ? [] : value
359
+ file_names = Array(path_names).map do |pn|
360
+ Pathname.new(pn).absolute? ? pn : File.expand_path(pn)
361
+ end.join("\n")
362
+ native.send_keys(file_names)
363
+ end
364
+ end
365
+
366
+ def with_file_detector
367
+ if driver.options[:browser] == :remote &&
368
+ bridge.respond_to?(:file_detector) &&
369
+ bridge.file_detector.nil?
370
+ begin
371
+ bridge.file_detector = lambda do |(fn, *)|
372
+ str = fn.to_s
373
+ str if File.exist?(str)
374
+ end
375
+ yield
376
+ ensure
377
+ bridge.file_detector = nil
378
+ end
379
+ else
380
+ yield
381
+ end
382
+ end
383
+
384
+ def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
385
+ # Ensure we are focused on the element
386
+ click
387
+
388
+ editable = driver.execute_script <<-JS, self
389
+ if (arguments[0].isContentEditable) {
390
+ var range = document.createRange();
391
+ var sel = window.getSelection();
392
+ arguments[0].focus();
393
+ range.selectNodeContents(arguments[0]);
394
+ sel.removeAllRanges();
395
+ sel.addRange(range);
396
+ return true;
397
+ }
398
+ return false;
399
+ JS
400
+
401
+ # The action api has a speed problem but both chrome and firefox 58 raise errors
402
+ # if we use the faster direct send_keys. For now just send_keys to the element
403
+ # we've already focused.
404
+ # native.send_keys(value.to_s)
405
+ browser_action.send_keys(value.to_s).perform if editable
406
+ end
407
+
408
+ def action_with_modifiers(click_options)
409
+ actions = browser_action.tap do |acts|
410
+ if click_options.center_offset? && click_options.coords?
411
+ acts.move_to(native).move_by(*click_options.coords)
169
412
  else
170
- result.unshift node.tag_name
413
+ acts.move_to(native, *click_options.coords)
171
414
  end
172
415
  end
416
+ modifiers_down(actions, click_options.keys)
417
+ yield actions
418
+ modifiers_up(actions, click_options.keys)
419
+ actions.perform
420
+ ensure
421
+ act = browser_action
422
+ act.release_actions if act.respond_to?(:release_actions)
423
+ end
173
424
 
174
- '/' + result.join('/')
425
+ def modifiers_down(actions, keys)
426
+ each_key(keys) { |key| actions.key_down(key) }
427
+ actions
175
428
  end
176
429
 
177
- private
430
+ def modifiers_up(actions, keys)
431
+ each_key(keys) { |key| actions.key_up(key) }
432
+ actions
433
+ end
178
434
 
179
- # a reference to the select node if this is an option node
180
- def select_node
181
- find_xpath('./ancestor::select').first
435
+ def browser
436
+ driver.browser
437
+ end
438
+
439
+ def bridge
440
+ browser.send(:bridge)
441
+ end
442
+
443
+ def browser_action
444
+ browser.action
445
+ end
446
+
447
+ def capabilities
448
+ browser.capabilities
449
+ end
450
+
451
+ def w3c?
452
+ (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
453
+ capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
454
+ end
455
+
456
+ def normalize_keys(keys)
457
+ keys.map do |key|
458
+ case key
459
+ when :ctrl then :control
460
+ when :command, :cmd then :meta
461
+ else
462
+ key
463
+ end
464
+ end
465
+ end
466
+
467
+ def each_key(keys, &block)
468
+ normalize_keys(keys).each(&block)
469
+ end
470
+
471
+ def find_context
472
+ native
473
+ end
474
+
475
+ def build_node(native_node, initial_cache = {})
476
+ self.class.new(driver, native_node, initial_cache)
477
+ end
478
+
479
+ def attrs(*attr_names)
480
+ return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']
481
+
482
+ driver.evaluate_script <<~'JS', self, attr_names.map(&:to_s)
483
+ (function(el, names){
484
+ return names.map(function(name){
485
+ return el[name]
486
+ });
487
+ })(arguments[0], arguments[1]);
488
+ JS
489
+ end
490
+
491
+ GET_XPATH_SCRIPT = <<~'JS'
492
+ (function(el, xml){
493
+ var xpath = '';
494
+ var pos, tempitem2;
495
+
496
+ if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
497
+ return "(: Shadow DOM element - no XPath :)";
498
+ };
499
+ while(el !== xml.documentElement) {
500
+ pos = 0;
501
+ tempitem2 = el;
502
+ while(tempitem2) {
503
+ if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) { // If it is ELEMENT_NODE of the same name
504
+ pos += 1;
505
+ }
506
+ tempitem2 = tempitem2.previousSibling;
507
+ }
508
+
509
+ if (el.namespaceURI != xml.documentElement.namespaceURI) {
510
+ xpath = "*[local-name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
511
+ } else {
512
+ xpath = el.nodeName.toUpperCase()+"["+pos+"]/"+xpath;
513
+ }
514
+
515
+ el = el.parentNode;
516
+ }
517
+ xpath = '/'+xml.documentElement.nodeName.toUpperCase()+'/'+xpath;
518
+ xpath = xpath.replace(/\/$/, '');
519
+ return xpath;
520
+ })(arguments[0], document)
521
+ JS
522
+
523
+ OBSCURED_OR_OFFSET_SCRIPT = <<~'JS'
524
+ (function(el, x, y) {
525
+ var box = el.getBoundingClientRect();
526
+ if (x == null) x = box.width/2;
527
+ if (y == null) y = box.height/2 ;
528
+
529
+ var px = box.left + x,
530
+ py = box.top + y,
531
+ e = document.elementFromPoint(px, py);
532
+
533
+ if (!el.contains(e))
534
+ return true;
535
+
536
+ return { x: px, y: py };
537
+ })(arguments[0], arguments[1], arguments[2])
538
+ JS
539
+
540
+ RAPID_APPEND_TEXT = <<~'JS'
541
+ (function(el, value) {
542
+ value = el.value + value;
543
+ if (el.maxLength && el.maxLength != -1){
544
+ value = value.slice(0, el.maxLength);
545
+ }
546
+ el.value = value;
547
+ })(arguments[0], arguments[1])
548
+ JS
549
+
550
+ # SettableValue encapsulates time/date field formatting
551
+ class SettableValue
552
+ attr_reader :value
553
+
554
+ def initialize(value)
555
+ @value = value
556
+ end
557
+
558
+ def to_s
559
+ value.to_s
560
+ end
561
+
562
+ def dateable?
563
+ !value.is_a?(String) && value.respond_to?(:to_date)
564
+ end
565
+
566
+ def to_date_str
567
+ value.to_date.iso8601
568
+ end
569
+
570
+ def timeable?
571
+ !value.is_a?(String) && value.respond_to?(:to_time)
572
+ end
573
+
574
+ def to_time_str
575
+ value.to_time.strftime('%H:%M')
576
+ end
577
+
578
+ def to_datetime_str
579
+ value.to_time.strftime('%Y-%m-%dT%H:%M')
580
+ end
581
+ end
582
+ private_constant :SettableValue
583
+
584
+ # ClickOptions encapsulates click option logic
585
+ class ClickOptions
586
+ attr_reader :keys, :options
587
+
588
+ def initialize(keys, options)
589
+ @keys = keys
590
+ @options = options
591
+ end
592
+
593
+ def coords?
594
+ options[:x] && options[:y]
595
+ end
596
+
597
+ def coords
598
+ [options[:x], options[:y]]
599
+ end
600
+
601
+ def center_offset?
602
+ options[:offset] == :center
603
+ end
604
+
605
+ def empty?
606
+ keys.empty? && !coords? && delay.zero?
607
+ end
608
+
609
+ def delay
610
+ options[:delay] || 0
611
+ end
182
612
  end
613
+ private_constant :ClickOptions
183
614
  end