capybara 2.18.0 → 3.38.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (315) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/History.md +889 -12
  4. data/License.txt +1 -1
  5. data/README.md +108 -78
  6. data/lib/capybara/config.rb +29 -57
  7. data/lib/capybara/cucumber.rb +2 -3
  8. data/lib/capybara/driver/base.rb +35 -18
  9. data/lib/capybara/driver/node.rb +40 -10
  10. data/lib/capybara/dsl.rb +10 -7
  11. data/lib/capybara/helpers.rb +70 -31
  12. data/lib/capybara/minitest/spec.rb +173 -83
  13. data/lib/capybara/minitest.rb +219 -112
  14. data/lib/capybara/node/actions.rb +274 -171
  15. data/lib/capybara/node/base.rb +42 -34
  16. data/lib/capybara/node/document.rb +15 -3
  17. data/lib/capybara/node/document_matchers.rb +19 -21
  18. data/lib/capybara/node/element.rb +362 -135
  19. data/lib/capybara/node/finders.rb +149 -137
  20. data/lib/capybara/node/matchers.rb +369 -209
  21. data/lib/capybara/node/simple.rb +59 -26
  22. data/lib/capybara/queries/active_element_query.rb +18 -0
  23. data/lib/capybara/queries/ancestor_query.rb +12 -9
  24. data/lib/capybara/queries/base_query.rb +39 -28
  25. data/lib/capybara/queries/current_path_query.rb +21 -27
  26. data/lib/capybara/queries/match_query.rb +14 -7
  27. data/lib/capybara/queries/selector_query.rb +658 -149
  28. data/lib/capybara/queries/sibling_query.rb +11 -9
  29. data/lib/capybara/queries/style_query.rb +45 -0
  30. data/lib/capybara/queries/text_query.rb +56 -38
  31. data/lib/capybara/queries/title_query.rb +8 -11
  32. data/lib/capybara/rack_test/browser.rb +113 -42
  33. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  34. data/lib/capybara/rack_test/driver.rb +22 -17
  35. data/lib/capybara/rack_test/errors.rb +6 -0
  36. data/lib/capybara/rack_test/form.rb +93 -58
  37. data/lib/capybara/rack_test/node.rb +188 -81
  38. data/lib/capybara/rails.rb +3 -7
  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 +53 -0
  43. data/lib/capybara/result.rb +96 -62
  44. data/lib/capybara/rspec/features.rb +17 -50
  45. data/lib/capybara/rspec/matcher_proxies.rb +52 -15
  46. data/lib/capybara/rspec/matchers/base.rb +111 -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 +77 -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 -311
  60. data/lib/capybara/rspec.rb +7 -11
  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 +89 -17
  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 +54 -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 +2 -17
  87. data/lib/capybara/selector/filter_set.rb +80 -33
  88. data/lib/capybara/selector/filters/base.rb +50 -6
  89. data/lib/capybara/selector/filters/expression_filter.rb +8 -26
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +16 -12
  92. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  93. data/lib/capybara/selector/selector.rb +93 -210
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +227 -526
  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 +332 -261
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  102. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  103. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -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 +226 -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/logger_suppressor.rb +44 -0
  112. data/lib/capybara/selenium/node.rb +545 -144
  113. data/lib/capybara/selenium/nodes/chrome_node.rb +137 -0
  114. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  115. data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
  116. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  117. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  118. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  119. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  120. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  121. data/lib/capybara/selenium/patches/logs.rb +45 -0
  122. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  123. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  124. data/lib/capybara/server/animation_disabler.rb +81 -0
  125. data/lib/capybara/server/checker.rb +44 -0
  126. data/lib/capybara/server/middleware.rb +71 -0
  127. data/lib/capybara/server.rb +59 -67
  128. data/lib/capybara/session/config.rb +81 -67
  129. data/lib/capybara/session/matchers.rb +28 -20
  130. data/lib/capybara/session.rb +336 -365
  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 +147 -12
  134. data/lib/capybara/spec/session/accept_alert_spec.rb +12 -11
  135. data/lib/capybara/spec/session/accept_confirm_spec.rb +6 -5
  136. data/lib/capybara/spec/session/accept_prompt_spec.rb +10 -10
  137. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  138. data/lib/capybara/spec/session/all_spec.rb +160 -56
  139. data/lib/capybara/spec/session/ancestor_spec.rb +27 -24
  140. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +68 -38
  141. data/lib/capybara/spec/session/assert_current_path_spec.rb +75 -0
  142. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  143. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  144. data/lib/capybara/spec/session/assert_text_spec.rb +258 -0
  145. data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +22 -12
  146. data/lib/capybara/spec/session/attach_file_spec.rb +144 -69
  147. data/lib/capybara/spec/session/body_spec.rb +12 -13
  148. data/lib/capybara/spec/session/check_spec.rb +117 -55
  149. data/lib/capybara/spec/session/choose_spec.rb +64 -31
  150. data/lib/capybara/spec/session/click_button_spec.rb +231 -173
  151. data/lib/capybara/spec/session/click_link_or_button_spec.rb +55 -35
  152. data/lib/capybara/spec/session/click_link_spec.rb +82 -58
  153. data/lib/capybara/spec/session/current_scope_spec.rb +12 -11
  154. data/lib/capybara/spec/session/current_url_spec.rb +57 -39
  155. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +4 -4
  156. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +3 -2
  157. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +11 -9
  158. data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
  159. data/lib/capybara/spec/session/element/match_xpath_spec.rb +9 -7
  160. data/lib/capybara/spec/session/element/matches_selector_spec.rb +71 -57
  161. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +8 -7
  162. data/lib/capybara/spec/session/evaluate_script_spec.rb +29 -8
  163. data/lib/capybara/spec/session/execute_script_spec.rb +10 -8
  164. data/lib/capybara/spec/session/fill_in_spec.rb +134 -43
  165. data/lib/capybara/spec/session/find_button_spec.rb +25 -24
  166. data/lib/capybara/spec/session/find_by_id_spec.rb +10 -9
  167. data/lib/capybara/spec/session/find_field_spec.rb +37 -41
  168. data/lib/capybara/spec/session/find_link_spec.rb +36 -17
  169. data/lib/capybara/spec/session/find_spec.rb +251 -144
  170. data/lib/capybara/spec/session/first_spec.rb +79 -51
  171. data/lib/capybara/spec/session/frame/frame_title_spec.rb +23 -0
  172. data/lib/capybara/spec/session/frame/frame_url_spec.rb +23 -0
  173. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +33 -20
  174. data/lib/capybara/spec/session/frame/within_frame_spec.rb +50 -32
  175. data/lib/capybara/spec/session/go_back_spec.rb +2 -1
  176. data/lib/capybara/spec/session/go_forward_spec.rb +2 -1
  177. data/lib/capybara/spec/session/has_all_selectors_spec.rb +31 -31
  178. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  179. data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
  180. data/lib/capybara/spec/session/has_button_spec.rb +100 -13
  181. data/lib/capybara/spec/session/has_css_spec.rb +272 -137
  182. data/lib/capybara/spec/session/has_current_path_spec.rb +59 -60
  183. data/lib/capybara/spec/session/has_field_spec.rb +139 -59
  184. data/lib/capybara/spec/session/has_link_spec.rb +43 -6
  185. data/lib/capybara/spec/session/has_none_selectors_spec.rb +42 -40
  186. data/lib/capybara/spec/session/has_select_spec.rb +107 -72
  187. data/lib/capybara/spec/session/has_selector_spec.rb +120 -71
  188. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  189. data/lib/capybara/spec/session/has_table_spec.rb +172 -5
  190. data/lib/capybara/spec/session/has_text_spec.rb +106 -62
  191. data/lib/capybara/spec/session/has_title_spec.rb +20 -14
  192. data/lib/capybara/spec/session/has_xpath_spec.rb +57 -38
  193. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +3 -2
  194. data/lib/capybara/spec/session/html_spec.rb +14 -6
  195. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  196. data/lib/capybara/spec/session/node_spec.rb +1002 -153
  197. data/lib/capybara/spec/session/node_wrapper_spec.rb +39 -0
  198. data/lib/capybara/spec/session/refresh_spec.rb +12 -6
  199. data/lib/capybara/spec/session/reset_session_spec.rb +82 -35
  200. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +2 -1
  201. data/lib/capybara/spec/session/save_and_open_page_spec.rb +3 -2
  202. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +8 -12
  203. data/lib/capybara/spec/session/save_page_spec.rb +42 -55
  204. data/lib/capybara/spec/session/save_screenshot_spec.rb +16 -14
  205. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  206. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  207. data/lib/capybara/spec/session/select_spec.rb +107 -81
  208. data/lib/capybara/spec/session/selectors_spec.rb +52 -19
  209. data/lib/capybara/spec/session/sibling_spec.rb +10 -10
  210. data/lib/capybara/spec/session/text_spec.rb +37 -21
  211. data/lib/capybara/spec/session/title_spec.rb +17 -5
  212. data/lib/capybara/spec/session/uncheck_spec.rb +42 -22
  213. data/lib/capybara/spec/session/unselect_spec.rb +39 -38
  214. data/lib/capybara/spec/session/visit_spec.rb +85 -53
  215. data/lib/capybara/spec/session/window/become_closed_spec.rb +24 -20
  216. data/lib/capybara/spec/session/window/current_window_spec.rb +5 -3
  217. data/lib/capybara/spec/session/window/open_new_window_spec.rb +5 -3
  218. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +27 -22
  219. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +12 -6
  220. data/lib/capybara/spec/session/window/window_spec.rb +97 -63
  221. data/lib/capybara/spec/session/window/windows_spec.rb +12 -10
  222. data/lib/capybara/spec/session/window/within_window_spec.rb +31 -86
  223. data/lib/capybara/spec/session/within_spec.rb +83 -44
  224. data/lib/capybara/spec/spec_helper.rb +53 -43
  225. data/lib/capybara/spec/test_app.rb +158 -43
  226. data/lib/capybara/spec/views/animated.erb +49 -0
  227. data/lib/capybara/spec/views/form.erb +154 -42
  228. data/lib/capybara/spec/views/frame_child.erb +4 -3
  229. data/lib/capybara/spec/views/frame_one.erb +2 -1
  230. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  231. data/lib/capybara/spec/views/frame_two.erb +1 -1
  232. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  233. data/lib/capybara/spec/views/layout.erb +10 -0
  234. data/lib/capybara/spec/views/obscured.erb +47 -0
  235. data/lib/capybara/spec/views/offset.erb +33 -0
  236. data/lib/capybara/spec/views/path.erb +2 -2
  237. data/lib/capybara/spec/views/popup_one.erb +1 -1
  238. data/lib/capybara/spec/views/popup_two.erb +1 -1
  239. data/lib/capybara/spec/views/react.erb +45 -0
  240. data/lib/capybara/spec/views/scroll.erb +21 -0
  241. data/lib/capybara/spec/views/spatial.erb +31 -0
  242. data/lib/capybara/spec/views/tables.erb +68 -1
  243. data/lib/capybara/spec/views/with_animation.erb +81 -0
  244. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  245. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  246. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  247. data/lib/capybara/spec/views/with_hover.erb +3 -2
  248. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  249. data/lib/capybara/spec/views/with_html.erb +67 -12
  250. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  251. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  252. data/lib/capybara/spec/views/with_js.erb +30 -5
  253. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  254. data/lib/capybara/spec/views/with_namespace.erb +21 -0
  255. data/lib/capybara/spec/views/with_scope.erb +2 -2
  256. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  257. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  258. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  259. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  260. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  261. data/lib/capybara/spec/views/with_windows.erb +1 -1
  262. data/lib/capybara/spec/views/within_frames.erb +5 -2
  263. data/lib/capybara/version.rb +2 -1
  264. data/lib/capybara/window.rb +36 -34
  265. data/lib/capybara.rb +129 -103
  266. data/spec/basic_node_spec.rb +60 -34
  267. data/spec/capybara_spec.rb +63 -88
  268. data/spec/counter_spec.rb +35 -0
  269. data/spec/css_builder_spec.rb +101 -0
  270. data/spec/css_splitter_spec.rb +38 -0
  271. data/spec/dsl_spec.rb +85 -64
  272. data/spec/filter_set_spec.rb +27 -9
  273. data/spec/fixtures/certificate.pem +25 -0
  274. data/spec/fixtures/key.pem +27 -0
  275. data/spec/fixtures/selenium_driver_rspec_failure.rb +6 -5
  276. data/spec/fixtures/selenium_driver_rspec_success.rb +6 -5
  277. data/spec/minitest_spec.rb +45 -7
  278. data/spec/minitest_spec_spec.rb +94 -63
  279. data/spec/per_session_config_spec.rb +14 -13
  280. data/spec/rack_test_spec.rb +194 -125
  281. data/spec/regexp_dissassembler_spec.rb +250 -0
  282. data/spec/result_spec.rb +102 -50
  283. data/spec/rspec/features_spec.rb +37 -31
  284. data/spec/rspec/scenarios_spec.rb +9 -7
  285. data/spec/rspec/shared_spec_matchers.rb +449 -422
  286. data/spec/rspec/views_spec.rb +5 -3
  287. data/spec/rspec_matchers_spec.rb +27 -11
  288. data/spec/rspec_spec.rb +109 -89
  289. data/spec/sauce_spec_chrome.rb +43 -0
  290. data/spec/selector_spec.rb +397 -68
  291. data/spec/selenium_spec_chrome.rb +186 -40
  292. data/spec/selenium_spec_chrome_remote.rb +103 -0
  293. data/spec/selenium_spec_edge.rb +49 -0
  294. data/spec/selenium_spec_firefox.rb +194 -41
  295. data/spec/selenium_spec_firefox_remote.rb +82 -0
  296. data/spec/selenium_spec_ie.rb +149 -0
  297. data/spec/selenium_spec_safari.rb +162 -0
  298. data/spec/server_spec.rb +201 -102
  299. data/spec/session_spec.rb +53 -16
  300. data/spec/shared_selenium_node.rb +79 -0
  301. data/spec/shared_selenium_session.rb +474 -122
  302. data/spec/spec_helper.rb +93 -7
  303. data/spec/xpath_builder_spec.rb +93 -0
  304. metadata +360 -73
  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/query.rb +0 -7
  309. data/lib/capybara/rspec/compound.rb +0 -95
  310. data/lib/capybara/spec/session/assert_current_path.rb +0 -72
  311. data/lib/capybara/spec/session/assert_selector.rb +0 -148
  312. data/lib/capybara/spec/session/assert_text.rb +0 -234
  313. data/lib/capybara/spec/session/source_spec.rb +0 -0
  314. data/lib/capybara/spec/views/with_title.erb +0 -5
  315. data/spec/selenium_spec_marionette.rb +0 -127
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Capybara::SpecHelper.spec "node" do
3
+ # NOTE: This file uses `sleep` to sync up parts of the tests. This is only implemented like this
4
+ # because of the methods being tested. In tests using Capybara this type of behavior should be implemented
5
+ # using Capybara provided assertions with builtin waiting behavior.
6
+
7
+ Capybara::SpecHelper.spec 'node' do
4
8
  before do
