capybara 2.15.1 → 3.36.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (311) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +1 -1
  3. data/History.md +902 -10
  4. data/License.txt +1 -1
  5. data/README.md +103 -75
  6. data/lib/capybara/config.rb +29 -56
  7. data/lib/capybara/cucumber.rb +2 -3
  8. data/lib/capybara/driver/base.rb +39 -18
  9. data/lib/capybara/driver/node.rb +36 -10
  10. data/lib/capybara/dsl.rb +15 -6
  11. data/lib/capybara/helpers.rb +65 -30
  12. data/lib/capybara/minitest/spec.rb +173 -81
  13. data/lib/capybara/minitest.rb +220 -111
  14. data/lib/capybara/node/actions.rb +274 -171
  15. data/lib/capybara/node/base.rb +41 -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 +353 -137
  19. data/lib/capybara/node/finders.rb +144 -138
  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 +13 -10
  24. data/lib/capybara/queries/base_query.rb +39 -28
  25. data/lib/capybara/queries/current_path_query.rb +22 -25
  26. data/lib/capybara/queries/match_query.rb +14 -7
  27. data/lib/capybara/queries/selector_query.rb +646 -145
  28. data/lib/capybara/queries/sibling_query.rb +12 -10
  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 +57 -41
  33. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  34. data/lib/capybara/rack_test/driver.rb +18 -13
  35. data/lib/capybara/rack_test/errors.rb +6 -0
  36. data/lib/capybara/rack_test/form.rb +73 -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 +44 -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 +45 -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 +144 -264
  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 +279 -0
  86. data/lib/capybara/selector/filter.rb +2 -17
  87. data/lib/capybara/selector/filter_set.rb +81 -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 +214 -0
  93. data/lib/capybara/selector/selector.rb +89 -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 +343 -276
  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 +228 -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 +40 -0
  112. data/lib/capybara/selenium/node.rb +508 -124
  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 +69 -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 +79 -59
  129. data/lib/capybara/session/matchers.rb +41 -25
  130. data/lib/capybara/session.rb +371 -357
  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 +159 -13
  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 +34 -6
  137. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  138. data/lib/capybara/spec/session/all_spec.rb +161 -55
  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.rb → assert_text_spec.rb} +91 -59
  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 +138 -69
  147. data/lib/capybara/spec/session/body_spec.rb +12 -13
  148. data/lib/capybara/spec/session/check_spec.rb +116 -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 +11 -10
  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 +23 -0
  162. data/lib/capybara/spec/session/evaluate_script_spec.rb +30 -9
  163. data/lib/capybara/spec/session/execute_script_spec.rb +10 -8
  164. data/lib/capybara/spec/session/fill_in_spec.rb +128 -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 +245 -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 +69 -0
  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 +94 -13
  181. data/lib/capybara/spec/session/has_css_spec.rb +272 -137
  182. data/lib/capybara/spec/session/has_current_path_spec.rb +87 -45
  183. data/lib/capybara/spec/session/has_field_spec.rb +139 -59
  184. data/lib/capybara/spec/session/has_link_spec.rb +34 -9
  185. data/lib/capybara/spec/session/has_none_selectors_spec.rb +78 -0
  186. data/lib/capybara/spec/session/has_select_spec.rb +103 -74
  187. data/lib/capybara/spec/session/has_selector_spec.rb +105 -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 +113 -61
  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 +950 -152
  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 +69 -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 -80
  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 +99 -32
  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 +70 -44
  224. data/lib/capybara/spec/spec_helper.rb +49 -43
  225. data/lib/capybara/spec/test_app.rb +89 -42
  226. data/lib/capybara/spec/views/animated.erb +49 -0
  227. data/lib/capybara/spec/views/form.erb +141 -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 +11 -0
  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 +18 -0
  247. data/lib/capybara/spec/views/with_hover.erb +8 -2
  248. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  249. data/lib/capybara/spec/views/with_html.erb +70 -11
  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 +39 -3
  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_other.erb +6 -0
  256. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  257. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  258. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  259. data/lib/capybara/spec/views/with_windows.erb +1 -1
  260. data/lib/capybara/spec/views/within_frames.erb +5 -2
  261. data/lib/capybara/version.rb +2 -1
  262. data/lib/capybara/window.rb +35 -33
  263. data/lib/capybara.rb +126 -103
  264. data/spec/basic_node_spec.rb +60 -34
  265. data/spec/capybara_spec.rb +53 -104
  266. data/spec/css_builder_spec.rb +101 -0
  267. data/spec/css_splitter_spec.rb +38 -0
  268. data/spec/dsl_spec.rb +81 -62
  269. data/spec/filter_set_spec.rb +27 -9
  270. data/spec/fixtures/certificate.pem +25 -0
  271. data/spec/fixtures/key.pem +27 -0
  272. data/spec/fixtures/selenium_driver_rspec_failure.rb +5 -4
  273. data/spec/fixtures/selenium_driver_rspec_success.rb +5 -4
  274. data/spec/minitest_spec.rb +49 -7
  275. data/spec/minitest_spec_spec.rb +94 -59
  276. data/spec/per_session_config_spec.rb +14 -13
  277. data/spec/rack_test_spec.rb +180 -125
  278. data/spec/regexp_dissassembler_spec.rb +250 -0
  279. data/spec/result_spec.rb +99 -45
  280. data/spec/rspec/features_spec.rb +37 -31
  281. data/spec/rspec/scenarios_spec.rb +9 -7
  282. data/spec/rspec/shared_spec_matchers.rb +448 -421
  283. data/spec/rspec/views_spec.rb +5 -3
  284. data/spec/rspec_matchers_spec.rb +27 -11
  285. data/spec/rspec_spec.rb +109 -89
  286. data/spec/sauce_spec_chrome.rb +43 -0
  287. data/spec/selector_spec.rb +396 -67
  288. data/spec/selenium_spec_chrome.rb +183 -35
  289. data/spec/selenium_spec_chrome_remote.rb +101 -0
  290. data/spec/selenium_spec_edge.rb +47 -0
  291. data/spec/selenium_spec_firefox.rb +184 -41
  292. data/spec/selenium_spec_firefox_remote.rb +80 -0
  293. data/spec/selenium_spec_ie.rb +147 -0
  294. data/spec/selenium_spec_safari.rb +156 -0
  295. data/spec/server_spec.rb +198 -99
  296. data/spec/session_spec.rb +53 -16
  297. data/spec/shared_selenium_node.rb +79 -0
  298. data/spec/shared_selenium_session.rb +489 -97
  299. data/spec/spec_helper.rb +93 -7
  300. data/spec/xpath_builder_spec.rb +93 -0
  301. metadata +365 -70
  302. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  303. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  304. data/.yard/yard_extensions.rb +0 -78
  305. data/lib/capybara/query.rb +0 -7
  306. data/lib/capybara/rspec/compound.rb +0 -95
  307. data/lib/capybara/spec/session/assert_current_path.rb +0 -72
  308. data/lib/capybara/spec/session/assert_selector.rb +0 -148
  309. data/lib/capybara/spec/session/source_spec.rb +0 -0
  310. data/lib/capybara/spec/views/with_title.erb +0 -5
  311. data/spec/selenium_spec_marionette.rb +0 -117
