capybara 3.32.2

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