5
9
  @session.visit('/with_html')
6
10
  end
7
11
 
8
- it "should act like a session object" do
12
+ it 'should act like a session object' do
9
13
  @session.visit('/form')
10
14
  @form = @session.find(:css, '#get-form')
11
15
  expect(@form).to have_field('Middle Name')
@@ -15,67 +19,70 @@ Capybara::SpecHelper.spec "node" do
15
19
  expect(extract_results(@session)['middle_name']).to eq('Monkey')
16
20
  end
17
21
 
18
- it "should scope CSS selectors" do
22
+ it 'should scope CSS selectors' do
19
23
  expect(@session.find(:css, '#second')).to have_no_css('h1')
20
24
  end
21
25
 
22
- describe "#query_scope" do
23
- it "should have a reference to the element the query was evaluated on if there is one" do
24
- @node = @session.find(:css, '#first')
25
- expect(@node.query_scope).to eq(@node.session.document)
26
- expect(@node.find(:css, '#foo').query_scope).to eq(@node)
27
- end
28
- end
29
-
30
- describe "#parent" do
31
- it "should be deprecated" do
32
- @node = @session.find(:css, '#first')
33
- expect(@node).to receive(:warn).with(/^DEPRECATED:/)
34
- expect(@node.parent).to eq(@node.query_scope)
26
+ describe '#query_scope' do
27
+ it 'should have a reference to the element the query was evaluated on if there is one' do
28
+ node = @session.find(:css, '#first')
29
+ expect(node.query_scope).to eq(node.session.document)
30
+ expect(node.find(:css, '#foo').query_scope).to eq(node)
35
31
  end
36
32
  end
37
33
 
38
- describe "#text" do
39
- it "should extract node texts" do
34
+ describe '#text' do
35
+ it 'should extract node texts' do
40
36
  expect(@session.all('//a')[0].text).to eq('labore')
41
37
  expect(@session.all('//a')[1].text).to eq('ullamco')
42
38
  end
43
39
 
44
- it "should return document text on /html selector" do
40
+ it 'should return document text on /html selector' do
45
41
  @session.visit('/with_simple_html')
46
42
  expect(@session.all('/html')[0].text).to eq('Bar')
47
43
  end
48
44
  end
49
45
 
50
- describe "#[]" do
51
- it "should extract node attributes" do
46
+ describe '#[]' do
47
+ it 'should extract node attributes' do
52
48
  expect(@session.all('//a')[0][:class]).to eq('simple')
53
49
  expect(@session.all('//a')[1][:id]).to eq('foo')
54
50
  expect(@session.all('//input')[0][:type]).to eq('text')
55
51
  end
56
52
 
57
- it "should extract boolean node attributes" do
53
+ it 'should extract boolean node attributes' do
58
54
  expect(@session.find('//input[@id="checked_field"]')[:checked]).to be_truthy
59
55
  end
60
56
  end
61
57
 
62
- describe "#value" do
63
- it "should allow retrieval of the value" do
58
+ describe '#style', requires: [:css] do
59
+ it 'should return the computed style value' do
60
+ expect(@session.find(:css, '#first').style('display')).to eq('display' => 'block')
61
+ expect(@session.find(:css, '#second').style(:display)).to eq('display' => 'inline')
62
+ end
63
+
64
+ it 'should return multiple style values' do
65
+ expect(@session.find(:css, '#first').style('display', :'line-height')).to eq('display' => 'block', 'line-height' => '25px')
66
+ end
67
+ end
68
+
69
+ describe '#value' do
70
+ it 'should allow retrieval of the value' do
64
71
  expect(@session.find('//textarea[@id="normal"]').value).to eq('banana')
65
72
  end
66
73
 
67
- it "should not swallow extra newlines in textarea" do
74
+ it 'should not swallow extra newlines in textarea' do
68
75
  expect(@session.find('//textarea[@id="additional_newline"]').value).to eq("\nbanana")
69
76
  end
70
77
 
71
- it "should not swallow leading newlines for set content in textarea" do
78
+ it 'should not swallow leading newlines for set content in textarea' do
72
79
  @session.find('//textarea[@id="normal"]').set("\nbanana")
73
80
  expect(@session.find('//textarea[@id="normal"]').value).to eq("\nbanana")
74
81
  end
75
82
 
76
- it "return any HTML content in textarea" do
77
- @session.find('//textarea[1]').set("some <em>html</em> here")
78
- expect(@session.find('//textarea[1]').value).to eq("some <em>html</em> here")
83
+ it 'return any HTML content in textarea' do
84
+ @session.find('//textarea[1]').set('some <em>html</em> here')
85
+ expect(@session.find('//textarea[1]').value).to eq('some <em>html</em> here')
79
86
  end
80
87
 
81
88
  it "defaults to 'on' for checkbox" do
@@ -89,125 +96,146 @@ Capybara::SpecHelper.spec "node" do
89
96
  end
90
97
  end
91
98
 
92
- describe "#set" do
93
- it "should allow assignment of field value" do
99
+ describe '#set' do
100
+ it 'should allow assignment of field value' do
94
101
  expect(@session.first('//input').value).to eq('monkey')
95
102
  @session.first('//input').set('gorilla')
96
103
  expect(@session.first('//input').value).to eq('gorilla')
97
104
  end
98
105
 
99
- it "should fill the field even if the caret was not at the end", requires: [:js] do
106
+ it 'should fill the field even if the caret was not at the end', requires: [:js] do
100
107
  @session.execute_script("var el = document.getElementById('test_field'); el.focus(); el.setSelectionRange(0, 0);")