@@ -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
191
+ @session.visit('/form')
192
+ expect(@session.find('//option', text: 'A.B.1')).to be_disabled
193
+ expect(@session.find('//option', text: 'A.2')).not_to be_disabled
194
+ end
195
+
196
+ it 'should see a disabled fieldset as disabled' do
176
197
  @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
198
+ expect(@session.find(:xpath, './/fieldset[@id="form_disabled_fieldset"]')).to be_disabled
179
199
  end
180
200
 
181
- context "in a disabled fieldset" do
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,71 +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!")]')
440
+ end
441
+
442
+ it 'should drag and drop if scrolling is needed' do
443
+ @session.visit('/with_js')
444
+ element = @session.find('//div[@id="drag_scroll"]')
445
+ target = @session.find('//div[@id="drop_scroll"]')
446
+ element.drag_to(target)
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")]')
299
700
  end
300
701
  end
301
702
 
302
703
  describe '#hover', requires: [:hover] do
303
- it "should allow hovering on an element" do
704
+ it 'should allow hovering on an element' do
705
+ @session.visit('/with_hover')
706
+ expect(@session.find(:css, '.wrapper:not(.scroll_needed) .hidden_until_hover', visible: false)).not_to be_visible
707
+ @session.find(:css, '.wrapper:not(.scroll_needed)').hover
708
+ expect(@session.find(:css, '.wrapper:not(.scroll_needed) .hidden_until_hover', visible: false)).to be_visible
709
+ end
710
+
711
+ it 'should allow hovering on an element that needs to be scrolled into view' do
712
+ @session.visit('/with_hover')
713
+ expect(@session.find(:css, '.wrapper.scroll_needed .hidden_until_hover', visible: false)).not_to be_visible
714
+ @session.find(:css, '.wrapper.scroll_needed').hover
715
+ expect(@session.find(:css, '.wrapper.scroll_needed .hidden_until_hover', visible: false)).to be_visible
716
+ end
717
+
718
+ it 'should hover again after following a link and back' do
304
719
  @session.visit('/with_hover')