101
108
  @session.first('//input').set('')
102
109
  expect(@session.first('//input').value).to eq('')
103
110
  end
104
111
 
105
- it "should not set if the text field is readonly" do
106
- expect(@session.first('//input[@readonly]').value).to eq('should not change')
107
- @session.first('//input[@readonly]').set('changed')
108
- expect(@session.first('//input[@readonly]').value).to eq('should not change')
109
- end
112
+ if ENV['CAPYBARA_THOROUGH']
113
+ it 'should raise if the text field is readonly' do
114
+ expect { @session.first('//input[@readonly]').set('changed') }.to raise_error(Capybara::ReadOnlyElementError)
115
+ end
110
116
 
111
- it "should raise if the text field is readonly" do
112
- expect(@session.first('//input[@readonly]').set('changed')).to raise_error(Capybara::ReadOnlyElementError)
113
- end if Capybara::VERSION.to_f > 3.0
117
+ it 'should raise if the textarea is readonly' do
118
+ expect { @session.first('//textarea[@readonly]').set('changed') }.to raise_error(Capybara::ReadOnlyElementError)
119
+ end
120
+ else
121
+ it 'should not change if the text field is readonly' do
122
+ @session.first('//input[@readonly]').set('changed')
123
+ expect(@session.first('//input[@readonly]').value).to eq 'should not change'
124
+ end
114
125
 
115
- it "should not set if the textarea is readonly" do
116
- expect(@session.first('//textarea[@readonly]').value).to eq('textarea should not change')
117
- @session.first('//textarea[@readonly]').set('changed')
118
- expect(@session.first('//textarea[@readonly]').value).to eq('textarea should not change')
126
+ it 'should not change if the textarea is readonly' do
127
+ @session.first('//textarea[@readonly]').set('changed')
128
+ expect(@session.first('//textarea[@readonly]').value).to eq 'textarea should not change'
129
+ end
119
130
  end
120
131
 
121
- it "should raise if the text field is readonly" do
122
- expect(@session.first('//textarea[@readonly]').set('changed')).to raise_error(Capybara::ReadOnlyElementError)
123
- end if Capybara::VERSION.to_f > 3.0
132
+ it 'should use global default options' do
133
+ Capybara.default_set_options = { clear: :backspace }
134
+ element = @session.first(:fillable_field, type: 'text')
135
+ allow(element.base).to receive(:set)
136
+ element.set('gorilla')
137
+ expect(element.base).to have_received(:set).with('gorilla', clear: :backspace)
138
+ end
124
139
 
125
- context "with a contenteditable element", requires: [:js] do
140
+ context 'with a contenteditable element', requires: [:js] do
126
141
  it 'should allow me to change the contents' do
127
142
  @session.visit('/with_js')
128
- @session.find(:css,'#existing_content_editable').set('WYSIWYG')
129
- expect(@session.find(:css,'#existing_content_editable').text).to eq('WYSIWYG')
143
+ @session.find(:css, '#existing_content_editable').set('WYSIWYG')
144
+ expect(@session.find(:css, '#existing_content_editable').text).to eq('WYSIWYG')
130
145
  end
131
146
 
132
147
  it 'should allow me to set the contents' do
133
148
  @session.visit('/with_js')
134
- @session.find(:css,'#blank_content_editable').set('WYSIWYG')
135
- expect(@session.find(:css,'#blank_content_editable').text).to eq('WYSIWYG')
149
+ @session.find(:css, '#blank_content_editable').set('WYSIWYG')
150
+ expect(@session.find(:css, '#blank_content_editable').text).to eq('WYSIWYG')
136
151
  end
137
152
 
138
153
  it 'should allow me to change the contents of a child element' do
139
154
  @session.visit('/with_js')
140
- @session.find(:css,'#existing_content_editable_child').set('WYSIWYG')
141
- expect(@session.find(:css,'#existing_content_editable_child').text).to eq('WYSIWYG')
142
- expect(@session.find(:css,'#existing_content_editable_child_parent').text).to eq('Some content WYSIWYG')
155
+ @session.find(:css, '#existing_content_editable_child').set('WYSIWYG')
156
+ expect(@session.find(:css, '#existing_content_editable_child').text).to eq('WYSIWYG')
157
+ expect(@session.find(:css, '#existing_content_editable_child_parent').text).to eq("Some content\nWYSIWYG")
143
158
  end
144
159
  end
145
160
  end
146
161
 
147
- describe "#tag_name" do
148
- it "should extract node tag name" do
162
+ describe '#tag_name' do
163
+ it 'should extract node tag name' do
149
164
  expect(@session.all('//a')[0].tag_name).to eq('a')
150
165
  expect(@session.all('//a')[1].tag_name).to eq('a')
151
166
  expect(@session.all('//p')[1].tag_name).to eq('p')
152
167
  end
153
168
  end
154
169
 
155
- describe "#disabled?" do
156
- it "should extract disabled node" do
170
+ describe '#disabled?' do
171
+ it 'should extract disabled node' do
157
172
  @session.visit('/form')
158
173
  expect(@session.find('//input[@id="customer_name"]')).to be_disabled
159
174
  expect(@session.find('//input[@id="customer_email"]')).not_to be_disabled
160
175
  end
161
176
 
162
- it "should see disabled options as disabled" do
177
+ it 'should see disabled options as disabled' do
163
178
  @session.visit('/form')
164
179
  expect(@session.find('//select[@id="form_title"]/option[1]')).not_to be_disabled
165
180
  expect(@session.find('//select[@id="form_title"]/option[@disabled]')).to be_disabled
166
181
  end
167
182
 
168
- it "should see enabled options in disabled select as disabled" do
183
+ it 'should see enabled options in disabled select as disabled' do
169
184
  @session.visit('/form')
170
185
  expect(@session.find('//select[@id="form_disabled_select"]/option')).to be_disabled
171
186
  expect(@session.find('//select[@id="form_disabled_select"]/optgroup/option')).to be_disabled
172
187
  expect(@session.find('//select[@id="form_title"]/option[1]')).not_to be_disabled
173
188
  end
174
189
 
175
- it "should see enabled options in disabled optgroup as disabled" do
190
+ it 'should see enabled options in disabled optgroup as disabled' do
176
191
  @session.visit('/form')
177
- expect(@session.find('//option', text: "A.B.1")).to be_disabled
178
- expect(@session.find('//option', text: "A.2")).not_to be_disabled
192
+ expect(@session.find('//option', text: 'A.B.1')).to be_disabled
193
+ expect(@session.find('//option', text: 'A.2')).not_to be_disabled
179
194
  end
180
195
 
181
- context "in a disabled fieldset" do
196
+ it 'should see a disabled fieldset as disabled' do
197
+ @session.visit('/form')
198
+ expect(@session.find(:xpath, './/fieldset[@id="form_disabled_fieldset"]')).to be_disabled
199
+ end
200
+
201
+ context 'in a disabled fieldset' do
182
202
  # https://html.spec.whatwg.org/#the-fieldset-element
183
- it "should see elements not in first legend as disabled" do
203
+ it 'should see elements not in first legend as disabled' do
184
204
  @session.visit('/form')
185
205
  expect(@session.find('//input[@id="form_disabled_fieldset_child"]')).to be_disabled
186
206
  expect(@session.find('//input[@id="form_disabled_fieldset_second_legend_child"]')).to be_disabled
187
207
  expect(@session.find('//input[@id="form_enabled_fieldset_child"]')).not_to be_disabled
188
208
  end
189
209
 
190
- it "should see elements in first legend as enabled" do
210
+ it 'should see elements in first legend as enabled' do
191
211
  @session.visit('/form')
192
212
  expect(@session.find('//input[@id="form_disabled_fieldset_legend_child"]')).not_to be_disabled
193
213
  end
194
214
 
195
- it "should sees options not in first legend as disabled" do
215
+ it 'should sees options not in first legend as disabled' do
196
216
  @session.visit('/form')
197
217
  expect(@session.find('//option', text: 'Disabled Child Option')).to be_disabled
198
218
  end
199
219
  end
200
- it "should be boolean" do
220
+
221
+ it 'should be boolean' do
201
222
  @session.visit('/form')
202
223
  expect(@session.find('//select[@id="form_disabled_select"]/option').disabled?).to be true
203
224
  expect(@session.find('//select[@id="form_disabled_select2"]/option').disabled?).to be true
204
225
  expect(@session.find('//select[@id="form_title"]/option[1]').disabled?).to be false
205
226
  end
227
+
228
+ it 'should be disabled for all elements that are CSS :disabled' do
229
+ @session.visit('/form')
230
+ # sanity check
231
+ expect(@session.all(:css, ':disabled')).to all(be_disabled)
232
+ end
206
233
  end
207
234
 
208
- describe "#visible?" do
209
- it "should extract node visibility" do
210
- Capybara.ignore_hidden_elements = false
235
+ describe '#visible?' do
236
+ before { Capybara.ignore_hidden_elements = false }
237
+
238
+ it 'should extract node visibility' do
211
239
  expect(@session.first('//a')).to be_visible
212
240
 
213
241
  expect(@session.find('//div[@id="hidden"]')).not_to be_visible
@@ -217,22 +245,124 @@ Capybara::SpecHelper.spec "node" do
217
245
  expect(@session.find('//input[@id="hidden_input"]')).not_to be_visible
218
246
  end
219
247
 
220
- it "should be boolean" do
221
- Capybara.ignore_hidden_elements = false
248
+ it 'template elements should not be visible' do
249
+ expect(@session.find('//template')).not_to be_visible
250
+ end
251
+
252
+ it 'should be boolean' do
222
253
  expect(@session.first('//a').visible?).to be true
223
254
  expect(@session.find('//div[@id="hidden"]').visible?).to be false
224
255
  end
256
+
257
+ it 'closed details > summary elements and descendants should be visible' do
258
+ expect(@session.find(:css, '#closed_details summary')).to be_visible
259
+ expect(@session.find(:css, '#closed_details summary h6')).to be_visible
260
+ end
261
+
262
+ it 'details non-summary descendants should be non-visible when closed' do
263
+ descendants = @session.all(:css, '#closed_details > *:not(summary), #closed_details > *:not(summary) *', minimum: 2)
264
+ expect(descendants).not_to include(be_visible)
265
+ end
266
+
267
+ it 'deatils descendants should be visible when open' do
268
+ descendants = @session.all(:css, '#open_details *')
269
+ expect(descendants).to all(be_visible)
270
+ end
271
+
272
+ it 'works when details is toggled open and closed' do
273
+ @session.find(:css, '#closed_details > summary').click
274
+ expect(@session).to have_css('#closed_details *', visible: :visible, count: 5)
275
+ .and(have_no_css('#closed_details *', visible: :hidden))
276
+
277
+ @session.find(:css, '#closed_details > summary').click
278
+ descendants_css = '#closed_details > *:not(summary), #closed_details > *:not(summary) *'
279
+ expect(@session).to have_no_css(descendants_css, visible: :visible)
280
+ .and(have_css(descendants_css, visible: :hidden, count: 3))
281
+ end
225
282
  end
226
283
 
227
- describe "#checked?" do
228
- it "should extract node checked state" do
284
+ describe '#obscured?', requires: [:css] do
285
+ it 'should see non visible elements as obscured' do
286
+ Capybara.ignore_hidden_elements = false
287
+ expect(@session.find('//div[@id="hidden"]')).to be_obscured
288
+ expect(@session.find('//div[@id="hidden_via_ancestor"]')).to be_obscured
289
+ expect(@session.find('//div[@id="hidden_attr"]')).to be_obscured
290
+ expect(@session.find('//a[@id="hidden_attr_via_ancestor"]')).to be_obscured
291
+ expect(@session.find('//input[@id="hidden_input"]')).to be_obscured
292
+ end
293
+
294
+ it 'should see non-overlapped elements as not obscured' do
295
+ @session.visit('/obscured')
296
+ expect(@session.find(:css, '#cover')).not_to be_obscured
297
+ end
298
+
299
+ it 'should see elements only overlapped by descendants as not obscured' do
300
+ expect(@session.first(:css, 'p:not(.para)')).not_to be_obscured
301
+ end
302
+
303
+ it 'should see elements outside the viewport as obscured' do
304
+ @session.visit('/obscured')
305
+ off = @session.find(:css, '#offscreen')
306
+ off_wrapper = @session.find(:css, '#offscreen_wrapper')
307
+ expect(off).to be_obscured
308
+ expect(off_wrapper).to be_obscured
309
+ @session.scroll_to(off_wrapper)
310
+ expect(off_wrapper).not_to be_obscured
311
+ expect(off).to be_obscured
312
+ off_wrapper.scroll_to(off)
313
+ expect(off).not_to be_obscured
314
+ expect(off_wrapper).not_to be_obscured
315
+ end
316
+
317
+ it 'should see overlapped elements as obscured' do
318
+ @session.visit('/obscured')
319
+ expect(@session.find(:css, '#obscured')).to be_obscured
320
+ end
321
+
322
+ it 'should be boolean' do
323
+ Capybara.ignore_hidden_elements = false
324
+ expect(@session.first('//a').obscured?).to be false
325
+ expect(@session.find('//div[@id="hidden"]').obscured?).to be true
326
+ end
327
+
328
+ it 'should work in frames' do
329
+ @session.visit('/obscured')
330
+ frame = @session.find(:css, '#frameOne')
331
+ @session.within_frame(frame) do
332
+ div = @session.find(:css, '#divInFrameOne')
333
+ expect(div).to be_obscured
334
+ @session.scroll_to div
335
+ expect(div).not_to be_obscured
336
+ end
337
+ end
338
+
339
+ it 'should work in nested iframes' do
340
+ @session.visit('/obscured')
341
+ frame = @session.find(:css, '#nestedFrames')
342
+ @session.within_frame(frame) do
343
+ @session.within_frame(:css, '#childFrame') do
344
+ gcframe = @session.find(:css, '#grandchildFrame2')
345
+ @session.within_frame(gcframe) do
346
+ expect(@session.find(:css, '#divInFrameTwo')).to be_obscured
347
+ end
348
+ @session.scroll_to(gcframe)
349
+ @session.within_frame(gcframe) do
350
+ expect(@session.find(:css, '#divInFrameTwo')).not_to be_obscured
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ describe '#checked?' do
358
+ it 'should extract node checked state' do
229
359
  @session.visit('/form')
230
360
  expect(@session.find('//input[@id="gender_female"]')).to be_checked
231
361
  expect(@session.find('//input[@id="gender_male"]')).not_to be_checked
232
362
  expect(@session.first('//h1')).not_to be_checked
233
363
  end
234
364
 
235
- it "should be boolean" do
365
+ it 'should be boolean' do
236
366
  @session.visit('/form')
237
367
  expect(@session.find('//input[@id="gender_female"]').checked?).to be true
238
368
  expect(@session.find('//input[@id="gender_male"]').checked?).to be false
@@ -240,15 +370,15 @@ Capybara::SpecHelper.spec "node" do
240
370
  end
241
371
  end
242
372
 
243
- describe "#selected?" do
244
- it "should extract node selected state" do
373
+ describe '#selected?' do
374
+ it 'should extract node selected state' do
245
375
  @session.visit('/form')
246
376
  expect(@session.find('//option[@value="en"]')).to be_selected
247
377
  expect(@session.find('//option[@value="sv"]')).not_to be_selected
248
378
  expect(@session.first('//h1')).not_to be_selected
249
379
  end
250
380
 
251
- it "should be boolean" do
381
+ it 'should be boolean' do
252
382
  @session.visit('/form')
253
383
  expect(@session.find('//option[@value="en"]').selected?).to be true
254
384
  expect(@session.find('//option[@value="sv"]').selected?).to be false
@@ -256,86 +386,358 @@ Capybara::SpecHelper.spec "node" do
256
386
  end
257
387
  end
258
388
 
259
- describe "#==" do
260
- it "preserve object identity" do
261
- expect(@session.find('//h1') == @session.find('//h1')).to be true
262
- expect(@session.find('//h1') === @session.find('//h1')).to be true
263
- expect(@session.find('//h1').eql? @session.find('//h1')).to be false
389
+ describe '#==' do
390
+ it 'preserve object identity' do
391
+ expect(@session.find('//h1') == @session.find('//h1')).to be true # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
392
+ expect(@session.find('//h1') === @session.find('//h1')).to be true # rubocop:disable Style/CaseEquality, Lint/BinaryOperatorWithIdenticalOperands
393
+ expect(@session.find('//h1').eql?(@session.find('//h1'))).to be false
264
394
  end
265
395
 
266
- it "returns false for unrelated object" do
267
- expect(@session.find('//h1') == "Not Capybara::Node::Base").to be false
396
+ it 'returns false for unrelated object' do
397
+ expect(@session.find('//h1') == 'Not Capybara::Node::Base').to be false
268
398
  end
269
399
  end
270
400
 
271
- describe "#path" do
401
+ describe '#path' do
272
402
  # Testing for specific XPaths here doesn't make sense since there
273
403
  # are many that can refer to the same element
274
- before :each do
404
+ before do
275
405
  @session.visit('/path')
276
406
  end
277
407
 
278
- it "returns xpath which points to itself" do
408
+ it 'returns xpath which points to itself' do
279
409
  element = @session.find(:link, 'Second Link')
280
410
  expect(@session.find(:xpath, element.path)).to eq(element)
281
411
  end
412
+
413
+ it 'reports when element in shadow dom', requires: [:shadow_dom] do
414
+ @session.visit('/with_js')
415
+ shadow = @session.find(:css, '#shadow')
416
+ element = @session.evaluate_script(<<~JS, shadow)
417
+ (function(root){
418
+ return root.shadowRoot.querySelector('span');
419
+ })(arguments[0])
420
+ JS
421
+ expect(element.path).to eq '(: Shadow DOM element - no XPath :)'
422
+ end
282
423
  end
283
424
 
284
- describe "#trigger", requires: [:js, :trigger] do
285
- it "should allow triggering of custom JS events" do
425
+ describe '#trigger', requires: %i[js trigger] do
426
+ it 'should allow triggering of custom JS events' do
286
427
  @session.visit('/with_js')
287
428
  @session.find(:css, '#with_focus_event').trigger(:focus)
288
429
  expect(@session).to have_css('#focus_event_triggered')
289
430
  end
290
431
  end
291
432
 
292
- describe '#drag_to', requires: [:js, :drag] do
293
- it "should drag and drop an object" do
433
+ describe '#drag_to', requires: %i[js drag] do
434
+ it 'should drag and drop an object' do
294
435
  @session.visit('/with_js')
295
436
  element = @session.find('//div[@id="drag"]')
296
437
  target = @session.find('//div[@id="drop"]')
297
438
  element.drag_to(target)
298
- expect(@session.find('//div[contains(., "Dropped!")]')).not_to be_nil
439
+ expect(@session).to have_xpath('//div[contains(., "Dropped!")]')
299
440
  end
300
441
 
301
- it "should drag and drop if scrolling is needed" do
442
+ it 'should drag and drop if scrolling is needed' do
302
443
  @session.visit('/with_js')
303
444
  element = @session.find('//div[@id="drag_scroll"]')
304
445
  target = @session.find('//div[@id="drop_scroll"]')
305
446
  element.drag_to(target)