305
- expect(@session.find(:css,'.hidden_until_hover', visible: false)).not_to be_visible
306
- @session.find(:css,'.wrapper').hover
307
- expect(@session.find(:css, '.hidden_until_hover', visible: false)).to be_visible
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
308
725
  end
309
726
  end
310
727
 
311
728
  describe '#click' do
312
- it "should not follow a link if no href" do
729
+ it 'should not follow a link if no href' do
313
730
  @session.find(:css, '#link_placeholder').click
314
731
  expect(@session.current_url).to match(%r{/with_html$})
315
732
  end
316
733
 
317
- 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
318
735
  @session.find(:css, '#link_blank_href').click
319
736
  sleep 1
320
737
  expect(@session).to have_current_path('/with_html')
321
738
  end
322
739
 
323
- it "should be able to check a checkbox" do
740
+ it 'should be able to check a checkbox' do
324
741
  @session.visit('form')
325
742
  cbox = @session.find(:checkbox, 'form_terms_of_use')
326
743
  expect(cbox).not_to be_checked
@@ -328,7 +745,7 @@ Capybara::SpecHelper.spec "node" do
328
745
  expect(cbox).to be_checked
329
746
  end
330
747
 
331
- it "should be able to uncheck a checkbox" do
748
+ it 'should be able to uncheck a checkbox' do
332
749
  @session.visit('/form')
333
750
  cbox = @session.find(:checkbox, 'form_pets_dog')
334
751
  expect(cbox).to be_checked
@@ -336,66 +753,458 @@ Capybara::SpecHelper.spec "node" do
336
753
  expect(cbox).not_to be_checked
337
754
  end
338
755
 
339
- it "should be able to select a radio button" do
756
+ it 'should be able to select a radio button' do
340
757
  @session.visit('/form')
341
758
  radio = @session.find(:radio_button, 'gender_male')
342
759
  expect(radio).not_to be_checked
343
760
  radio.click
344
761
  expect(radio).to be_checked