306
- expect(@session.find('//div[contains(., "Dropped!")]')).not_to be_nil
447
+ expect(@session).to have_xpath('//div[contains(., "Dropped!")]')
448
+ end
449
+
450
+ it 'should drag a link' do
451
+ @session.visit('/with_js')
452
+ link = @session.find_link('drag_link')
453
+ target = @session.find(:id, 'drop')
454
+ link.drag_to target
455
+ expect(@session).to have_xpath('//div[contains(., "Dropped!")]')
456
+ end
457
+
458
+ it 'should work with Dragula' do
459
+ @session.visit('/with_dragula')
460
+ @session.within(:css, '#sortable.ready') do
461
+ src = @session.find('div', text: 'Item 1')
462
+ target = @session.find('div', text: 'Item 3')
463
+ src.drag_to target
464
+ expect(@session).to have_content(/Item 2.*Item 1/, normalize_ws: true)
465
+ end
466
+ end
467
+
468
+ it 'should work with jsTree' do
469
+ @session.visit('/with_jstree')
470
+ @session.within(:css, '#container') do
471
+ @session.assert_text(/A.*B.*C/m)
472
+ source = @session.find(:css, '#j1_1_anchor')
473
+ target = @session.find(:css, '#j1_2_anchor')
474
+
475
+ source.drag_to(target)
476
+
477
+ @session.assert_no_text(/A.*B.*C/m)
478
+ @session.assert_text(/B.*C/m)
479
+ end
480
+ end
481
+
482
+ it 'should simulate a single held down modifier key' do
483
+ %I[
484
+ alt
485
+ ctrl
486
+ meta
487
+ shift
488
+ ].each do |modifier_key|
489
+ @session.visit('/with_js')
490
+
491
+ element = @session.find('//div[@id="drag"]')
492
+ target = @session.find('//div[@id="drop"]')
493
+
494
+ element.drag_to(target, drop_modifiers: modifier_key)
495
+ expect(@session).to have_css('div.drag_start', exact_text: 'Dragged!')
496
+ expect(@session).to have_xpath("//div[contains(., 'Dropped!-#{modifier_key}')]")
497
+ end
498
+ end
499
+
500
+ it 'should simulate multiple held down modifier keys' do
501
+ @session.visit('/with_js')
502
+
503
+ element = @session.find('//div[@id="drag"]')
504
+ target = @session.find('//div[@id="drop"]')
505
+
506
+ modifier_keys = %I[alt ctrl meta shift]
507
+
508
+ element.drag_to(target, drop_modifiers: modifier_keys)
509
+ expect(@session).to have_xpath("//div[contains(., 'Dropped!-#{modifier_keys.join('-')}')]")
510
+ end
511
+
512
+ it 'should support key aliases' do
513
+ { control: :ctrl,
514
+ command: :meta,
515
+ cmd: :meta }.each do |(key_alias, key)|
516
+ @session.visit('/with_js')
517
+
518
+ element = @session.find('//div[@id="drag"]')
519
+ target = @session.find('//div[@id="drop"]')
520
+
521
+ element.drag_to(target, drop_modifiers: [key_alias])
522
+ expect(target).to have_text("Dropped!-#{key}", exact: true)
523
+ end
524
+ end
525
+
526
+ context 'HTML5', requires: %i[js html5_drag] do
527
+ it 'should HTML5 drag and drop an object' do
528
+ @session.visit('/with_js')
529
+ element = @session.find('//div[@id="drag_html5"]')
530
+ target = @session.find('//div[@id="drop_html5"]')
531
+ element.drag_to(target)
532
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5")]')
533
+ end
534
+
535
+ it 'should HTML5 drag and drop an object child' do
536
+ @session.visit('/with_js')
537
+ element = @session.find('//div[@id="drag_html5"]/p')
538
+ target = @session.find('//div[@id="drop_html5"]')
539
+ element.drag_to(target)
540
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5")]')
541
+ end
542
+
543
+ it 'should set clientX/Y in dragover events' do
544
+ @session.visit('/with_js')
545
+ element = @session.find('//div[@id="drag_html5"]')
546
+ target = @session.find('//div[@id="drop_html5"]')
547
+ element.drag_to(target)
548
+ expect(@session).to have_css('div.log', text: /DragOver with client position: [1-9]\d*,[1-9]\d*/, count: 2)
549
+ end
550
+
551
+ it 'should preserve clientX/Y from last dragover event' do
552
+ @session.visit('/with_js')
553
+ element = @session.find('//div[@id="drag_html5"]')
554
+ target = @session.find('//div[@id="drop_html5"]')
555
+ element.drag_to(target)
556
+
557
+ conditions = %w[DragLeave Drop DragEnd].map do |text|
558
+ have_css('div.log', text: text)
559
+ end
560
+ expect(@session).to(conditions.reduce { |memo, cond| memo.and(cond) })
561
+
562
+ # The first "DragOver" div is inserted by the last dragover event dispatched
563
+ drag_over_div = @session.first('//div[@class="log" and starts-with(text(), "DragOver")]')
564
+ position = drag_over_div.text.sub('DragOver ', '')
565
+
566
+ expect(@session).to have_css('div.log', text: /DragLeave #{position}/, count: 1)
567
+ expect(@session).to have_css('div.log', text: /Drop #{position}/, count: 1)
568
+ expect(@session).to have_css('div.log', text: /DragEnd #{position}/, count: 1)
569
+ end
570
+
571
+ it 'should not HTML5 drag and drop on a non HTML5 drop element' do
572
+ @session.visit('/with_js')
573
+ element = @session.find('//div[@id="drag_html5"]')
574
+ target = @session.find('//div[@id="drop_html5"]')
575
+ target.execute_script("$(this).removeClass('drop');")
576
+ element.drag_to(target)
577
+ sleep 1
578
+ expect(@session).not_to have_xpath('//div[contains(., "HTML5 Dropped")]')
579
+ end
580
+
581
+ it 'should HTML5 drag and drop when scrolling needed' do
582
+ @session.visit('/with_js')
583
+ element = @session.find('//div[@id="drag_html5_scroll"]')
584
+ target = @session.find('//div[@id="drop_html5_scroll"]')
585
+ element.drag_to(target)
586
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain drag_html5_scroll")]')
587
+ end
588
+
589
+ it 'should drag HTML5 default draggable elements' do
590
+ @session.visit('/with_js')
591
+ link = @session.find_link('drag_link_html5')
592
+ target = @session.find(:id, 'drop_html5')
593
+ link.drag_to target
594
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped")]')
595
+ end
596
+
597
+ it 'should work with SortableJS' do
598
+ @session.visit('/with_sortable_js')
599
+ @session.within(:css, '#sortable') do
600
+ src = @session.find('div', text: 'Item 1')
601
+ target = @session.find('div', text: 'Item 3')
602
+ src.drag_to target
603
+ expect(@session).to have_content(/Item 3.*Item 1/, normalize_ws: true)
604
+ end
605
+ end
606
+
607
+ it 'should drag HTML5 default draggable element child' do
608
+ @session.visit('/with_js')
609
+ source = @session.find_link('drag_link_html5').find(:css, 'p')
610
+ target = @session.find(:id, 'drop_html5')
611
+ source.drag_to target
612
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped")]')
613
+ end
614
+
615
+ it 'should simulate a single held down modifier key' do
616
+ %I[alt ctrl meta shift].each do |modifier_key|
617
+ @session.visit('/with_js')
618
+
619
+ element = @session.find('//div[@id="drag_html5"]')
620
+ target = @session.find('//div[@id="drop_html5"]')
621
+
622
+ element.drag_to(target, drop_modifiers: modifier_key)
623
+
624
+ expect(@session).to have_css('div.drag_start', exact_text: 'HTML5 Dragged!')
625
+ expect(@session).to have_xpath("//div[contains(., 'HTML5 Dropped string: text/plain drag_html5-#{modifier_key}')]")
626
+ end
627
+ end
628
+
629
+ it 'should simulate multiple held down modifier keys' do
630
+ @session.visit('/with_js')
631
+
632
+ element = @session.find('//div[@id="drag_html5"]')
633
+ target = @session.find('//div[@id="drop_html5"]')
634
+
635
+ modifier_keys = %I[alt ctrl meta shift]
636
+
637
+ element.drag_to(target, drop_modifiers: modifier_keys)
638
+ expect(@session).to have_xpath("//div[contains(., 'HTML5 Dropped string: text/plain drag_html5-#{modifier_keys.join('-')}')]")
639
+ end
640
+
641
+ it 'should support key aliases' do
642
+ { control: :ctrl,
643
+ command: :meta,
644
+ cmd: :meta }.each do |(key_alias, key)|
645
+ @session.visit('/with_js')
646
+
647
+ element = @session.find('//div[@id="drag_html5"]')
648
+ target = @session.find('//div[@id="drop_html5"]')
649
+
650
+ element.drag_to(target, drop_modifiers: [key_alias])
651
+ expect(target).to have_text(%r{^HTML5 Dropped string: text/plain drag_html5-#{key}$}m, exact: true)
652
+ end
653
+ end
654
+ end
655
+ end
656
+
657
+ describe 'Element#drop', requires: %i[js html5_drag] do
658
+ it 'can drop a file' do
659
+ @session.visit('/with_js')
660
+ target = @session.find('//div[@id="drop_html5"]')
661
+ target.drop(
662
+ with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__)))
663
+ )
664
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]')
665
+ end
666
+
667
+ it 'can drop multiple files' do
668
+ @session.visit('/with_js')
669
+ target = @session.find('//div[@id="drop_html5"]')
670
+ target.drop(
671
+ with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__))),
672
+ with_os_path_separators(File.expand_path('../fixtures/test_file.txt', File.dirname(__FILE__)))
673
+ )
674
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]')
675
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: test_file.txt")]')
676
+ end
677
+
678
+ it 'can drop strings' do
679
+ @session.visit('/with_js')
680
+ target = @session.find('//div[@id="drop_html5"]')
681
+ target.drop('text/plain' => 'Some dropped text')
682
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain Some dropped text")]')
683
+ end
684
+
685
+ it 'can drop a pathname' do
686
+ @session.visit('/with_js')
687
+ target = @session.find('//div[@id="drop_html5"]')
688
+ target.drop(
689
+ Pathname.new(with_os_path_separators(File.expand_path('../fixtures/capybara.jpg', File.dirname(__FILE__))))
690
+ )
691
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped file: capybara.jpg")]')
692
+ end
693
+
694
+ it 'can drop multiple strings' do
695
+ @session.visit('/with_js')
696
+ target = @session.find('//div[@id="drop_html5"]')
697
+ target.drop('text/plain' => 'Some dropped text', 'text/url' => 'http://www.google.com')
698
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/plain Some dropped text")]')
699
+ expect(@session).to have_xpath('//div[contains(., "HTML5 Dropped string: text/url http://www.google.com")]')
307
700
  end
308
701
  end
309
702
 
310
703
  describe '#hover', requires: [:hover] do
311
- it "should allow hovering on an element" do
704
+ it 'should allow hovering on an element' do
312
705
  @session.visit('/with_hover')
313
706
  expect(@session.find(:css, '.wrapper:not(.scroll_needed) .hidden_until_hover', visible: false)).not_to be_visible
314
- @session.find(:css,'.wrapper:not(.scroll_needed)').hover
707
+ @session.find(:css, '.wrapper:not(.scroll_needed)').hover
315
708
  expect(@session.find(:css, '.wrapper:not(.scroll_needed) .hidden_until_hover', visible: false)).to be_visible
316
709
  end
317
710
 
318
- it "should allow hovering on an element that needs to be scrolled into view" do
711
+ it 'should allow hovering on an element that needs to be scrolled into view' do
319
712
  @session.visit('/with_hover')
320
713
  expect(@session.find(:css, '.wrapper.scroll_needed .hidden_until_hover', visible: false)).not_to be_visible
321
- @session.find(:css,'.wrapper.scroll_needed').hover
714
+ @session.find(:css, '.wrapper.scroll_needed').hover
322
715
  expect(@session.find(:css, '.wrapper.scroll_needed .hidden_until_hover', visible: false)).to be_visible
323
716
  end
717
+
718
+ it 'should hover again after following a link and back' do
719
+ @session.visit('/with_hover')
720
+ @session.find(:css, '.wrapper:not(.scroll_needed)').hover
721
+ @session.click_link('Other hover page')
722
+ @session.click_link('Go back')
723
+ @session.find(:css, '.wrapper:not(.scroll_needed)').hover
724
+ expect(@session.find(:css, '.wrapper:not(.scroll_needed) .hidden_until_hover', visible: false)).to be_visible
725
+ end
324
726
  end
325
727
 
326
728
  describe '#click' do
327
- it "should not follow a link if no href" do
729
+ it 'should not follow a link if no href' do
328
730
  @session.find(:css, '#link_placeholder').click
329
731
  expect(@session.current_url).to match(%r{/with_html$})
330
732
  end
331
733
 
332
- it "should go to the same page if href is blank" do
734
+ it 'should go to the same page if href is blank' do
333
735
  @session.find(:css, '#link_blank_href').click
334
736
  sleep 1
335
737
  expect(@session).to have_current_path('/with_html')
336
738
  end
337
739
 
338
- it "should be able to check a checkbox" do
740
+ it 'should be able to check a checkbox' do
339
741
  @session.visit('form')
340
742
  cbox = @session.find(:checkbox, 'form_terms_of_use')
341
743
  expect(cbox).not_to be_checked
@@ -343,7 +745,7 @@ Capybara::SpecHelper.spec "node" do
343
745
  expect(cbox).to be_checked
344
746
  end
345
747
 
346
- it "should be able to uncheck a checkbox" do
748
+ it 'should be able to uncheck a checkbox' do
347
749
  @session.visit('/form')
348
750
  cbox = @session.find(:checkbox, 'form_pets_dog')
349
751
  expect(cbox).to be_checked
@@ -351,65 +753,523 @@ Capybara::SpecHelper.spec "node" do
351
753
  expect(cbox).not_to be_checked
352
754
  end
353
755
 
354
- it "should be able to select a radio button" do
756
+ it 'should be able to select a radio button' do
355
757
  @session.visit('/form')
356
758
  radio = @session.find(:radio_button, 'gender_male')
357
759
  expect(radio).not_to be_checked
358
760
  radio.click
359
761
  expect(radio).to be_checked
360
762
  end