345
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
+ @session.visit('with_js')
779
+ @session.find(:css, '#click-test').click(x: 5, y: 5)
780
+ link = @session.find(:link, 'has-been-clicked')
781
+ locations = link.text.match(/^Has been clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
782
+ # 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
783
+ # integer/float conversions and rounding.
784
+ expect(locations[:x].to_f).to be_within(1).of(5)
785
+ expect(locations[:y].to_f).to be_within(1).of(5)
786
+ end
787
+
788
+ it 'should raise error if both x and y values are not passed' do
789
+ @session.visit('with_js')
790
+ el = @session.find(:css, '#click-test')
791
+ expect { el.click(x: 5) }.to raise_error ArgumentError
792
+ expect { el.click(x: nil, y: 3) }.to raise_error ArgumentError
793
+ end
794
+
795
+ it 'should be able to click a table row', requires: [:js] do
796
+ @session.visit('/tables')
797
+ tr = @session.find(:css, '#agent_table tr:first-child').click
798
+ expect(tr).to have_css('label', text: 'Clicked')
799
+ end
800
+
801
+ it 'should retry clicking', requires: [:js] do
802
+ @session.visit('/animated')
803
+ obscured = @session.find(:css, '#obscured')
804
+ @session.execute_script <<~JS
805
+ setTimeout(function(){ $('#cover').hide(); }, 700)
806
+ JS
807
+ expect { obscured.click }.not_to raise_error
808
+ end
809
+
810
+ it 'should allow to retry longer', requires: [:js] do
811
+ @session.visit('/animated')
812
+ obscured = @session.find(:css, '#obscured')
813
+ @session.execute_script <<~JS
814
+ setTimeout(function(){ $('#cover').hide(); }, 3000)
815
+ JS
816
+ expect { obscured.click(wait: 4) }.not_to raise_error
817
+ end
818
+
819
+ it 'should not retry clicking when wait is disabled', requires: [:js] do
820
+ @session.visit('/animated')
821
+ obscured = @session.find(:css, '#obscured')
822
+ @session.execute_script <<~JS
823
+ setTimeout(function(){ $('#cover').hide(); }, 2000)
824
+ JS
825
+ expect { obscured.click(wait: 0) }.to(raise_error { |e| expect(e).to be_an_invalid_element_error(@session) })
826
+ end
827
+
828
+ context 'offset', requires: [:js] do
829
+ before do
830
+ @session.visit('/offset')
831
+ end
832
+
833
+ let :clicker do
834
+ @session.find(:id, 'clicker')
835
+ end
836
+
837
+ context 'when w3c_click_offset is false' do
838
+ before do
839
+ Capybara.w3c_click_offset = false
840
+ end
841
+
842
+ it 'should offset from top left of element' do
843
+ clicker.click(x: 10, y: 5)
844
+ expect(@session).to have_text(/clicked at 110,105/)
845
+ end
846
+
847
+ it 'should offset outside the element' do
848
+ clicker.click(x: -15, y: -10)
849
+ expect(@session).to have_text(/clicked at 85,90/)
850
+ end
851
+
852
+ it 'should default to click the middle' do
853
+ clicker.click
854
+ expect(@session).to have_text(/clicked at 150,150/)
855
+ end
856
+ end
857
+
858
+ context 'when w3c_click_offset is true' do
859
+ before do
860
+ Capybara.w3c_click_offset = true
861
+ end
862
+
863
+ it 'should offset from center of element' do
864
+ clicker.click(x: 10, y: 5)
865
+ expect(@session).to have_text(/clicked at 160,155/)
866
+ end
867
+
868
+ it 'should offset outside from center of element' do
869
+ clicker.click(x: -65, y: -60)
870
+ expect(@session).to have_text(/clicked at 85,90/)
871
+ end
872
+
873
+ it 'should default to click the middle' do
874
+ clicker.click
875
+ expect(@session).to have_text(/clicked at 150,150/)
876
+ end
877
+ end
878
+ end
879
+
880
+ context 'delay', requires: [:js] do
881
+ it 'should delay the mouse up' do
882
+ @session.visit('with_js')
883
+ @session.find(:css, '#click-test').click(delay: 2)
884
+ delay = @session.evaluate_script('window.click_delay')
885
+ expect(delay).to be >= 2
886
+ end
887
+ end
346
888
  end
347
889
 
348
890
  describe '#double_click', requires: [:js] do
349
- it "should double click an element" do
350
- pending "selenium-webdriver/geckodriver doesn't support mouse move_to" if marionette?(@session)
891
+ it 'should double click an element' do
351
892
  @session.visit('/with_js')
352
893
  @session.find(:css, '#click-test').double_click
353
- expect(@session.find(:css, '#has-been-double-clicked')).to be
894
+ expect(@session.find(:css, '#has-been-double-clicked')).to be_truthy
895
+ end
896
+
897
+ it 'should allow modifiers', requires: [:js] do
898
+ @session.visit('/with_js')
899
+ @session.find(:css, '#click-test').double_click(:alt)
900
+ expect(@session).to have_link('Has been alt double clicked')
901
+ end
902
+
903
+ it 'should allow to adjust the offset', requires: [:js] do
904
+ @session.visit('with_js')
905
+ @session.find(:css, '#click-test').double_click(x: 10, y: 5)
906
+ link = @session.find(:link, 'has-been-double-clicked')
907
+ locations = link.text.match(/^Has been double clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
908
+ # Resulting click location should be very close to 10, 5 relative to top left corner of the element, but may not be exact due
909
+ # to integer/float conversions and rounding.
910
+ expect(locations[:x].to_f).to be_within(1).of(10)
911
+ expect(locations[:y].to_f).to be_within(1).of(5)
912
+ end
913
+
914
+ it 'should retry clicking', requires: [:js] do
915
+ @session.visit('/animated')
916
+ obscured = @session.find(:css, '#obscured')
917
+ @session.execute_script <<~JS
918
+ setTimeout(function(){ $('#cover').hide(); }, 700)
919
+ JS
920
+ expect { obscured.double_click }.not_to raise_error
921
+ end
922
+
923
+ context 'offset', requires: [:js] do
924
+ before do
925
+ @session.visit('/offset')
926
+ end
927
+
928
+ let :clicker do
929
+ @session.find(:id, 'clicker')
930
+ end
931
+
932
+ context 'when w3c_click_offset is false' do
933
+ before do
934
+ Capybara.w3c_click_offset = false
935
+ end
936
+
937
+ it 'should offset from top left of element' do
938
+ clicker.double_click(x: 10, y: 5)
939
+ expect(@session).to have_text(/clicked at 110,105/)
940
+ end
941
+
942
+ it 'should offset outside the element' do
943
+ clicker.double_click(x: -15, y: -10)
944
+ expect(@session).to have_text(/clicked at 85,90/)
945
+ end
946
+
947
+ it 'should default to click the middle' do
948
+ clicker.double_click
949
+ expect(@session).to have_text(/clicked at 150,150/)
950
+ end
951
+ end
952
+
953
+ context 'when w3c_click_offset is true' do
954
+ before do
955
+ Capybara.w3c_click_offset = true
956
+ end
957
+
958
+ it 'should offset from center of element' do
959
+ clicker.double_click(x: 10, y: 5)
960
+ expect(@session).to have_text(/clicked at 160,155/)
961
+ end
962
+
963
+ it 'should offset outside from center of element' do
964
+ clicker.double_click(x: -65, y: -60)
965
+ expect(@session).to have_text(/clicked at 85,90/)
966
+ end
967
+
968
+ it 'should default to click the middle' do
969
+ clicker.double_click
970
+ expect(@session).to have_text(/clicked at 150,150/)
971
+ end
972
+ end
354
973
  end
355
974
  end
356
975
 
357
976
  describe '#right_click', requires: [:js] do
358
- it "should right click an element" do
359
- pending "selenium-webdriver/geckodriver doesn't support mouse move_to" if marionette?(@session)
977
+ it 'should right click an element' do
360
978
  @session.visit('/with_js')
361
979
  @session.find(:css, '#click-test').right_click
362
- expect(@session.find(:css, '#has-been-right-clicked')).to be
980
+ expect(@session.find(:css, '#has-been-right-clicked')).to be_truthy
981
+ end
982
+
983
+ it 'should allow modifiers', requires: [:js] do
984
+ @session.visit('/with_js')
985
+ @session.find(:css, '#click-test').right_click(:meta)
986
+ expect(@session).to have_link('Has been meta right clicked')
987
+ end
988
+
989
+ it 'should allow to adjust the offset', requires: [:js] do
990
+ @session.visit('with_js')
991
+ @session.find(:css, '#click-test').right_click(x: 10, y: 10)
992
+ link = @session.find(:link, 'has-been-right-clicked')
993
+ locations = link.text.match(/^Has been right clicked at (?<x>[\d.-]+),(?<y>[\d.-]+)$/)
994
+ # Resulting click location should be very close to 10, 10 relative to top left corner of the element, but may not be exact due
995
+ # to integer/float conversions and rounding
996
+ expect(locations[:x].to_f).to be_within(1).of(10)
997
+ expect(locations[:y].to_f).to be_within(1).of(10)
998
+ end
999
+
1000
+ it 'should retry clicking', requires: [:js] do
1001
+ @session.visit('/animated')
1002
+ obscured = @session.find(:css, '#obscured')
1003
+ @session.execute_script <<~JS
1004
+ setTimeout(function(){ $('#cover').hide(); }, 700)
1005
+ JS
1006
+ expect { obscured.right_click }.not_to raise_error
1007
+ end
1008
+
1009
+ context 'offset', requires: [:js] do
1010
+ before do
1011
+ @session.visit('/offset')
1012
+ end
1013
+
1014
+ let :clicker do
1015
+ @session.find(:id, 'clicker')
1016
+ end
1017
+
1018
+ context 'when w3c_click_offset is false' do
1019
+ before do
1020
+ Capybara.w3c_click_offset = false
1021
+ end
1022
+
1023
+ it 'should offset from top left of element' do
1024
+ clicker.right_click(x: 10, y: 5)
1025
+ expect(@session).to have_text(/clicked at 110,105/)
1026
+ end
1027
+
1028
+ it 'should offset outside the element' do
1029
+ clicker.right_click(x: -15, y: -10)
1030
+ expect(@session).to have_text(/clicked at 85,90/)
1031
+ end
1032
+
1033
+ it 'should default to click the middle' do
1034
+ clicker.right_click
1035
+ expect(@session).to have_text(/clicked at 150,150/)
1036
+ end
1037
+ end
1038
+
1039
+ context 'when w3c_click_offset is true' do
1040
+ before do
1041
+ Capybara.w3c_click_offset = true
1042
+ end
1043
+
1044
+ it 'should offset from center of element' do
1045
+ clicker.right_click(x: 10, y: 5)
1046
+ expect(@session).to have_text(/clicked at 160,155/)
1047
+ end
1048
+
1049
+ it 'should offset outside from center of element' do
1050
+ clicker.right_click(x: -65, y: -60)
1051
+ expect(@session).to have_text(/clicked at 85,90/)
1052
+ end
1053
+
1054
+ it 'should default to click the middle' do
1055
+ clicker.right_click
1056
+ expect(@session).to have_text(/clicked at 150,150/)
1057
+ end
1058
+ end
1059
+ end
1060
+
1061
+ context 'delay', requires: [:js] do
1062
+ it 'should delay the mouse up' do
1063
+ @session.visit('with_js')
1064
+ @session.find(:css, '#click-test').right_click(delay: 2)
1065
+ delay = @session.evaluate_script('window.right_click_delay')
1066
+ expect(delay).to be >= 2
1067
+ end
363
1068
  end
364
1069
  end
365
1070
 
366
1071
  describe '#send_keys', requires: [:send_keys] do
367
- it "should send a string of keys to an element" do
1072
+ it 'should send a string of keys to an element' do
368
1073
  @session.visit('/form')
369
1074
  @session.find(:css, '#address1_city').send_keys('Oceanside')
370
1075
  expect(@session.find(:css, '#address1_city').value).to eq 'Oceanside'
371
1076
  end
372
1077
 
373
- it "should send special characters" do
374
- pending "selenium-webdriver/geckodriver doesn't support complex sets of characters" if marionette?(@session)
1078
+ it 'should send special characters' do
375
1079
  @session.visit('/form')
376
1080
  @session.find(:css, '#address1_city').send_keys('Ocean', :space, 'sie', :left, 'd')
377
1081
  expect(@session.find(:css, '#address1_city').value).to eq 'Ocean side'
378
1082
  end
379
1083
 
380
- it "should allow for multiple simultaneous keys" do
381
- pending "selenium-webdriver/geckodriver doesn't support complex sets of characters" if marionette?(@session)
1084
+ it 'should allow for multiple simultaneous keys' do
382
1085
  @session.visit('/form')
383
1086
  @session.find(:css, '#address1_city').send_keys([:shift, 'o'], 'ceanside')
384
1087
  expect(@session.find(:css, '#address1_city').value).to eq 'Oceanside'
385
1088
  end
386
1089
 
387
- it "should generate key events", requires: [:send_keys, :js] do
388
- pending "selenium-webdriver/geckodriver doesn't support complex sets of characters" if marionette?(@session)
1090
+ it 'should hold modifiers at top level' do
1091
+ @session.visit('/form')
1092
+ @session.find(:css, '#address1_city').send_keys('ocean', :shift, 'side')
1093
+ expect(@session.find(:css, '#address1_city').value).to eq 'oceanSIDE'
1094
+ end
1095
+
1096
+ it 'should generate key events', requires: %i[send_keys js] do
389
1097
  @session.visit('/with_js')
390
- @session.find(:css, '#with-key-events').send_keys([:shift,'t'], [:shift,'w'])
1098
+ @session.find(:css, '#with-key-events').send_keys([:shift, 't'], [:shift, 'w'])
391
1099
  expect(@session.find(:css, '#key-events-output')).to have_text('keydown:16 keydown:84 keydown:16 keydown:87')
392
1100
  end
393
1101
  end
394
1102
 
1103
+ describe '#execute_script', requires: %i[js es_args] do
1104
+ it 'should execute the given script in the context of the element and return nothing' do
1105
+ @session.visit('/with_js')
1106
+ expect(@session.find(:css, '#change').execute_script("this.textContent = 'Funky Doodle'")).to be_nil
1107
+ expect(@session).to have_css('#change', text: 'Funky Doodle')
1108
+ end
1109
+
1110
+ it 'should pass arguments to the script' do
1111
+ @session.visit('/with_js')
1112
+ @session.find(:css, '#change').execute_script('this.textContent = arguments[0]', 'Doodle Funk')
1113
+ expect(@session).to have_css('#change', text: 'Doodle Funk')
1114
+ end
1115
+ end
1116
+
1117
+ describe '#evaluate_script', requires: %i[js es_args] do
1118
+ it 'should evaluate the given script in the context of the element and return whatever it produces' do
1119
+ @session.visit('/with_js')
1120
+ el = @session.find(:css, '#with_change_event')
1121
+ expect(el.evaluate_script('this.value')).to eq('default value')
1122
+ end
1123
+
1124
+ it 'should ignore leading whitespace' do
1125
+ @session.visit('/with_js')
1126
+ expect(@session.find(:css, '#change').evaluate_script('
1127
+ 2 + 3
1128
+ ')).to eq 5
1129
+ end
1130
+
1131
+ it 'should pass arguments to the script' do
1132
+ @session.visit('/with_js')
1133
+ @session.find(:css, '#change').evaluate_script('this.textContent = arguments[0]', 'Doodle Funk')
1134
+ expect(@session).to have_css('#change', text: 'Doodle Funk')
1135
+ end
1136
+
1137
+ it 'should pass multiple arguments' do
1138
+ @session.visit('/with_js')
1139
+ change = @session.find(:css, '#change')
1140
+ expect(change.evaluate_script('arguments[0] + arguments[1]', 2, 3)).to eq 5
1141
+ end
1142
+
1143
+ it 'should support returning elements' do
1144
+ @session.visit('/with_js')
1145
+ change = @session.find(:css, '#change') # ensure page has loaded and element is available
1146
+ el = change.evaluate_script('this')
1147
+ expect(el).to be_instance_of(Capybara::Node::Element)
1148
+ expect(el).to eq(change)
1149
+ end
1150
+
1151
+ it 'should support multiple statements via IIFE' do
1152
+ @session.visit('/with_js')
1153
+ change = @session.find(:css, '#change') # ensure page has loaded and element is available
1154
+ res = change.evaluate_script(<<~JS, 3)
1155
+ (function(n){
1156
+ var el = this;
1157
+ return [el, n];
1158
+ }).apply(this, arguments)
1159
+ JS
1160
+ expect(res).to eq [change, 3]
1161
+ end
1162
+ end
1163
+
1164
+ describe '#evaluate_async_script', requires: %i[js es_args] do
1165
+ it 'should evaluate the given script in the context of the element' do
1166
+ @session.visit('/with_js')
1167
+ el = @session.find(:css, '#with_change_event')
1168
+ expect(el.evaluate_async_script('arguments[0](this.value)')).to eq('default value')
1169
+ end
1170
+
1171
+ it 'should support returning elements after asynchronous operation' do
1172
+ @session.visit('/with_js')
1173
+ change = @session.find(:css, '#change') # ensure page has loaded and element is available
1174
+ el = change.evaluate_async_script('var cb = arguments[0]; setTimeout(function(el){ cb(el) }, 100, this)')
1175
+ expect(el).to eq(change)
1176
+ end
1177
+ end
1178
+
395
1179
  describe '#reload', requires: [:js] do
396
- context "without automatic reload" do
1180
+ it 'should reload elements found via ancestor with CSS' do
1181
+ @session.visit('/with_js')
1182
+ node = @session.find(:css, '#reload-me em').ancestor(:css, 'div')
1183
+ node.reload
1184
+ expect(node[:id]).to eq 'reload-me'
1185
+ end
1186
+
1187
+ it 'should reload elements found via ancestor with XPath' do
1188
+ @session.visit('/with_js')
1189
+ node = @session.find(:css, '#reload-me em').ancestor(:xpath, './/div')
1190
+ node.reload
1191
+ expect(node[:id]).to eq 'reload-me'
1192
+ end
1193
+
1194
+ it 'should reload elements found via sibling' do
1195
+ @session.visit('/with_js')
1196
+ node = @session.find(:css, '#the-list li', text: 'Item 1').sibling(:css, 'li')
1197
+ expect(node.text).to eq 'Item 2'
1198
+ node.reload
1199
+ expect(node.text).to eq 'Item 2'
1200
+ end
1201
+
1202
+ context 'without automatic reload' do
397
1203
  before { Capybara.automatic_reload = false }
398
- it "should reload the current context of the node" do
1204
+
1205
+ after { Capybara.automatic_reload = true }
1206
+
1207
+ it 'should reload the current context of the node' do
399
1208
  @session.visit('/with_js')
400
1209
  node = @session.find(:css, '#reload-me')
401
1210
  @session.click_link('Reload!')
@@ -404,7 +1213,7 @@ Capybara::SpecHelper.spec "node" do
404
1213
  expect(node.text).to eq('has been reloaded')
405
1214
  end
406
1215
 
407
- it "should reload a parent node" do
1216
+ it 'should reload a parent node' do
408
1217
  @session.visit('/with_js')
409
1218
  node = @session.find(:css, '#reload-me').find(:css, 'em')
410
1219
  @session.click_link('Reload!')
@@ -413,26 +1222,25 @@ Capybara::SpecHelper.spec "node" do
413
1222
  expect(node.text).to eq('has been reloaded')
414
1223
  end
415
1224
 
416
- it "should not automatically reload" do
1225
+ it 'should not automatically reload' do
417
1226
  @session.visit('/with_js')
418
1227
  node = @session.find(:css, '#reload-me')
419
1228
  @session.click_link('Reload!')
420
1229
  sleep(0.3)
421
1230
  expect do
422
1231
  expect(node).to have_text('has been reloaded')
423
- end.to raise_error do |error|
1232
+ end.to(raise_error do |error|
424
1233
  expect(error).to be_an_invalid_element_error(@session)
425
- end
1234
+ end)
426
1235
  end
427
- after { Capybara.automatic_reload = true }
428
1236
  end
429
1237
 
430
- context "with automatic reload" do
1238
+ context 'with automatic reload' do
431
1239
  before do
432
1240
  Capybara.default_max_wait_time = 4
433
1241
  end
434
1242
 
435
- it "should reload the current context of the node automatically" do
1243
+ it 'should reload the current context of the node automatically' do
436
1244
  @session.visit('/with_js')
437
1245
  node = @session.find(:css, '#reload-me')
438
1246
  @session.click_link('Reload!')
@@ -440,7 +1248,7 @@ Capybara::SpecHelper.spec "node" do
440
1248
  expect(node.text).to eq('has been reloaded')
441
1249
  end
442
1250
 
443
- it "should reload a parent node automatically" do
1251
+ it 'should reload a parent node automatically' do
444
1252
  @session.visit('/with_js')
445
1253
  node = @session.find(:css, '#reload-me').find(:css, 'em')
446
1254
  @session.click_link('Reload!')
@@ -448,7 +1256,7 @@ Capybara::SpecHelper.spec "node" do
448
1256
  expect(node.text).to eq('has been reloaded')
449
1257
  end
450
1258
 
451
- it "should reload a node automatically when using find" do
1259
+ it 'should reload a node automatically when using find' do
452
1260
  @session.visit('/with_js')
453
1261
  node = @session.find(:css, '#reload-me')
454
1262
  @session.click_link('Reload!')
@@ -464,19 +1272,15 @@ Capybara::SpecHelper.spec "node" do
464
1272
 
465
1273
  expect do
466
1274
  expect(node).to have_text('Foo')
467
- end.to raise_error { |error|
468
- expect(error).to be_an_invalid_element_error(@session)
469
- }
1275
+ end.to(raise_error { |error| expect(error).to be_an_invalid_element_error(@session) })
470
1276
  expect do
471
1277
  expect(node).to have_text('Bar')
472
- end.to raise_error { |error|
473
- expect(error).to be_an_invalid_element_error(@session)
474
- }
1278
+ end.to(raise_error { |error| expect(error).to be_an_invalid_element_error(@session) })
475
1279
  end
476
1280
 
477
- it "should reload nodes with options" do
1281
+ it 'should reload nodes with options' do
478
1282
  @session.visit('/with_js')
479
- node = @session.find(:css, 'em', text: "reloaded")
1283
+ node = @session.find(:css, 'em', text: 'reloaded')
480
1284
  @session.click_link('Reload!')
481
1285
  sleep(1)
482
1286
  expect(node.text).to eq('has been reloaded')
@@ -484,31 +1288,25 @@ Capybara::SpecHelper.spec "node" do
484
1288
  end
485
1289
  end
486
1290
 
487
- context "when #synchronize raises server errors" do
488
- it "sets an explanatory exception as the cause of server exceptions", requires: [:server, :js] do
489
- skip "This version of ruby doesn't support exception causes" unless Exception.instance_methods.include? :cause
490
- quietly { @session.visit("/error") }
1291
+ context 'when #synchronize raises server errors' do
1292
+ it 'sets an explanatory exception as the cause of server exceptions', requires: %i[server js] do
1293
+ quietly { @session.visit('/error') }
491
1294
  expect do
492
1295
  @session.find(:css, 'span')
493
- end.to raise_error(TestApp::TestAppError) do |e|
1296
+ end.to(raise_error(TestApp::TestAppError) do |e|
494
1297
  expect(e.cause).to be_a Capybara::CapybaraError
495
1298
  expect(e.cause.message).to match(/Your application server raised an error/)
496
- end
1299
+ end)
497
1300
  end
498
1301
 
499
- it "sets an explanatory exception as the cause of server exceptions with errors with initializers", requires: [:server, :js] do
500
- skip "This version of ruby doesn't support exception causes" unless Exception.instance_methods.include? :cause
501
- quietly { @session.visit("/other_error") }
1302
+ it 'sets an explanatory exception as the cause of server exceptions with errors with initializers', requires: %i[server js] do
1303
+ quietly { @session.visit('/other_error') }
502
1304
  expect do
503
1305
  @session.find(:css, 'span')
504
- end.to raise_error(TestApp::TestAppOtherError) do |e|
1306
+ end.to(raise_error(TestApp::TestAppOtherError) do |e|
505
1307
  expect(e.cause).to be_a Capybara::CapybaraError
506
1308
  expect(e.cause.message).to match(/Your application server raised an error/)
507
- end
1309
+ end)
508
1310
  end
509
1311
  end
510
-
511
- def be_an_invalid_element_error(session)
512
- satisfy { |error| session.driver.invalid_element_errors.any? { |e| error.is_a? e } }
513
- end
514
1312
  end