763
+
764
+ it 'should allow modifiers', requires: [:js] do
765
+ @session.visit('/with_js')
766
+ @session.find(:css, '#click-test').click(:shift)
767
+ expect(@session).to have_link('Has been shift clicked')
768
+ end
769
+
770
+ it 'should allow multiple modifiers', requires: [:js] do
771
+ @session.visit('with_js')
772
+ @session.find(:css, '#click-test').click(:control, :alt, :meta, :shift)
773
+ # Selenium with Chrome on OSX ctrl-click generates a right click so just verify all keys but not click type
774
+ expect(@session).to have_link('alt control meta shift')
775
+ end
776
+
777
+ it 'should allow to adjust the click offset', requires: [:js] do
778
+ Capybara.w3c_click_offset = false
779
+ @session.visit('with_js')
780
+ @session.find(:css, '#click-test').click(x: 5, y: 5)
781
+ link = @session.find(:link, 'has-been-clicked')
782
+ locations = link.text.match(/^Has been clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
783
+ # Resulting click location should be very close to 0, 0 relative to top left corner of the element, but may not be exact due to
784
+ # integer/float conversions and rounding.
785
+ expect(locations[:x].to_f).to be_within(1).of(5)
786
+ expect(locations[:y].to_f).to be_within(1).of(5)
787
+ end
788
+
789
+ it 'should raise error if both x and y values are not passed' do
790
+ @session.visit('with_js')
791
+ el = @session.find(:css, '#click-test')
792
+ expect { el.click(x: 5) }.to raise_error ArgumentError
793
+ expect { el.click(x: nil, y: 3) }.to raise_error ArgumentError
794
+ end
795
+
796
+ it 'should be able to click a table row', requires: [:js] do
797
+ @session.visit('/tables')
798
+ tr = @session.find(:css, '#agent_table tr:first-child').click
799
+ expect(tr).to have_css('label', text: 'Clicked')
800
+ end
801
+
802
+ it 'should retry clicking', requires: [:js] do
803
+ @session.visit('/animated')
804
+ obscured = @session.find(:css, '#obscured')
805
+ @session.execute_script <<~JS
806
+ setTimeout(function(){ $('#cover').hide(); }, 700)
807
+ JS
808
+ expect { obscured.click }.not_to raise_error
809
+ end
810
+
811
+ it 'should allow to retry longer', requires: [:js] do
812
+ @session.visit('/animated')
813
+ obscured = @session.find(:css, '#obscured')
814
+ @session.execute_script <<~JS
815
+ setTimeout(function(){ $('#cover').hide(); }, 3000)
816
+ JS
817
+ expect { obscured.click(wait: 4) }.not_to raise_error
818
+ end
819
+
820
+ it 'should not retry clicking when wait is disabled', requires: [:js] do
821
+ @session.visit('/animated')
822
+ obscured = @session.find(:css, '#obscured')
823
+ @session.execute_script <<~JS
824
+ setTimeout(function(){ $('#cover').hide(); }, 2000)
825
+ JS
826
+ expect { obscured.click(wait: 0) }.to(raise_error { |e| expect(e).to be_an_invalid_element_error(@session) })
827
+ end
828
+
829
+ context 'offset', requires: [:js] do
830
+ before do
831
+ @session.visit('/offset')
832
+ end
833
+
834
+ let :clicker do
835
+ @session.find(:id, 'clicker')
836
+ end
837
+
838
+ context 'when w3c_click_offset is false' do
839
+ before do
840
+ Capybara.w3c_click_offset = false
841
+ end
842
+
843
+ it 'should offset from top left of element' do
844
+ clicker.click(x: 10, y: 5)
845
+ expect(@session).to have_text(/clicked at 110,105/)
846
+ end
847
+
848
+ it 'should offset outside the element' do
849
+ clicker.click(x: -15, y: -10)
850
+ expect(@session).to have_text(/clicked at 85,90/)
851
+ end
852
+
853
+ it 'should default to click the middle' do
854
+ clicker.click
855
+ expect(@session).to have_text(/clicked at 150,150/)
856
+ end
857
+ end
858
+
859
+ context 'when w3c_click_offset is true' do
860
+ before do
861
+ Capybara.w3c_click_offset = true
862
+ end
863
+
864
+ it 'should offset from center of element' do
865
+ clicker.click(x: 10, y: 5)
866
+ expect(@session).to have_text(/clicked at 160,155/)
867
+ end
868
+
869
+ it 'should offset outside from center of element' do
870
+ clicker.click(x: -65, y: -60)
871
+ expect(@session).to have_text(/clicked at 85,90/)
872
+ end
873
+
874
+ it 'should default to click the middle' do
875
+ clicker.click
876
+ expect(@session).to have_text(/clicked at 150,150/)
877
+ end
878
+ end
879
+ end
880
+
881
+ context 'delay', requires: [:js] do
882
+ it 'should delay the mouse up' do
883
+ @session.visit('with_js')
884
+ @session.find(:css, '#click-test').click(delay: 2)
885
+ delay = @session.evaluate_script('window.click_delay')
886
+ expect(delay).to be >= 2
887
+ end
888
+ end
361
889
  end
362
890
 
363
891
  describe '#double_click', requires: [:js] do
364
- it "should double click an element" do
365
- pending "selenium-webdriver/geckodriver doesn't generate double click event" if marionette?(@session)
892
+ it 'should double click an element' do
366
893
  @session.visit('/with_js')
367
894
  @session.find(:css, '#click-test').double_click
368
- expect(@session.find(:css, '#has-been-double-clicked')).to be
895
+ expect(@session.find(:css, '#has-been-double-clicked')).to be_truthy
896
+ end
897
+
898
+ it 'should allow modifiers', requires: [:js] do
899
+ @session.visit('/with_js')
900
+ @session.find(:css, '#click-test').double_click(:alt)
901
+ expect(@session).to have_link('Has been alt double clicked')
902
+ end
903
+
904
+ it 'should allow to adjust the offset', requires: [:js] do
905
+ Capybara.w3c_click_offset = false
906
+ @session.visit('with_js')
907
+ @session.find(:css, '#click-test').double_click(x: 10, y: 5)
908
+ link = @session.find(:link, 'has-been-double-clicked')
909
+ locations = link.text.match(/^Has been double clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
910
+ # Resulting click location should be very close to 10, 5 relative to top left corner of the element, but may not be exact due
911
+ # to integer/float conversions and rounding.
912
+ expect(locations[:x].to_f).to be_within(1).of(10)
913
+ expect(locations[:y].to_f).to be_within(1).of(5)
914
+ end
915
+
916
+ it 'should retry clicking', requires: [:js] do
917
+ @session.visit('/animated')
918
+ obscured = @session.find(:css, '#obscured')
919
+ @session.execute_script <<~JS
920
+ setTimeout(function(){ $('#cover').hide(); }, 700)
921
+ JS
922
+ expect { obscured.double_click }.not_to raise_error
923
+ end
924
+
925
+ context 'offset', requires: [:js] do
926
+ before do
927
+ @session.visit('/offset')
928
+ end
929
+
930
+ let :clicker do
931
+ @session.find(:id, 'clicker')
932
+ end
933
+
934
+ context 'when w3c_click_offset is false' do
935
+ before do
936
+ Capybara.w3c_click_offset = false
937
+ end
938
+
939
+ it 'should offset from top left of element' do
940
+ clicker.double_click(x: 10, y: 5)
941
+ expect(@session).to have_text(/clicked at 110,105/)
942
+ end
943
+
944
+ it 'should offset outside the element' do
945
+ clicker.double_click(x: -15, y: -10)
946
+ expect(@session).to have_text(/clicked at 85,90/)
947
+ end
948
+
949
+ it 'should default to click the middle' do
950
+ clicker.double_click
951
+ expect(@session).to have_text(/clicked at 150,150/)
952
+ end
953
+ end
954
+
955
+ context 'when w3c_click_offset is true' do
956
+ before do
957
+ Capybara.w3c_click_offset = true
958
+ end
959
+
960
+ it 'should offset from center of element' do
961
+ clicker.double_click(x: 10, y: 5)
962
+ expect(@session).to have_text(/clicked at 160,155/)
963
+ end
964
+
965
+ it 'should offset outside from center of element' do
966
+ clicker.double_click(x: -65, y: -60)
967
+ expect(@session).to have_text(/clicked at 85,90/)
968
+ end
969
+
970
+ it 'should default to click the middle' do
971
+ clicker.double_click
972
+ expect(@session).to have_text(/clicked at 150,150/)
973
+ end
974
+ end
369
975
  end
370
976
  end
371
977
 
372
978
  describe '#right_click', requires: [:js] do
373
- it "should right click an element" do
979
+ it 'should right click an element' do
374
980
  @session.visit('/with_js')
375
981
  @session.find(:css, '#click-test').right_click
376
- expect(@session.find(:css, '#has-been-right-clicked')).to be
982
+ expect(@session.find(:css, '#has-been-right-clicked')).to be_truthy
983
+ end
984
+
985
+ it 'should allow modifiers', requires: [:js] do
986
+ @session.visit('/with_js')
987
+ @session.find(:css, '#click-test').right_click(:meta)
988
+ expect(@session).to have_link('Has been meta right clicked')
989
+ end
990
+
991
+ it 'should allow to adjust the offset', requires: [:js] do
992
+ Capybara.w3c_click_offset = false
993
+ @session.visit('with_js')
994
+ @session.find(:css, '#click-test').right_click(x: 10, y: 10)
995
+ link = @session.find(:link, 'has-been-right-clicked')
996
+ locations = link.text.match(/^Has been right clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
997
+ # Resulting click location should be very close to 10, 10 relative to top left corner of the element, but may not be exact due
998
+ # to integer/float conversions and rounding
999
+ expect(locations[:x].to_f).to be_within(1).of(10)
1000
+ expect(locations[:y].to_f).to be_within(1).of(10)
1001
+ end
1002
+
1003
+ it 'should retry clicking', requires: [:js] do
1004
+ @session.visit('/animated')
1005
+ obscured = @session.find(:css, '#obscured')
1006
+ @session.execute_script <<~JS
1007
+ setTimeout(function(){ $('#cover').hide(); }, 700)
1008
+ JS
1009
+ expect { obscured.right_click }.not_to raise_error
1010
+ end
1011
+
1012
+ context 'offset', requires: [:js] do
1013
+ before do
1014
+ @session.visit('/offset')
1015
+ end
1016
+
1017
+ let :clicker do
1018
+ @session.find(:id, 'clicker')
1019
+ end
1020
+
1021
+ context 'when w3c_click_offset is false' do
1022
+ before do
1023
+ Capybara.w3c_click_offset = false
1024
+ end
1025
+
1026
+ it 'should offset from top left of element' do
1027
+ clicker.right_click(x: 10, y: 5)
1028
+ expect(@session).to have_text(/clicked at 110,105/)
1029
+ end
1030
+
1031
+ it 'should offset outside the element' do
1032
+ clicker.right_click(x: -15, y: -10)
1033
+ expect(@session).to have_text(/clicked at 85,90/)
1034
+ end
1035
+
1036
+ it 'should default to click the middle' do
1037
+ clicker.right_click
1038
+ expect(@session).to have_text(/clicked at 150,150/)
1039
+ end
1040
+ end
1041
+
1042
+ context 'when w3c_click_offset is true' do
1043
+ before do
1044
+ Capybara.w3c_click_offset = true
1045
+ end
1046
+
1047
+ it 'should offset from center of element' do
1048
+ clicker.right_click(x: 10, y: 5)
1049
+ expect(@session).to have_text(/clicked at 160,155/)
1050
+ end
1051
+
1052
+ it 'should offset outside from center of element' do
1053
+ clicker.right_click(x: -65, y: -60)
1054
+ expect(@session).to have_text(/clicked at 85,90/)
1055
+ end
1056
+
1057
+ it 'should default to click the middle' do
1058
+ clicker.right_click
1059
+ expect(@session).to have_text(/clicked at 150,150/)
1060
+ end
1061
+ end
1062
+ end
1063
+
1064
+ context 'delay', requires: [:js] do
1065
+ it 'should delay the mouse up' do
1066
+ @session.visit('with_js')
1067
+ @session.find(:css, '#click-test').right_click(delay: 2)
1068
+ delay = @session.evaluate_script('window.right_click_delay')
1069
+ expect(delay).to be >= 2
1070
+ end
377
1071
  end
378
1072
  end
379
1073
 
380
1074
  describe '#send_keys', requires: [:send_keys] do
381
- it "should send a string of keys to an element" do
1075
+ it 'should send a string of keys to an element' do
382
1076
  @session.visit('/form')
383
1077
  @session.find(:css, '#address1_city').send_keys('Oceanside')
384
1078
  expect(@session.find(:css, '#address1_city').value).to eq 'Oceanside'
385
1079
  end
386
1080
 
387
- it "should send special characters" do
388
- pending "selenium-webdriver/geckodriver doesn't support complex sets of characters" if marionette?(@session)
1081
+ it 'should send special characters' do
389
1082
  @session.visit('/form')
390
1083
  @session.find(:css, '#address1_city').send_keys('Ocean', :space, 'sie', :left, 'd')
391
1084
  expect(@session.find(:css, '#address1_city').value).to eq 'Ocean side'
392
1085
  end
393
1086
 
394
- it "should allow for multiple simultaneous keys" do
395
- pending "selenium-webdriver/geckodriver doesn't support complex sets of characters" if marionette?(@session)
1087
+ it 'should allow for multiple simultaneous keys' do
396
1088
  @session.visit('/form')
397
1089
  @session.find(:css, '#address1_city').send_keys([:shift, 'o'], 'ceanside')
398
1090
  expect(@session.find(:css, '#address1_city').value).to eq 'Oceanside'
399
1091
  end
400
1092
 
401
- it "should generate key events", requires: [:send_keys, :js] do
402
- pending "selenium-webdriver/geckodriver doesn't support complex sets of characters" if marionette?(@session)
1093
+ it 'should hold modifiers at top level' do
1094
+ @session.visit('/form')
1095
+ @session.find(:css, '#address1_city').send_keys('ocean', :shift, 'side')
1096
+ expect(@session.find(:css, '#address1_city').value).to eq 'oceanSIDE'
1097
+ end
1098
+
1099
+ it 'should generate key events', requires: %i[send_keys js] do
403
1100
  @session.visit('/with_js')
404
- @session.find(:css, '#with-key-events').send_keys([:shift,'t'], [:shift,'w'])
1101
+ @session.find(:css, '#with-key-events').send_keys([:shift, 't'], [:shift, 'w'])
405
1102
  expect(@session.find(:css, '#key-events-output')).to have_text('keydown:16 keydown:84 keydown:16 keydown:87')
406
1103
  end
407
1104
  end
408
1105
 
1106
+ describe '#execute_script', requires: %i[js es_args] do
1107
+ it 'should execute the given script in the context of the element and return nothing' do
1108
+ @session.visit('/with_js')
1109
+ expect(@session.find(:css, '#change').execute_script("this.textContent = 'Funky Doodle'")).to be_nil
1110
+ expect(@session).to have_css('#change', text: 'Funky Doodle')
1111
+ end
1112
+
1113
+ it 'should pass arguments to the script' do
1114
+ @session.visit('/with_js')
1115
+ @session.find(:css, '#change').execute_script('this.textContent = arguments[0]', 'Doodle Funk')
1116
+ expect(@session).to have_css('#change', text: 'Doodle Funk')
1117
+ end
1118
+ end
1119
+
1120
+ describe '#evaluate_script', requires: %i[js es_args] do
1121
+ it 'should evaluate the given script in the context of the element and return whatever it produces' do
1122
+ @session.visit('/with_js')
1123
+ el = @session.find(:css, '#with_change_event')
1124
+ expect(el.evaluate_script('this.value')).to eq('default value')
1125
+ end
1126
+
1127
+ it 'should ignore leading whitespace' do
1128
+ @session.visit('/with_js')
1129
+ expect(@session.find(:css, '#change').evaluate_script('
1130
+ 2 + 3
1131
+ ')).to eq 5
1132
+ end
1133
+
1134
+ it 'should pass arguments to the script' do
1135
+ @session.visit('/with_js')
1136
+ @session.find(:css, '#change').evaluate_script('this.textContent = arguments[0]', 'Doodle Funk')
1137
+ expect(@session).to have_css('#change', text: 'Doodle Funk')
1138
+ end
1139
+
1140
+ it 'should pass multiple arguments' do
1141
+ @session.visit('/with_js')
1142
+ change = @session.find(:css, '#change')
1143
+ expect(change.evaluate_script('arguments[0] + arguments[1]', 2, 3)).to eq 5
1144
+ end
1145
+
1146
+ it 'should support returning elements' do
1147
+ @session.visit('/with_js')
1148
+ change = @session.find(:css, '#change') # ensure page has loaded and element is available
1149
+ el = change.evaluate_script('this')
1150
+ expect(el).to be_instance_of(Capybara::Node::Element)
1151
+ expect(el).to eq(change)
1152
+ end
1153
+
1154
+ it 'should support multiple statements via IIFE' do
1155
+ @session.visit('/with_js')
1156
+ change = @session.find(:css, '#change') # ensure page has loaded and element is available
1157
+ res = change.evaluate_script(<<~JS, 3)
1158
+ (function(n){
1159
+ var el = this;
1160
+ return [el, n];
1161
+ }).apply(this, arguments)
1162
+ JS
1163
+ expect(res).to eq [change, 3]
1164
+ end
1165
+ end
1166
+
1167
+ describe '#evaluate_async_script', requires: %i[js es_args] do
1168
+ it 'should evaluate the given script in the context of the element' do
1169
+ @session.visit('/with_js')
1170
+ el = @session.find(:css, '#with_change_event')
1171
+ expect(el.evaluate_async_script('arguments[0](this.value)')).to eq('default value')
1172
+ end
1173
+
1174
+ it 'should support returning elements after asynchronous operation' do
1175
+ @session.visit('/with_js')
1176
+ change = @session.find(:css, '#change') # ensure page has loaded and element is available
1177
+ el = change.evaluate_async_script('var cb = arguments[0]; setTimeout(function(el){ cb(el) }, 100, this)')
1178
+ expect(el).to eq(change)
1179
+ end
1180
+ end
1181
+
1182
+ describe '#shadow_root', requires: %i[js] do
1183
+ it 'should get the shadow root' do
1184
+ @session.visit('/with_shadow')
1185
+ expect do
1186
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1187
+ expect(shadow_root).to be_a(Capybara::Node::Element)
1188
+ end.not_to raise_error
1189
+ end
1190
+
1191
+ it 'should find elements inside the shadow dom using CSS' do
1192
+ @session.visit('/with_shadow')
1193
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1194
+ expect(shadow_root).to have_css('#shadow_content', text: 'some text')
1195
+ end
1196
+
1197
+ it 'should find nested shadow roots' do
1198
+ @session.visit('/with_shadow')
1199
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1200
+ nested_shadow_root = shadow_root.find(:css, '#nested_shadow_host').shadow_root
1201
+ expect(nested_shadow_root).to have_css('#nested_shadow_content', text: 'nested text')
1202
+ end
1203
+
1204
+ it 'should click on elements' do
1205
+ @session.visit('/with_shadow')
1206
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1207
+ checkbox = shadow_root.find(:css, 'input[type="checkbox"]')
1208
+ expect(checkbox).not_to be_checked
1209
+ checkbox.click
1210
+ expect(checkbox).to be_checked
1211
+ end
1212
+
1213
+ it 'should use convenience methods once moved to a descendant of the shadow root' do
1214
+ @session.visit('/with_shadow')
1215
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1216
+ descendant = shadow_root.find(:css, '#controls_wrapper')
1217
+ expect do
1218
+ descendant.check('shadow_checkbox')
1219
+ end.not_to raise_error
1220
+ expect(descendant).to have_checked_field('shadow_checkbox')
1221
+ end
1222
+
1223
+ it 'should produce error messages when failing' do
1224
+ @session.visit('/with_shadow')
1225
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1226
+ expect do
1227
+ expect(shadow_root).to have_css('#shadow_content', text: 'Not in the document')
1228
+ end.to raise_error(/tag="ShadowRoot"/)
1229
+ end
1230
+
1231
+ it 'should get visible text' do
1232
+ @session.visit('/with_shadow')
1233
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1234
+ expect(shadow_root).to have_text('some text scroll.html')
1235
+ end
1236
+
1237
+ it 'should get all text' do
1238
+ @session.visit('/with_shadow')
1239
+ shadow_root = @session.find(:css, '#shadow_host').shadow_root
1240
+ expect(shadow_root).to have_text(:all, 'some text scroll.html')
1241
+ end
1242
+ end
1243
+
409
1244
  describe '#reload', requires: [:js] do
410
- context "without automatic reload" do
1245
+ it 'should reload elements found via ancestor with CSS' do
1246
+ @session.visit('/with_js')
1247
+ node = @session.find(:css, '#reload-me em').ancestor(:css, 'div')
1248
+ node.reload
1249
+ expect(node[:id]).to eq 'reload-me'
1250
+ end
1251
+
1252
+ it 'should reload elements found via ancestor with XPath' do
1253
+ @session.visit('/with_js')
1254
+ node = @session.find(:css, '#reload-me em').ancestor(:xpath, './/div')
1255
+ node.reload
1256
+ expect(node[:id]).to eq 'reload-me'
1257
+ end
1258
+
1259
+ it 'should reload elements found via sibling' do
1260
+ @session.visit('/with_js')
1261
+ node = @session.find(:css, '#the-list li', text: 'Item 1').sibling(:css, 'li')
1262
+ expect(node.text).to eq 'Item 2'
1263
+ node.reload
1264
+ expect(node.text).to eq 'Item 2'
1265
+ end
1266
+
1267
+ context 'without automatic reload' do
411
1268
  before { Capybara.automatic_reload = false }
412
- it "should reload the current context of the node" do
1269
+
1270
+ after { Capybara.automatic_reload = true }
1271
+
1272
+ it 'should reload the current context of the node' do
413
1273
  @session.visit('/with_js')
414
1274
  node = @session.find(:css, '#reload-me')
415
1275
  @session.click_link('Reload!')
@@ -418,7 +1278,7 @@ Capybara::SpecHelper.spec "node" do
418
1278
  expect(node.text).to eq('has been reloaded')
419
1279
  end
420
1280
 
421
- it "should reload a parent node" do
1281
+ it 'should reload a parent node' do
422
1282
  @session.visit('/with_js')
423
1283
  node = @session.find(:css, '#reload-me').find(:css, 'em')
424
1284
  @session.click_link('Reload!')
@@ -427,26 +1287,25 @@ Capybara::SpecHelper.spec "node" do
427
1287
  expect(node.text).to eq('has been reloaded')
428
1288
  end
429
1289
 
430
- it "should not automatically reload" do
1290
+ it 'should not automatically reload' do
431
1291
  @session.visit('/with_js')
432
1292
  node = @session.find(:css, '#reload-me')
433
1293
  @session.click_link('Reload!')
434
1294
  sleep(0.3)
435
1295
  expect do
436
1296
  expect(node).to have_text('has been reloaded')
437
- end.to raise_error do |error|
1297
+ end.to(raise_error do |error|
438
1298
  expect(error).to be_an_invalid_element_error(@session)
439
- end
1299
+ end)
440
1300
  end
441
- after { Capybara.automatic_reload = true }
442
1301
  end
443
1302
 
444
- context "with automatic reload" do
1303
+ context 'with automatic reload' do
445
1304
  before do
446
1305
  Capybara.default_max_wait_time = 4
447
1306
  end
448
1307
 
449
- it "should reload the current context of the node automatically" do
1308
+ it 'should reload the current context of the node automatically' do
450
1309
  @session.visit('/with_js')
451
1310
  node = @session.find(:css, '#reload-me')
452
1311
  @session.click_link('Reload!')
@@ -454,7 +1313,7 @@ Capybara::SpecHelper.spec "node" do
454
1313
  expect(node.text).to eq('has been reloaded')
455
1314
  end
456
1315
 
457
- it "should reload a parent node automatically" do
1316
+ it 'should reload a parent node automatically' do
458
1317
  @session.visit('/with_js')
459
1318
  node = @session.find(:css, '#reload-me').find(:css, 'em')
460
1319
  @session.click_link('Reload!')
@@ -462,7 +1321,7 @@ Capybara::SpecHelper.spec "node" do
462
1321
  expect(node.text).to eq('has been reloaded')
463
1322
  end
464
1323
 
465
- it "should reload a node automatically when using find" do
1324
+ it 'should reload a node automatically when using find' do
466
1325
  @session.visit('/with_js')
467
1326
  node = @session.find(:css, '#reload-me')
468
1327
  @session.click_link('Reload!')
@@ -478,19 +1337,15 @@ Capybara::SpecHelper.spec "node" do
478
1337
 
479
1338
  expect do
480
1339
  expect(node).to have_text('Foo')
481
- end.to raise_error { |error|
482
- expect(error).to be_an_invalid_element_error(@session)
483
- }
1340
+ end.to(raise_error { |error| expect(error).to be_an_invalid_element_error(@session) })
484
1341
  expect do
485
1342
  expect(node).to have_text('Bar')
486
- end.to raise_error { |error|
487
- expect(error).to be_an_invalid_element_error(@session)
488
- }
1343
+ end.to(raise_error { |error| expect(error).to be_an_invalid_element_error(@session) })
489
1344
  end
490
1345
 
491
- it "should reload nodes with options" do
1346
+ it 'should reload nodes with options' do
492
1347
  @session.visit('/with_js')
493
- node = @session.find(:css, 'em', text: "reloaded")
1348
+ node = @session.find(:css, 'em', text: 'reloaded')
494
1349
  @session.click_link('Reload!')
495
1350
  sleep(1)
496
1351
  expect(node.text).to eq('has been reloaded')
@@ -498,31 +1353,25 @@ Capybara::SpecHelper.spec "node" do
498
1353
  end
499
1354
  end
500
1355
 
501
- context "when #synchronize raises server errors" do
502
- it "sets an explanatory exception as the cause of server exceptions", requires: [:server, :js] do
503
- skip "This version of ruby doesn't support exception causes" unless Exception.instance_methods.include? :cause
504
- quietly { @session.visit("/error") }
1356
+ context 'when #synchronize raises server errors' do
1357
+ it 'sets an explanatory exception as the cause of server exceptions', requires: %i[server js] do
1358
+ quietly { @session.visit('/error') }
505
1359
  expect do
506
1360
  @session.find(:css, 'span')
507
- end.to raise_error(TestApp::TestAppError) do |e|
1361
+ end.to(raise_error(TestApp::TestAppError) do |e|
508
1362
  expect(e.cause).to be_a Capybara::CapybaraError
509
1363
  expect(e.cause.message).to match(/Your application server raised an error/)
510
- end
1364
+ end)
511
1365
  end
512
1366
 
513
- it "sets an explanatory exception as the cause of server exceptions with errors with initializers", requires: [:server, :js] do
514
- skip "This version of ruby doesn't support exception causes" unless Exception.instance_methods.include? :cause
515
- quietly { @session.visit("/other_error") }
1367
+ it 'sets an explanatory exception as the cause of server exceptions with errors with initializers', requires: %i[server js] do
1368
+ quietly { @session.visit('/other_error') }
516
1369
  expect do
517
1370
  @session.find(:css, 'span')
518
- end.to raise_error(TestApp::TestAppOtherError) do |e|
1371
+ end.to(raise_error(TestApp::TestAppOtherError) do |e|
519
1372
  expect(e.cause).to be_a Capybara::CapybaraError
520
1373
  expect(e.cause.message).to match(/Your application server raised an error/)
521
- end
1374
+ end)
522
1375
  end
523
1376
  end
524
-
525
- def be_an_invalid_element_error(session)
526
- satisfy { |error| session.driver.invalid_element_errors.any? { |e| error.is_a? e } }
527
- end
528
1377
  end